Solutions 
Affichage de plans de requêtes simples 
Recherche de motif texte 
Affichez le plan de cette requête (sur la base cave). 
 
SELECT  * FROM  appellation WHERE  libelle LIKE  'Brouilly%' ;cave=# explain  SELECT  * FROM  appellation WHERE  libelle LIKE  'Brouilly%' ;
                         QUERY  PLAN 
------------------------------------------------------------ 
 Seq Scan  on  appellation  (cost=0.00 ..6.99  rows=1  width=24 )
   Filter: (libelle ~~ 'Brouilly%' ::text )
(2  lignes)Que constatez-vous ?
Affichez maintenant le nombre de blocs accédés par cette requête. 
 
cave=# explain  (analyze ,buffers) SELECT  * FROM  appellation
cave=# WHERE  libelle LIKE  'Brouilly%' ;
                                      QUERY  PLAN 
-------------------------------------------------------------------------------- 
 Seq Scan  on  appellation  (cost=0.00 ..6.99  rows=1  width=24 )
                          (actual time=0.066 ..0.169  rows=1  loops=1 )
   Filter: (libelle ~~ 'Brouilly%' ::text )
   Rows  Removed by  Filter: 318 
   Buffers: shared  hit=3 
 Total runtime: 0.202  ms
(5  lignes)
Cette requête ne passe pas par un index. Essayez de lui forcer la main. 
 
cave=# set  enable_seqscan TO  off ;
SET 
cave=# explain  (analyze ,buffers) SELECT  * FROM  appellation
cave=# WHERE  libelle LIKE  'Brouilly%' ;
                                      QUERY  PLAN 
-------------------------------------------------------------------------------- 
 Seq Scan  on  appellation  (cost=10000000000.00 ..10000000006.99  rows=1  width=24 )
                          (actual time=0.073 ..0.197  rows=1  loops=1 )
   Filter: (libelle ~~ 'Brouilly%' ::text )
   Rows  Removed by  Filter: 318 
   Buffers: shared  hit=3 
 Total runtime: 0.238  ms
(5  lignes)Passer enable_seqscan à « off » n'interdit pas l'utilisation des scans séquentiels. Il ne fait que les défavoriser fortement : regardez le coût estimé du scan séquentiel.
L'index n'est toujours pas utilisé. L'index « par défaut » n'est pas capable de répondre à des questions sur motif. 
 
En effet, l'index par défaut trie les données par la collation de la colonne de la table. Il lui est impossible de savoir que libelle LIKE 'Brouilly%' est équivalent à libelle >= 'Brouilly' AND libelle < 'Brouillz'. Ce genre de transformation n'est d'ailleurs pas forcément trivial, ni même possible. Il existe dans certaines langues des équivalences (ß et ss en allemand par exemple) qui rendent ce genre de transformation au mieux hasardeuse.
Créez un index capable de ces opérations. Testez à nouveau le plan. 
 
Pour pouvoir répondre à cette question, on doit donc avoir un index spécialisé, qui compare les chaînes non plus par rapport à leur collation, mais à leur valeur binaire (octale en fait).
CREATE  INDEX  appellation_libelle_key_search
    ON  appellation (libelle text_pattern_ops);On indique par cette commande à PostgreSQL de ne plus utiliser la classe d'opérateurs habituelle de comparaison de texte, mais la classe text_pattern_ops, qui est spécialement faite pour les recherches LIKE 'xxxx%' : cette classe ne trie plus les chaînes par leur ordre alphabétique, mais par leur valeur octale.
Si on redemande le plan :
cave=# EXPLAIN  (ANALYZE ,BUFFERS) SELECT  * FROM  appellation
cave=# WHERE  libelle LIKE  'Brouilly%' ;
                                     QUERY  PLAN 
-------------------------------------------------------------------------------- 
 Index  Scan  using  appellation_libelle_key_search on  appellation
                              (cost=0.27 ..8.29  rows=1  width=24 )
                              (actual time=0.057 ..0.059  rows=1  loops=1 )
   Index  Cond: ((libelle ~>=~ 'Brouilly' ::text )
                AND  (libelle ~<~ 'Brouillz' ::text ))
   Filter: (libelle ~~ 'Brouilly%' ::text )
   Buffers: shared  hit=1  read=2 
 Total runtime: 0.108  ms
(5  lignes)On utilise enfin un index.
Réactivez enable_seqscan. Testez à nouveau le plan. 
 
cave=# reset  enable_seqscan ;
RESET 
cave=# explain  (analyze ,buffers) SELECT  * FROM  appellation
cave=# WHERE  libelle LIKE  'Brouilly%' ;
                                    QUERY  PLAN 
-------------------------------------------------------------------------------- 
 Seq Scan  on  appellation  (cost=0.00 ..6.99  rows=1  width=24 )
                          (actual time=0.063 ..0.172  rows=1  loops=1 )
   Filter: (libelle ~~ 'Brouilly%' ::text )
   Rows  Removed by  Filter: 318 
   Buffers: shared  hit=3 
 Total runtime: 0.211  ms
(5  lignes)
Quelle est la conclusion ? 
 
PostgreSQL choisit de ne pas utiliser cet index. Le temps d'exécution est pourtant un peu meilleur avec l'index (60 microsecondes contre 172 microsecondes). Néanmoins, cela n'est vrai que parce que les données sont en cache. En cas de données hors du cache, le plan par parcours séquentiel (seq scan ) est probablement meilleur. Certes il prend plus de temps CPU puisqu'il doit consulter 318 enregistrements inutiles. Par contre, il ne fait qu'un accès à 3 blocs séquentiels (les 3 blocs de la table), ce qui est le plus sûr.
La table est trop petite pour que PostgreSQL considère l'utilisation d'un index.
Recherche de motif texte avancé 
La base cave ne contient pas de données textuelles appropriées, nous allons en utiliser une autre.
Lancez textes.sql ou textes_10pct.sql (préférable sur une machine peu puissante, ou une instance PostgreSQL non paramétrée). 
 
Ce script crée une table textes, contenant le texte intégral d'un grand nombre de livres en français du projet Gutenberg, soit 10 millions de lignes pour 85 millions de mots.
Nous allons rechercher toutes les références à « Fantine » dans les textes. On devrait trouver beaucoup d'enregistrements provenant des « Misérables ».
La méthode SQL standard pour écrire cela est : 
 
SELECT  * FROM  textes WHERE  contenu ILIKE '%fantine%' ;Exécutez cette requête, et regardez son plan d'exécution.
textes=# explain  (analyze ,buffers) SELECT  * FROM  textes
textes=# WHERE  contenu ILIKE '%fantine%' ;
                                       QUERY  PLAN 
----------------------------------------------------------------------------------------------------------------- 
 Seq Scan  on  textes  (cost=0.00 ..325809.40  rows=874  width=102 )
                     (actual time=224.634 ..22567.231  rows=921  loops=1 )
   Filter: (contenu ~~* '%fantine%' ::text )
   Rows  Removed by  Filter: 11421523 
   Buffers: shared  hit=130459  read=58323 
 Total runtime: 22567.679  ms
(5  lignes)Cette requête ne peut pas être optimisée avec les index standard (btree) : c'est une recherche insensible à la casse et avec plusieurs % dont un au début.
Avec GiST 
Créez un index trigramme: 
 
textes=# CREATE  EXTENSION pg_trgm;
textes=# CREATE  INDEX  idx_trgm ON  textes USING  gist (contenu gist_trgm_ops);
CREATE  INDEX 
Temps : 962794 ,399  ms
Quelle est la taille de l'index ? 
 
L'index fait cette taille (pour une table de 1,5Go) :
textes=# select  pg_size_pretty(pg_relation_size('idx_trgm' ));
 pg_size_pretty
---------------- 
 2483  MB
(1  ligne)
Réexécutez la requête. Que constatez-vous ? 
 
textes=# explain  (analyze ,buffers) SELECT  * FROM  textes
textes=# WHERE  contenu ILIKE '%fantine%' ;
                                          QUERY  PLAN 
-------------------------------------------------------------------------------- 
 Bitmap  Heap  Scan  on  textes  (cost=111.49 ..3573.39  rows=912  width=102 )
                             (actual time=1942.872 ..1949.393  rows=922  loops=1 )
   Recheck Cond: (contenu ~~* '%fantine%' ::text )
   Rows  Removed by  Index  Recheck: 75 
   Buffers: shared  hit=16030  read=144183  written=14741 
   ->  Bitmap  Index  Scan  on  idx_trgm  (cost=0.00 ..111.26  rows=912  width=0 )
                            (actual time=1942.671 ..1942.671  rows=997  loops=1 )
         Index  Cond: (contenu ~~* '%fantine%' ::text )
         Buffers: shared  hit=16029  read=143344  written=14662 
 Total runtime: 1949.565  ms
(8  lignes)
Temps : 1951 ,175  msPostgreSQL dispose de mécanismes spécifiques avancés pour certains types de données. Ils ne sont pas toujours installés en standard, mais leur connaissance peut avoir un impact énorme sur les performances.
Le mécanisme GiST est assez efficace pour répondre à ce genre de questions. Il nécessite quand même un accès à un grand nombre de blocs, d'après le plan : 160 000 blocs lus, 15 000 écrits (dans un fichier temporaire, on pourrait s'en débarrasser en augmentant le work_mem). Le gain est donc conséquent, mais pas gigantesque : le plan initial lisait 190 000 blocs. On gagne surtout en temps de calcul, car on accède directement aux bons enregistrements. Le parcours de l'index, par contre, est coûteux.
Avec Gin 
Créez un index trigramme: 
 
textes=# CREATE  EXTENSION pg_trgm;
textes=# CREATE  INDEX  idx_trgm ON  textes USING  gin (contenu gin_trgm_ops);
CREATE  INDEX 
Temps : 591534 ,917  msL'index fait cette taille (pour une table de 1,5Go) :
textes=# select  pg_size_pretty(pg_total_relation_size('textes' ));
 pg_size_pretty
---------------- 
 4346  MB
(1  ligne)L'index est très volumineux.
Réexécutez la requête. Que constatez-vous ? 
 
textes=# explain  (analyze ,buffers) SELECT  * FROM  textes
textes=# WHERE  contenu ILIKE '%fantine%' ;
                                              QUERY  PLAN 
-------------------------------------------------------------------------------- 
 Bitmap  Heap  Scan  on  textes  (cost=103.06 ..3561.22  rows=911  width=102 )
                             (actual time=777.469 ..780.834  rows=921  loops=1 )
   Recheck Cond: (contenu ~~* '%fantine%' ::text )
   Rows  Removed by  Index  Recheck: 75 
   Buffers: shared  hit=2666 
   ->  Bitmap  Index  Scan  on  idx_trgm  (cost=0.00 ..102.83  rows=911  width=0 )
                            (actual time=777.283 ..777.283  rows=996  loops=1 )
         Index  Cond: (contenu ~~* '%fantine%' ::text )
         Buffers: shared  hit=1827 
 Total runtime: 780.954  ms
(8  lignes)PostgreSQL dispose de mécanismes spécifiques avancés pour certains types de données. Ils ne sont pas toujours installés en standard, mais leur connaissance peut avoir un impact énorme sur les performances. Le mécanisme Gin est vraiment très efficace pour répondre à ce genre de questions. Il s'agit de répondre en moins d'une seconde à « quelles lignes contiennent la chaîne "fantine" ? » sur 12 millions de lignes de texte. Les Index Gin sont par contre très coûteux à maintenir. Ici, on n'accède qu'à 2 666 blocs, ce qui est vraiment excellent. Mais l'index est bien plus volumineux que l'index GiST.
Avec le Full Text Search 
Le résultat sera bien sûr différent, et le FTS est moins souple.
Version GiST :
textes=# create  index  idx_fts
          on  textes
          using  gist (to_tsvector('french' ,contenu));
CREATE  INDEX 
Temps : 1807467 ,811  ms
textes=# EXPLAIN  (analyze ,buffers) SELECT  * FROM  textes
textes=# WHERE  to_tsvector('french' ,contenu) @@ to_tsquery('french' ,'fantine' );
                 QUERY  PLAN 
------------------------------------------------------------------------ 
 Bitmap  Heap  Scan  on  textes  (cost=2209.51 ..137275.87  rows=63109  width=97 )
                             (actual time=648.596 ..659.733  rows=311  loops=1 )
   Recheck Cond: (to_tsvector('french' ::regconfig , contenu) @@
                  ' '' fantin '' ' ::tsquery )
   Buffers: shared  hit=37165 
   ->  Bitmap  Index  Scan  on  idx_fts  (cost=0.00 ..2193.74  rows=63109  width=0 )
                                (actual time=648.493 ..648.493  rows=311  loops=1 )
         Index  Cond: (to_tsvector('french' ::regconfig , contenu) @@
                      ' '' fantin '' ' ::tsquery )
         Buffers: shared  hit=37016 
 Total runtime: 659.820  ms
(7  lignes)
Temps : 660 ,364  msEt la taille de l'index :
textes=# select  pg_size_pretty(pg_relation_size('idx_fts' ));
 pg_size_pretty
---------------- 
 671  MB
(1  ligne)Version Gin :
textes=# CREATE  INDEX  idx_fts ON  textes
textes=# USING  gin (to_tsvector('french' ,contenu));
CREATE  INDEX 
Temps : 491499 ,599  ms
textes=# EXPLAIN  (analyze ,buffers) SELECT  * FROM  textes
textes=# WHERE  to_tsvector('french' ,contenu) @@ to_tsquery('french' ,'fantine' );
                                                       QUERY  PLAN 
------------------------------------------------------------------------------ 
 Bitmap  Heap  Scan  on  textes  (cost=693.10 ..135759.45  rows=63109  width=97 )
                             (actual time=0.278 ..0.699  rows=311  loops=1 )
   Recheck Cond: (to_tsvector('french' ::regconfig , contenu) @@
                  ' '' fantin '' ' ::tsquery )
   Buffers: shared  hit=153 
   ->  Bitmap  Index  Scan  on  idx_fts  (cost=0.00 ..677.32  rows=63109  width=0 )
                                     (actual time=0.222 ..0.222  rows=311  loops=1 )
         Index  Cond: (to_tsvector('french' ::regconfig , contenu) @@
                      ' '' fantin '' ' ::tsquery )
         Buffers: shared  hit=4 
 Total runtime: 0.793  ms
(7  lignes)
Temps : 1 ,534  msTaille de l'index :
textes=# select  pg_size_pretty(pg_relation_size('idx_fts' ));
 pg_size_pretty
---------------- 
 593  MB
(1  ligne)On constate donc que le Full Text Search est bien plus efficace que le trigramme, du moins pour le Full Text Search + Gin : trouver 1 mot parmi plus de cent millions, dans 300 endroits différents dure 1,5 ms.
Par contre, le trigramme permet des recherches floues (orthographe approximative), et des recherches sur autre chose que des mots, même si ces points ne sont pas abordés ici.
Optimisation d'une requête 
Optimisation 1 
Nous travaillerons sur la requête contenue dans le fichier requete1.sql pour cet exercice:
-- \timing 
-- explain analyze 
select 
      m.annee||' - ' ||a.libelle as  millesime_region,
      sum (s.nombre) as  contenants,
      sum (s.nombre*c.contenance) as  litres
from 
     contenant c
     join  stock s
       on  s.contenant_id = c.id
     join  (select  round (random ()*50 )+1950  as  annee) m
       on  s.annee =  m.annee
     join  vin v
       on  s.vin_id = v.id
     left  join  appellation a
     on  v.appellation_id = a.id
group  by  m.annee||' - ' ||a.libelle;L'exécution de la requête donne le plan suivant, avec un temps qui peut varier en fonction de la machine utilisée et de son activité:
HashAggregate  (cost=12763.56..12773.13 rows=319 width=32)
               (actual time=1542.472..1542.879 rows=319 loops=1)
   ->  Hash Left Join  (cost=184.59..12741.89 rows=2889 width=32)
                       (actual time=180.263..1520.812 rows=11334 loops=1)
         Hash Cond: (v.appellation_id = a.id)
         ->  Hash Join  (cost=174.42..12663.10 rows=2889 width=20)
                        (actual time=179.426..1473.270 rows=11334 loops=1)
               Hash Cond: (s.contenant_id = c.id)
               ->  Hash Join  (cost=173.37..12622.33 rows=2889 width=20)
                              (actual time=179.401..1446.687 rows=11334 loops=1)
                     Hash Cond: (s.vin_id = v.id)
                     ->  Hash Join  (cost=0.04..12391.22 rows=2889 width=20)
                            (actual time=164.388..1398.643 rows=11334 loops=1)
                           Hash Cond: ((s.annee)::double precision =
                                ((round((random() * 50::double precision)) +
                                1950::double precision)))
                           ->  Seq Scan on stock s
                                (cost=0.00..9472.86 rows=577886 width=16)
                                (actual time=0.003..684.039 rows=577886 loops=1)
                           ->  Hash  (cost=0.03..0.03 rows=1 width=8)
                                     (actual time=0.009..0.009 rows=1 loops=1)
                                 Buckets: 1024  Batches: 1  Memory Usage: 1kB
                                 ->  Result  (cost=0.00..0.02 rows=1 width=0)
                                     (actual time=0.005..0.006 rows=1 loops=1)
                     ->  Hash  (cost=97.59..97.59 rows=6059 width=8)
                               (actual time=14.987..14.987 rows=6059 loops=1)
                           Buckets: 1024  Batches: 1  Memory Usage: 237kB
                           ->  Seq Scan on vin v
                                (cost=0.00..97.59 rows=6059 width=8)
                                (actual time=0.009..7.413 rows=6059 loops=1)
               ->  Hash  (cost=1.02..1.02 rows=2 width=8)
                         (actual time=0.013..0.013 rows=2 loops=1)
                     Buckets: 1024  Batches: 1  Memory Usage: 1kB
                     ->  Seq Scan on contenant c
                            (cost=0.00..1.02 rows=2 width=8)
                            (actual time=0.003..0.005 rows=2 loops=1)
         ->  Hash  (cost=6.19..6.19 rows=319 width=20)
                   (actual time=0.806..0.806 rows=319 loops=1)
               Buckets: 1024  Batches: 1  Memory Usage: 17kB
               ->  Seq Scan on appellation a
                    (cost=0.00..6.19 rows=319 width=20)
                    (actual time=0.004..0.379 rows=319 loops=1)
 Total runtime: 1543.242 ms
(23 rows)Nous créons à présent un index sur stock.annee comme suit :
create  index  stock_annee on  stock (annee) ;Et exécutons à nouveau la requête. Hélas nous constatons que rien ne change, ni le plan, ni le temps pris par la requête.
Nous n'avons pas lancé ANALYZE, cela explique que l'optimiseur n'utilise pas l'index : il n'en a pas encore la connaissance.
Le plan n'a toujours pas changé ! Ni le temps d'exécution ?!
Interdisons donc de faire les seq scans  à l'optimiseur :
SET  ENABLE_SEQSCAN TO  OFF ;Nous remarquons que le plan d'exécution est encore pire :
 HashAggregate  (cost=40763.39..40772.96 rows=319 width=32)
                (actual time=2022.971..2023.390 rows=319 loops=1)
   ->  Hash Left Join  (cost=313.94..40741.72 rows=2889 width=32)
                       (actual time=18.149..1995.889 rows=11299 loops=1)
         Hash Cond: (v.appellation_id = a.id)
         ->  Hash Join  (cost=290.92..40650.09 rows=2889 width=20)
                        (actual time=17.172..1937.644 rows=11299 loops=1)
               Hash Cond: (s.vin_id = v.id)
               ->  Nested Loop  (cost=0.04..40301.43 rows=2889 width=20)
                                (actual time=0.456..1882.531 rows=11299 loops=1)
                     Join Filter: (s.contenant_id = c.id)
                     ->  Hash Join  (cost=0.04..40202.48 rows=2889 width=20)
                                (actual time=0.444..1778.149 rows=11299 loops=1)
                           Hash Cond: ((s.annee)::double precision =
                                ((round((random() * 50::double precision)) +
                                1950::double precision)))
                           ->  Index Scan using stock_pkey on stock s
                                (cost=0.00..37284.12 rows=577886 width=16)
                                (actual time=0.009..1044.061 rows=577886 loops=1)
                           ->  Hash  (cost=0.03..0.03 rows=1 width=8)
                                     (actual time=0.011..0.011 rows=1 loops=1)
                                 Buckets: 1024  Batches: 1  Memory Usage: 1kB
                                 ->  Result  (cost=0.00..0.02 rows=1 width=0)
                                      (actual time=0.005..0.006 rows=1 loops=1)
                     ->  Materialize  (cost=0.00..12.29 rows=2 width=8)
                                (actual time=0.001..0.003 rows=2 loops=11299)
                           ->  Index Scan using contenant_pkey on contenant c
                                    (cost=0.00..12.28 rows=2 width=8)
                                    (actual time=0.004..0.010 rows=2 loops=1)
               ->  Hash  (cost=215.14..215.14 rows=6059 width=8)
                         (actual time=16.699..16.699 rows=6059 loops=1)
                     Buckets: 1024  Batches: 1  Memory Usage: 237kB
                     ->  Index Scan using vin_pkey on vin v
                            (cost=0.00..215.14 rows=6059 width=8)
                            (actual time=0.010..8.871 rows=6059 loops=1)
         ->  Hash  (cost=19.04..19.04 rows=319 width=20)
                   (actual time=0.936..0.936 rows=319 loops=1)
               Buckets: 1024  Batches: 1  Memory Usage: 17kB
               ->  Index Scan using appellation_pkey on appellation a
                        (cost=0.00..19.04 rows=319 width=20)
                        (actual time=0.016..0.461 rows=319 loops=1)
 Total runtime: 2023.742 ms
(22 rows)Que faire alors ?
Il convient d'autoriser à nouveau les seq scan , puis, peut-être, de réécrire la requête.
Nous réécrivons la requête comme suit (fichier requete2.sql) :
explain  analyze 
select 
      s.annee||' - ' ||a.libelle as  millesime_region,
      sum (s.nombre) as  contenants,
      sum (s.nombre*c.contenance) as  litres
from 
     contenant c
     join  stock s
       on  s.contenant_id = c.id
     join  vin v
       on  s.vin_id = v.id
     left  join  appellation a
       on  v.appellation_id = a.id
where  s.annee =  (select  round (random ()*50 )+1950  as  annee)
group  by  s.annee||' - ' ||a.libelle;Il y a une jointure en moins, ce qui est toujours appréciable. Nous pouvons faire cette réécriture parce que la requête select round(random()*50)+1950 as annee ne ramène qu'un seul enregistrement.
Voici le résultat :
 HashAggregate  (cost=12734.64..12737.10 rows=82 width=28)
                (actual time=265.899..266.317 rows=319 loops=1)
   InitPlan 1 (returns $0)
     ->  Result  (cost=0.00..0.02 rows=1 width=0)
                 (actual time=0.005..0.006 rows=1 loops=1)
   ->  Hash Left Join  (cost=184.55..12712.96 rows=2889 width=28)
                       (actual time=127.787..245.314 rows=11287 loops=1)
         Hash Cond: (v.appellation_id = a.id)
         ->  Hash Join  (cost=174.37..12634.17 rows=2889 width=16)
                        (actual time=126.950..208.077 rows=11287 loops=1)
               Hash Cond: (s.contenant_id = c.id)
               ->  Hash Join  (cost=173.33..12593.40 rows=2889 width=16)
                              (actual time=126.925..181.867 rows=11287 loops=1)
                     Hash Cond: (s.vin_id = v.id)
                     ->  Seq Scan on stock s
                            (cost=0.00..12362.29 rows=2889 width=16)
                            (actual time=112.101..135.932 rows=11287 loops=1)
                           Filter: ((annee)::double precision = $0)
                     ->  Hash  (cost=97.59..97.59 rows=6059 width=8)
                               (actual time=14.794..14.794 rows=6059 loops=1)
                           Buckets: 1024  Batches: 1  Memory Usage: 237kB
                           ->  Seq Scan on vin v
                                (cost=0.00..97.59 rows=6059 width=8)
                                (actual time=0.010..7.321 rows=6059 loops=1)
               ->  Hash  (cost=1.02..1.02 rows=2 width=8)
                         (actual time=0.013..0.013 rows=2 loops=1)
                     Buckets: 1024  Batches: 1  Memory Usage: 1kB
                     ->  Seq Scan on contenant c
                            (cost=0.00..1.02 rows=2 width=8)
                            (actual time=0.004..0.006 rows=2 loops=1)
         ->  Hash  (cost=6.19..6.19 rows=319 width=20)
                   (actual time=0.815..0.815 rows=319 loops=1)
               Buckets: 1024  Batches: 1  Memory Usage: 17kB
               ->  Seq Scan on appellation a
                    (cost=0.00..6.19 rows=319 width=20)
                    (actual time=0.004..0.387 rows=319 loops=1)
 Total runtime: 266.663 ms
(21 rows)Nous sommes ainsi passés de 2 s à 250 ms : la requête est donc environ 10 fois plus rapide.
Que peut-on conclure de cet exercice ?
que la création d'un index est une bonne idée ; cependant l'optimiseur peut ne pas l'utiliser, pour de bonnes raisons ;
qu'interdire les seq scan  est toujours une mauvaise idée (ne présumez pas de votre supériorité sur l'optimiseur !)
 
Optimisation 2 
Voici la requête 2 telle que nous l'avons trouvé dans l'exercice précédent :
explain  analyze 
select 
      s.annee||' - ' ||a.libelle as  millesime_region,
      sum (s.nombre) as  contenants,
      sum (s.nombre*c.contenance) as  litres
from 
     contenant c
     join  stock s
       on  s.contenant_id = c.id
     join  vin v
       on  s.vin_id = v.id
     left  join  appellation a
       on  v.appellation_id = a.id
where  s.annee =  (select  round (random ()*50 )+1950  as  annee)
group  by  s.annee||' - ' ||a.libelle;On peut se demander si la jointure externe (LEFT JOIN) est fondée... On va donc vérifier l'utilité de la ligne suivante :
vin v left  join  appellation a on  v.appellation_id = a.idCela se traduit par « récupérer tous les tuples de la table vin, et pour chaque correspondance dans appellation, la récupérer, si elle existe ».
En regardant la description de la table vin (\d vin dans psql), on remarque la contrainte de clé étrangère suivante :
« vin_appellation_id_fkey »
  FOREIGN KEY (appellation_id)
  REFERENCES appellation(id)Cela veut dire qu'on a la certitude que pour chaque vin, si une référence à la table appellation est présente, elle est nécessairement vérifiable.
De plus, on remarque :
appellation_id | integer | not nullCe qui veut dire que la valeur de ce champ ne peut être nulle. Elle contient donc obligatoirement une valeur qui est présente dans la table appellation.
On peut vérifier au niveau des tuples en faisant un COUNT(*) du résultat, une fois en INNER JOIN et une fois en LEFT JOIN. Si le résultat est identique, la jointure externe ne sert à rien :
select  count (*)
from  vin v
  inner  join  appellation a on  (v.appellation_id = a.id);
 count 
------- 
  6057 select  count (*)
from  vin v
  left  join  appellation a on  (v.appellation_id = a.id);
 count 
------- 
  6057 On peut donc réécrire la requête 2 sans la jointure externe inutile, comme on vient de le démontrer :
explain  analyze 
select 
      s.annee||' - ' ||a.libelle as  millesime_region,
      sum (s.nombre) as  contenants,
      sum (s.nombre*c.contenance) as  litres
from 
     contenant c
     join  stock s
       on  s.contenant_id = c.id
     join  vin v
       on  s.vin_id = v.id
     join  appellation a
       on  v.appellation_id = a.id
where  s.annee =  (select  round (random ()*50 )+1950  as  annee)
group  by  s.annee||' - ' ||a.libelle;Voici le résultat :
 HashAggregate  (cost=12734.64..12737.10 rows=82 width=28)
                (actual time=266.916..267.343 rows=319 loops=1)
   InitPlan 1 (returns $0)
     ->  Result  (cost=0.00..0.02 rows=1 width=0)
                 (actual time=0.005..0.006 rows=1 loops=1)
   ->  Hash Join  (cost=184.55..12712.96 rows=2889 width=28)
                  (actual time=118.759..246.391 rows=11299 loops=1)
         Hash Cond: (v.appellation_id = a.id)
         ->  Hash Join  (cost=174.37..12634.17 rows=2889 width=16)
                        (actual time=117.933..208.503 rows=11299 loops=1)
               Hash Cond: (s.contenant_id = c.id)
               ->  Hash Join  (cost=173.33..12593.40 rows=2889 width=16)
                              (actual time=117.914..182.501 rows=11299 loops=1)
                     Hash Cond: (s.vin_id = v.id)
                     ->  Seq Scan on stock s
                            (cost=0.00..12362.29 rows=2889 width=16)
                            (actual time=102.903..135.451 rows=11299 loops=1)
                           Filter: ((annee)::double precision = $0)
                     ->  Hash  (cost=97.59..97.59 rows=6059 width=8)
                               (actual time=14.979..14.979 rows=6059 loops=1)
                           Buckets: 1024  Batches: 1  Memory Usage: 237kB
                           ->  Seq Scan on vin v
                                (cost=0.00..97.59 rows=6059 width=8)
                                (actual time=0.010..7.387 rows=6059 loops=1)
               ->  Hash  (cost=1.02..1.02 rows=2 width=8)
                         (actual time=0.009..0.009 rows=2 loops=1)
                     Buckets: 1024  Batches: 1  Memory Usage: 1kB
                     ->  Seq Scan on contenant c
                            (cost=0.00..1.02 rows=2 width=8)
                            (actual time=0.002..0.004 rows=2 loops=1)
         ->  Hash  (cost=6.19..6.19 rows=319 width=20)
                   (actual time=0.802..0.802 rows=319 loops=1)
               Buckets: 1024  Batches: 1  Memory Usage: 17kB
               ->  Seq Scan on appellation a
                    (cost=0.00..6.19 rows=319 width=20)
                    (actual time=0.004..0.397 rows=319 loops=1)
 Total runtime: 267.688 ms
(21 rows)Cette réécriture n'a pas d'effet sur le temps d'exécution de la requête dans notre cas. Mais il est probable qu'avec des cardinalités différentes dans la base, cette réécriture aurait eu un impact. Remplacer un LEFT JOIN par un JOIN est le plus souvent intéressant, car il laisse davantage de liberté au moteur sur le sens de planification des requêtes.
Optimisation 3 
Si on observe attentivement le plan, on constate qu'on a toujours le parcours séquentiel de la table stock, qui est notre plus grosse table. Pourquoi a-t-il lieu ?
Si on regarde le filtre (ligne Filter) du parcours de la table stock, on constate qu'il est écrit :
  Filter: ((annee)::double precision = $0)Ceci signifie que pour tous les enregistrements de la table, l'année est convertie en nombre en double précision (un nombre à virgule flottante), afin d'être comparée à $0, une valeur filtre appliquée à la table. Cette valeur est le résultat du calcul :
select  round (random ()*50 )+1950  as  anneecomme indiquée par le début du plan (les lignes de l'initplan 1).
Pourquoi compare-t-il l'année, déclarée comme un entier (integer), en la convertissant en un nombre à virgule flottante ?
Parce que la fonction round() retourne un nombre à virgule flottante. La somme d'un nombre à virgule flottante et d'un entier est évidemment un nombre à virgule flottante. Si on veut que la fonction round() retourne un entier, il faut forcer explicitement sa conversion, via CAST(xxx as int) ou ::int.
Réécrivons encore une fois cette requête :
explain  analyze 
select 
      s.annee||' - ' ||a.libelle as  millesime_region,
      sum (s.nombre) as  contenants,
      sum (s.nombre*c.contenance) as  litres
from 
     contenant c
     join  stock s
       on  s.contenant_id = c.id
     join  vin v
       on  s.vin_id = v.id
     join  appellation a
       on  v.appellation_id = a.id
where  s.annee =  (select  cast (round (random ()*50 ) as  int )+1950  as  annee)
group  by  s.annee||' - ' ||a.libelle;Voici son plan :
 HashAggregate  (cost=1251.12..1260.69 rows=319 width=28)
                (actual time=138.418..138.825 rows=319 loops=1)
   InitPlan 1 (returns $0)
     ->  Result  (cost=0.00..0.02 rows=1 width=0)
                 (actual time=0.005..0.006 rows=1 loops=1)
   ->  Hash Join  (cost=267.86..1166.13 rows=11329 width=28)
                  (actual time=31.108..118.193 rows=11389 loops=1)
         Hash Cond: (s.contenant_id = c.id)
         ->  Hash Join  (cost=266.82..896.02 rows=11329 width=28)
                        (actual time=31.071..80.980 rows=11389 loops=1)
               Hash Cond: (s.vin_id = v.id)
               ->  Index Scan using stock_annee on stock s
                    (cost=0.00..402.61 rows=11331 width=16)
                    (actual time=0.049..17.191 rows=11389 loops=1)
                     Index Cond: (annee = $0)
               ->  Hash  (cost=191.08..191.08 rows=6059 width=20)
                         (actual time=31.006..31.006 rows=6059 loops=1)
                     Buckets: 1024  Batches: 1  Memory Usage: 313kB
                     ->  Hash Join  (cost=10.18..191.08 rows=6059 width=20)
                                (actual time=0.814..22.856 rows=6059 loops=1)
                           Hash Cond: (v.appellation_id = a.id)
                           ->  Seq Scan on vin v
                                (cost=0.00..97.59 rows=6059 width=8)
                                (actual time=0.005..7.197 rows=6059 loops=1)
                           ->  Hash  (cost=6.19..6.19 rows=319 width=20)
                                     (actual time=0.800..0.800 rows=319 loops=1)
                                 Buckets: 1024  Batches: 1  Memory Usage: 17kB
                                 ->  Seq Scan on appellation a
                                    (cost=0.00..6.19 rows=319 width=20)
                                    (actual time=0.002..0.363 rows=319 loops=1)
         ->  Hash  (cost=1.02..1.02 rows=2 width=8)
                   (actual time=0.013..0.013 rows=2 loops=1)
               Buckets: 1024  Batches: 1  Memory Usage: 1kB
               ->  Seq Scan on contenant c  (cost=0.00..1.02 rows=2 width=8)
                                    (actual time=0.003..0.006 rows=2 loops=1)
 Total runtime: 139.252 ms
(21 rows)On constate qu'on utilise enfin l'index de stock. Le temps d'exécution a encore été divisé par deux.
NB :  ce problème d'incohérence de type était la cause du plus gros ralentissement de la requête. En reprenant la requête initiale, et en ajoutant directement le cast, la requête s'exécute déjà en 160 millisecondes.
Corrélation entre colonnes 
Importez le fichier correlations.sql.
  createdb  correlations
  psql  correlations <  correlations.sql
Exécutez cette requête, et regardez son plan d'exécution. Où est le problème ? 
 
Cette requête a été exécutée dans un environnement où le cache a été intégralement vidé, pour être dans la situation la plus défavorable possible. Vous obtiendrez probablement des performances meilleures, surtout si vous réexécutez cette requête.
explain  (analyze ,buffers)
   SELECT  * FROM  colis WHERE  id_ville IN  (
     SELECT  id_ville
     FROM  villes
     WHERE  localite ='PARIS' 
       AND  codepostal LIKE  '75%' 
);
                                                QUERY  PLAN 
-------------------------------------------------------------------------------- 
 Nested  Loop   (cost=6.75 ..13533.81  rows=3265  width=16 )
              (actual time=38.020 ..364383.516  rows=170802  loops=1 )
   Buffers: shared  hit=91539  read=82652 
   I/O Timings: read=359812.828 
   ->  Seq Scan  on  villes  (cost=0.00 ..1209.32  rows=19  width=
                           (actual time=23.979 ..45.383  rows=940  loops=1 )
         Filter: ((codepostal ~~ '75%' ::text ) AND  (localite = 'PARIS' ::text ))
         Rows  Removed by  Filter: 54015 
         Buffers: shared  hit=1  read=384 
         I/O Timings: read=22.326 
   ->  Bitmap  Heap  Scan  on  colis (cost=6.75 ..682.88  rows=181  width=16 )
                                 (actual time=1.305 ..387.239  rows=182  loops=940 )
         Recheck Cond: (id_ville = villes.id_ville)
         Buffers: shared  hit=91538  read=82268 
         I/O Timings: read=359790.502 
         ->  Bitmap  Index  Scan  on  idx_colis_ville
                (cost=0.00 ..6.70  rows=181  width=0 )
                (actual time=0.115 ..0.115  rows=182  loops=940 )
               Index  Cond: (id_ville = villes.id_ville)
               Buffers: shared  hit=2815  read=476 
               I/O Timings: read=22.862 
 Total runtime: 364466.458  ms
(17  lignes)On constate que l'optimiseur part sur une boucle extrêmement coûteuse : 940 parcours sur colis, par id_ville. En moyenne, ces parcours durent environ 400 ms. Le résultat est vraiment très mauvais.
Il fait ce choix parce qu'il estime que la condition
localite ='PARIS' AND codepostal LIKE '75%'va ramener 19 enregistrements. En réalité, elle en ramène 940, soit 50 fois plus, d'où un très mauvais choix. Pourquoi PostgreSQL fait-il cette erreur ?
marc=# EXPLAIN SELECT * FROM villes;
                         QUERY PLAN
-------------------------------------------------------------
 Seq Scan on villes  (cost=0.00..934.55 rows=54955 width=27)
(1 ligne)
marc=# EXPLAIN SELECT * FROM villes WHERE localite='PARIS';
                         QUERY PLAN
------------------------------------------------------------
 Seq Scan on villes  (cost=0.00..1071.94 rows=995 width=27)
   Filter: (localite = 'PARIS'::text)
(2 lignes)
marc=# EXPLAIN SELECT * FROM villes WHERE codepostal LIKE '75%';
                         QUERY PLAN
-------------------------------------------------------------
 Seq Scan on villes  (cost=0.00..1071.94 rows=1042 width=27)
   Filter: (codepostal ~~ '75%'::text)
(2 lignes)
marc=# EXPLAIN SELECT * FROM villes WHERE localite='PARIS'
marc=# AND codepostal LIKE '75%';
                               QUERY PLAN
------------------------------------------------------------------------
 Seq Scan on villes  (cost=0.00..1209.32 rows=19 width=27)
   Filter: ((codepostal ~~ '75%'::text) AND (localite = 'PARIS'::text))
(2 lignes)D'après les statistiques, villes contient 54955 enregistrements, 995 contenant PARIS (presque 2%), 1042 commençant par 75 (presque 2%).
Il y a donc 2% d'enregistrements vérifiant chaque critère (c'est normal, ils sont presque équivalents). PostgreSQL, n'ayant aucune autre information, part de l'hypothèse que les colonnes ne sont pas liées, et qu'il y a donc 2% de 2% (soit environ 0,04%) des enregistrements qui vérifient les deux.
Si on fait le calcul exact, on a donc :
(995/54955)*(1042/54955)*54955soit 18,8 enregistrements (arrondi à 19) qui vérifient le critère. Ce qui est évidemment faux.
Exécutez cette requête sans la dernière clause AND codepostal LIKE '75%'. Que constatez-vous ? 
 
explain  (analyze ,buffers) select  * from  colis where  id_ville in  (
    select  id_ville from  villes where  localite ='PARIS' 
);
                                          QUERY  PLAN 
-------------------------------------------------------------------------------- 
 Hash  Semi Join   (cost=1083.86 ..183312.59  rows=173060  width=16 )
                 (actual time=48.975 ..4362.348  rows=170802  loops=1 )
   Hash  Cond: (colis.id_ville = villes.id_ville)
   Buffers: shared  hit=7  read=54435 
   I/O Timings: read=1219.212 
   ->  Seq Scan  on  colis  (cost=0.00 ..154053.55  rows=9999955  width=16 )
                          (actual time=6.178 ..2228.259  rows=9999911  loops=1 )
         Buffers: shared  hit=2  read=54052 
         I/O Timings: read=1199.307 
   ->  Hash   (cost=1071.94 ..1071.94  rows=954  width=
             (actual time=42.676 ..42.676  rows=940  loops=1 )
         Buckets: 1024   Batches: 1   Memory Usage: 37kB
         Buffers: shared  hit=2  read=383 
         I/O Timings: read=19.905 
         ->  Seq Scan  on  villes  (cost=0.00 ..1071.94  rows=954  width=
               (actual time=35.900 ..41.957  rows=940  loops=1 )
               Filter: (localite = 'PARIS' ::text )
               Rows  Removed by  Filter: 54015 
               Buffers: shared  hit=2  read=383 
               I/O Timings: read=19.905 
 Total runtime: 4375.105  ms
(17  lignes)Cette fois-ci le plan est bon, et les estimations aussi.
Quelle solution pourrait-on adopter, si on doit réellement spécifier ces deux conditions ? 
 
On pourrait indexer sur une fonction des deux. C'est maladroit, mais malheureusement la seule solution sûre :
CREATE  FUNCTION  test_ville (ville text,codepostal text) RETURNS text
IMMUTABLE LANGUAGE SQL as  $$
SELECT  ville || '-'  || codepostal
$$ ;
CREATE  INDEX  idx_test_ville ON  villes (test_ville(localite , codepostal));
ANALYZE  villes;
EXPLAIN  (analyze ,buffers) SELECT  * FROM  colis WHERE  id_ville IN  (
    SELECT  id_ville
    FROM  villes
    WHERE  test_ville(localite,codepostal) LIKE  'PARIS-75%' 
);
                                         QUERY  PLAN 
-------------------------------------------------------------------------------- 
 Hash  Semi Join   (cost=1360.59 ..183924.46  rows=203146  width=16 )
                 (actual time=46.127 ..3530.348  rows=170802  loops=1 )
   Hash  Cond: (colis.id_ville = villes.id_ville)
   Buffers: shared  hit=454  read=53989 
   ->  Seq Scan  on  colis  (cost=0.00 ..154054.11  rows=9999911  width=16 )
                          (actual time=0.025 ..1297.520  rows=9999911  loops=1 )
         Buffers: shared  hit=66  read=53989 
   ->  Hash   (cost=1346.71 ..1346.71  rows=1110  width=8 )
             (actual time=46.024 ..46.024  rows=940  loops=1 )
         Buckets: 1024   Batches: 1   Memory Usage: 37kB
         Buffers: shared  hit=385 
         ->  Seq Scan  on  villes  (cost=0.00 ..1346.71  rows=1110  width=8 )
                                 (actual time=37.257 ..45.610  rows=940  loops=1 )
               Filter: (((localite || '-' ::text ) || codepostal) ~~
                        'PARIS-75%' ::text )
               Rows  Removed by  Filter: 54015 
               Buffers: shared  hit=385 
 Total runtime: 3543.838  msOn constate qu'avec cette méthode il n'y a plus d'erreur d'estimation. Elle est bien sûr très pénible à utiliser, et ne doit donc être réservée qu'aux quelques rares requêtes ayant été identifiées comme ayant un comportement pathologique.
On peut aussi créer une colonne supplémentaire maintenue par un trigger, plutôt qu'un index : cela sera moins coûteux à maintenir, et permettra d'avoir la même statistique.
Conclusion 
Que peut-on conclure de cet exercice ?
que la ré-écriture est souvent la meilleure des solutions : interrogez-vous toujours sur la façon dont vous écrivez vos requêtes, plutôt que de mettre en doute PostgreSQL a priori  ; 
que la ré-écriture de requête est souvent complexe - néanmoins, surveillez un certain nombre de choses :
casts implicites suspects ; 
jointures externes inutiles ; 
sous-requêtes imbriquées ; 
jointures inutiles (données constantes)