PostgreSQLLa base de données la plus sophistiquée au monde.
Documentation PostgreSQL 17.6 » Langage SQL » Conseils sur les performances » Utiliser EXPLAIN

14.1. Utiliser EXPLAIN #

PostgreSQL rĂ©alise un plan de requĂȘte pour chaque requĂȘte qu'il reçoit. Choisir le bon plan correspondant Ă  la structure de la requĂȘte et aux propriĂ©tĂ©s des donnĂ©es est absolument critique pour de bonnes performances, donc le systĂšme inclut un planificateur ou optimiseur complexe qui tente de choisir les bons plans. Vous pouvez utiliser la commande EXPLAIN pour voir quel plan de requĂȘte le planificateur crĂ©e pour une requĂȘte particuliĂšre. La lecture du plan est un art qui requiert de l'expĂ©rience pour le maĂźtriser, mais cette section essaie de couvrir les bases.

Les exemples dans cette section sont tirĂ©s de la base de donnĂ©es pour les tests de rĂ©gression aprĂšs avoir effectuĂ© un VACUUM ANALYZE, avec les sources de la version de dĂ©veloppement v17. Vous devriez obtenir des rĂ©sultats similaires si vous essayez les exemples vous-mĂȘme, mais vos estimations de coĂ»t et de nombre de lignes pourraient lĂ©gĂšrement varier parce que les statistiques d'ANALYZE sont basĂ©es sur des Ă©chantillons alĂ©atoires, et parce que les coĂ»ts sont dĂ©pendants de la plateforme utilisĂ©e.

Les exemples utilisent le format de sortie par dĂ©faut (« text Â») d'EXPLAIN, qui est compact et pratique pour la lecture. Si vous voulez utiliser la sortie d'EXPLAIN avec un programme pour une analyse ultĂ©rieure, vous devriez utiliser un des formats de sortie au format machine (XML, JSON ou YAML) Ă  la place.

14.1.1. Concepts d'EXPLAIN #

La structure d'un plan de requĂȘte est un arbre de nƓuds de plan. Les nƓuds de bas niveau sont les nƓuds de parcours : ils renvoient les lignes brutes d'une table. Il existe diffĂ©rents types de nƓuds de parcours pour les diffĂ©rentes mĂ©thodes d'accĂšs aux tables : parcours sĂ©quentiel, parcours d'index et parcours d'index bitmap. Il y a Ă©galement des ensembles de lignes qui ne proviennent pas de tables, avec par exemple des clauses VALUES ainsi que des fonctions renvoyant des ensembles dans un FROM, qui ont leurs propres types de nƓuds de parcours. Si la requĂȘte requiert des jointures, agrĂ©gations, tris ou d'autres opĂ©rations sur les lignes brutes, ce seront des nƓuds supplĂ©mentaires au-dessus des nƓuds de parcours pour rĂ©aliser ces opĂ©rations. Encore une fois, il existe plus d'une façon de rĂ©aliser ces opĂ©rations, donc diffĂ©rents types de nƓuds peuvent aussi apparaĂźtre ici. La sortie d'EXPLAIN comprend une ligne pour chaque nƓud dans l'arbre du plan, montrant le type de nƓud basique avec les estimations de coĂ»t que le planificateur a faites pour l'exĂ©cution de ce nƓud du plan. Des lignes supplĂ©mentaires peuvent apparaĂźtre, indentĂ©es par rapport Ă  la ligne de rĂ©sumĂ© du nƓud, pour montrer les propriĂ©tĂ©s supplĂ©mentaires du nƓud. La premiĂšre ligne (le nƓud tout en haut) comprend le coĂ»t d'exĂ©cution total estimĂ© pour le plan ; c'est ce nombre que le planificateur cherche Ă  minimiser.

Voici un exemple trivial, juste pour montrer Ă  quoi ressemble l'affichage.

EXPLAIN SELECT * FROM tenk1;

                         QUERY PLAN
-------------------------------------------------------------
 Seq Scan on tenk1  (cost=0.00..445.00 rows=10000 width=244)

Puisque la requĂȘte n'a pas de clause WHERE, il faut parcourir toutes les lignes de la table, c'est pourquoi le planificateur a choisi d'utiliser un plan avec un simple parcours sĂ©quentiel. Les nombres affichĂ©s entre parenthĂšses sont (de gauche Ă  droite) :

  • CoĂ»t estimĂ© du lancement. Cela correspond au temps passĂ© avant que l'affichage de la sortie ne commence, par exemple le temps de faire un tri dans un nƓud de tri ;

  • CoĂ»t total estimĂ©. Cela suppose que le nƓud du plan d'exĂ©cution est exĂ©cutĂ© entiĂšrement, c'est-Ă -dire que toutes les lignes disponibles sont rĂ©cupĂ©rĂ©es. En pratique, un nƓud parent peut arrĂȘter la rĂ©cupĂ©ration de toutes les lignes disponibles avant la fin (voir l'exemple LIMIT ci-dessous) ;

  • Nombre de lignes estimĂ© en sortie par ce nƓud de plan. Encore une fois, on suppose que le nƓud est exĂ©cutĂ© entiĂšrement.

  • Largeur moyenne estimĂ©e (en octets) des lignes en sortie par ce nƓud du plan.

Les coĂ»ts sont mesurĂ©s en unitĂ©s arbitraires dĂ©terminĂ©es par les paramĂštres de coĂ»t du planificateur (voir Section 19.7.2). La pratique habituelle est de mesurer les coĂ»ts en unitĂ© de rĂ©cupĂ©ration de pages disque ; autrement dit, seq_page_cost est initialisĂ© Ă  1.0 par convention et les autres paramĂštres de coĂ»t sont relatifs Ă  cette valeur. Les exemples de cette section sont exĂ©cutĂ©s avec les paramĂštres de coĂ»t par dĂ©faut.

Il est important de comprendre que le coĂ»t d'un nƓud de haut niveau inclut le coĂ»t de tous les nƓuds fils. Il est aussi important de rĂ©aliser que le coĂ»t reflĂšte seulement les Ă©lĂ©ments d'importance pour le planificateur. En particulier, le coĂ»t ne considĂšre pas le temps passĂ© Ă  convertir les valeurs en entrĂ©e vers le format texte ou Ă  les transmettre au client. Les deux pourraient ĂȘtre des facteurs importants du temps rĂ©el passĂ© ; mais l'optimiseur ignore ces coĂ»ts parce qu'il ne peut pas les changer sans modifier le plan. (Chaque plan correct renverra le mĂȘme ensemble de ligne.)

La valeur rows est un peu difficile car il ne s'agit pas du nombre de lignes traitĂ©es ou parcourues par le plan de nƓuds, mais plutĂŽt le nombre Ă©mis par le nƓud. C'est habituellement moins, reflĂ©tant la sĂ©lectivitĂ© estimĂ©e des conditions de la clause WHERE qui sont appliquĂ©es au nƓud. IdĂ©alement, les estimations des lignes de haut niveau seront une approximation des nombres de lignes dĂ©jĂ  renvoyĂ©es, mises Ă  jour, supprimĂ©es par la requĂȘte.

Quand un UPDATE, un DELETE ou un MERGE affecte une table partitionnĂ©e ou une hiĂ©rarchie d'hĂ©ritage, la sortie pourrait ressembler Ă  ceci :

+EXPLAIN UPDATE gtest_parent SET f1 = CURRENT_DATE WHERE f2 = 101;

                                       QUERY PLAN
-------------------------------------------------------------------​---------------------
 Update on gtest_parent  (cost=0.00..3.06 rows=0 width=0)
   Update on gtest_child gtest_parent_1
   Update on gtest_child2 gtest_parent_2
   Update on gtest_child3 gtest_parent_3
   ->  Append  (cost=0.00..3.06 rows=3 width=14)
         ->  Seq Scan on gtest_child gtest_parent_1  (cost=0.00..1.01 rows=1 width=14)
               Filter: (f2 = 101)
         ->  Seq Scan on gtest_child2 gtest_parent_2  (cost=0.00..1.01 rows=1 width=14)
               Filter: (f2 = 101)
         ->  Seq Scan on gtest_child3 gtest_parent_3  (cost=0.00..1.01 rows=1 width=14)
               Filter: (f2 = 101)

Dans cet exemple, le nƓud Update doit prendre en compte les trois tables filles mais pas la table partitionnĂ©e mentionnĂ©e Ă  l'origine (tout simplement parce qu'elle ne contient aucune donnĂ©es). Donc il y a trois sous-plans de parcours en entrĂ©e, un par table. Pour plus de clartĂ©, le nƓud Update est annotĂ© pour afficher les tables cibles spĂ©cifiques Ă  mettre Ă  jour, dans le mĂȘme ordre que les sous-plans correspondants.

Le Temps de planification (Planning time) affichĂ© est le temps qu'il a fallu pour gĂ©nĂ©rer le plan d'exĂ©cution de la requĂȘte analysĂ©e et pour l'optimiser. Cela n'inclut ni le temps de réécriture ni le temps d'analyse.

Pour revenir Ă  notre exemple :

EXPLAIN SELECT * FROM tenk1;

                         QUERY PLAN
-------------------------------------------------------------
 Seq Scan on tenk1  (cost=0.00..445.00 rows=10000 width=244)

Ces nombres sont directement dĂ©rivĂ©s. Si vous faites :

SELECT relpages, reltuples FROM pg_class WHERE relname = 'tenk1';

vous trouverez que tenk1 a 345 blocs disque et 10000 lignes. Le coût estimé est calculé avec (nombre de blocs lus * seq_page_cost) + (lignes parcourues * cpu_tuple_cost). Par défaut, seq_page_cost vaut 1,0 et cpu_tuple_cost vaut 0,01. Donc le coût estimé est de (345 * 1,0) + (10000 * 0,01), soit 445.

Maintenant, modifions la requĂȘte originale pour ajouter une condition WHERE :

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 7000;

                         QUERY PLAN
------------------------------------------------------------
 Seq Scan on tenk1  (cost=0.00..470.00 rows=7000 width=244)
   Filter: (unique1 < 7000)

Notez que l'affichage d'EXPLAIN montre la clause WHERE appliquĂ©e comme une condition de « filtre Â» rattachĂ©e au nƓud de parcours sĂ©quentiel ; ceci signifie que le nƓud de plan vĂ©rifie la condition pour chaque ligne qu'il parcourt et ne conserve que celles qui satisfont la condition. L'estimation des lignes en sortie a baissĂ© Ă  cause de la clause WHERE. NĂ©anmoins, le parcours devra toujours visiter les 10000 lignes, donc le coĂ»t n'a pas baissĂ© ; en fait, il a un peu augmentĂ©(par 10000 * cpu_operator_cost pour ĂȘtre exact) dans le but de reflĂ©ter le temps CPU supplĂ©mentaire dĂ©pensĂ© pour vĂ©rifier la condition WHERE.

Le nombre rĂ©el de lignes que cette requĂȘte sĂ©lectionnera est 7000, mais l'estimation rows est approximative. Si vous tentez de dupliquer cette expĂ©rience, vous obtiendrez probablement une estimation lĂ©gĂšrement diffĂ©rente ; de plus, elle changera aprĂšs chaque commande ANALYZE parce que les statistiques produites par ANALYZE sont prises Ă  partir d'un extrait au hasard de la table.

Maintenant, rendons la condition plus restrictive :

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100;

                                  QUERY PLAN
------------------------------------------------------------------------------
 Bitmap Heap Scan on tenk1  (cost=5.06..224.98 rows=100 width=244)
   Recheck Cond: (unique1 < 100)
   ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0)
         Index Cond: (unique1 < 100)

Ici, le planificateur a dĂ©cidĂ© d'utiliser un plan en deux Ă©tapes : le nƓud en bas du plan visite un index pour trouver l'emplacement des lignes correspondant Ă  la condition de l'index, puis le nƓud du plan du dessus rĂ©cupĂšre rĂ©ellement ces lignes de la table. RĂ©cupĂ©rer sĂ©parĂ©ment les lignes est bien plus coĂ»teux que de les lire sĂ©quentiellement, mais comme toutes les pages de la table n'ont pas Ă  ĂȘtre visitĂ©es, cela revient toujours moins cher qu'un parcours sĂ©quentiel (la raison de l'utilisation d'un plan Ă  deux niveaux est que le nƓud du plan du dessus trie les emplacements des lignes identifiĂ©es par l'index dans l'ordre physique avant de les lire pour minimiser les coĂ»ts des rĂ©cupĂ©rations sĂ©parĂ©es. Le « bitmap Â» mentionnĂ© dans les noms de nƓuds est le mĂ©canisme qui s'occupe du tri).

Maintenant, ajoutons une autre condition Ă  la clause WHERE :

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100 AND stringu1 = 'xxx';

                                  QUERY PLAN
------------------------------------------------------------------------------
 Bitmap Heap Scan on tenk1  (cost=5.04..225.20 rows=1 width=244)
   Recheck Cond: (unique1 < 100)
   Filter: (stringu1 = 'xxx'::name)
   ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0)
         Index Cond: (unique1 < 100)

L'ajout de la condition stringu1 = 'xxx' rĂ©duit l'estimation du nombre de lignes renvoyĂ©es, mais pas son coĂ»t, car il faut toujours parcourir le mĂȘme ensemble de lignes. C'est dĂ» fait que la clause stringu1 ne peut ĂȘtre appliquĂ©e comme une condition d'index car l'index ne porte que sur la colonne unique1. À la place, un filtre a Ă©tĂ© appliquĂ© sur les lignes rĂ©cupĂ©rĂ©es en utilisant l'index. C'est pourquoi le coĂ»t a lĂ©gĂšrement augmentĂ© pour reflĂ©ter la vĂ©rification supplĂ©mentaire.

Dans certains cas, le planificateur prĂ©fĂšrera un plan « simple Â» d'index :

EXPLAIN SELECT * FROM tenk1 WHERE unique1 = 42;

                                 QUERY PLAN
-----------------------------------------------------------------------------
 Index Scan using tenk1_unique1 on tenk1  (cost=0.29..8.30 rows=1 width=244)
   Index Cond: (unique1 = 42)

Dans ce type de plan, les lignes de la table sont rĂ©cupĂ©rĂ©es dans l'ordre de l'index, ce qui les rend encore plus coĂ»teuses Ă  rĂ©cupĂ©rer, mais il y en a tellement peu que le coĂ»t supplĂ©mentaire pour trier l'ordre des lignes n'est pas rentable. Vous verrez principalement ce type de plan pour les requĂȘtes qui ne rĂ©cupĂšrent qu'une seule ligne, ou pour les requĂȘtes qui ont une condition ORDER BY qui correspond Ă  l'ordre de l'index, car cela ne nĂ©cessite aucune Ă©tape supplĂ©mentaire pour satisfaire l'ORDER BY. Dans cet exemple, ajouter ORDER BY unique1 ferait que l'optimiseur utilise le mĂȘme plan parce que l'index fournit dĂ©jĂ  implicitement le tri requis.

L'optimiseur peut exĂ©cuter une clause ORDER BY de plusieurs façons. L'exemple ci-dessus montre qu'une clause de tri peut ĂȘtre effectuĂ© implicitement. L'optimiseur peut aussi ajouter une Ă©tape de tri (Sort) explicite :

EXPLAIN SELECT * FROM tenk1 ORDER BY unique1;
                            QUERY PLAN
-------------------------------------------------------------------
 Sort  (cost=1109.39..1134.39 rows=10000 width=244)
   Sort Key: unique1
   ->  Seq Scan on tenk1  (cost=0.00..445.00 rows=10000 width=244)

Si une partie du plan garantit un ordre sur un prĂ©fixe des clĂ©s de tri requises, alors l'optimiseur peut dĂ©cider Ă  la place d'utiliser une Ă©tape de tri incrĂ©mental (Incremental Sort) :

EXPLAIN SELECT * FROM tenk1 ORDER BY hundred, ten LIMIT 100;
                                              QUERY PLAN
-------------------------------------------------------------------​-----------------------------
 Limit  (cost=19.35..39.49 rows=100 width=244)
   ->  Incremental Sort  (cost=19.35..2033.39 rows=10000 width=244)
         Sort Key: hundred, ten
         Presorted Key: hundred
         ->  Index Scan using tenk1_hundred on tenk1  (cost=0.29..1574.20 rows=10000 width=244)

ComparĂ© aux tris habituels, le tri incrĂ©mental permet de renvoyer les lignes avant que l'ensemble du rĂ©sultat ne soit triĂ©, ce qui permet en particulier des optimisations avec les requĂȘtes utilisant la clause LIMIT. Il peut aussi rĂ©duire l'utilisation de la mĂ©moire et la probabilitĂ© d'envoyer des tris sur disque, mais cela a un coĂ»t : une surcharge pour rĂ©partir l'ensemble de lignes rĂ©sultats dans plusieurs groupes de tri.

S'il y a des index sur plusieurs colonnes rĂ©fĂ©rencĂ©es dans la clause WHERE, le planificateur pourrait choisir d'utiliser une combinaison binaire (AND et OR) des index :

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000;

                                      QUERY PLAN
 -------------------------------------------------------------------------------------
 Bitmap Heap Scan on tenk1  (cost=25.07..60.11 rows=10 width=244)
    Recheck Cond: ((unique1 < 100) AND (unique2 > 9000))
   ->  BitmapAnd  (cost=25.07..25.07 rows=10 width=0)
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0)
                Index Cond: (unique1 < 100)
         ->  Bitmap Index Scan on tenk1_unique2  (cost=0.00..19.78 rows=999 width=0)
                Index Cond: (unique2 > 9000)

Mais ceci requiert de visiter plusieurs index, donc ce n'est pas nécessairement un gain comparé à l'utilisation d'un seul index et au traitement de l'autre condition par un filtre. Si vous variez les échelles de valeurs impliquées, vous vous apercevrez que le plan change en accord.

Voici un exemple montrant les effets d'un LIMIT :

EXPLAIN SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000 LIMIT 2;

                                     QUERY PLAN
-------------------------------------------------------------------------------------
 Limit  (cost=0.29..14.28 rows=2 width=244)
   ->  Index Scan using tenk1_unique2 on tenk1  (cost=0.29..70.27 rows=10 width=244)
         Index Cond: (unique2 > 9000)
         Filter: (unique1 < 100)

C'est la mĂȘme requĂȘte qu'au-dessus, mais avec l'ajout de LIMIT, ce qui fait que toutes les lignes ne seront pas rĂ©cupĂ©rĂ©es, et donc que le planificateur change sa façon de procĂ©der. Notez que le coĂ»t total ainsi que le nombre de lignes du nƓud de parcours d'index sont affichĂ©s comme si le nƓud devait ĂȘtre exĂ©cutĂ© entiĂšrement. Cependant, le nƓud Limit s'attend Ă  s'arrĂȘter aprĂšs avoir rĂ©cupĂ©rĂ© seulement un cinquiĂšme de ces lignes, c'est pourquoi son coĂ»t total n'est qu'un cinquiĂšme du coĂ»t prĂ©cĂ©dent, ce qui est le vrai coĂ»t estimĂ© de la requĂȘte. Ce plan est prĂ©fĂ©rable Ă  l'ajout d'un nƓud Limit au plan prĂ©cĂ©dent, car le Limit ne pourrait pas empĂȘcher le coĂ»t de dĂ©part du parcours d'index Bitmap, ce qui augmenterait le coĂ»t d'environ 25 unitĂ©s avec cette approche.

Maintenant, essayons de joindre deux tables, en utilisant les colonnes dont nous avons discutĂ© :

EXPLAIN SELECT *
FROM tenk1 t1, tenk2 t2
WHERE t1.unique1 < 10 AND t1.unique2 = t2.unique2;

                                       QUERY PLAN
 --------------------------------------------------------------------------------------
 Nested Loop  (cost=4.65..118.50 rows=10 width=488)
   ->  Bitmap Heap Scan on tenk1 t1  (cost=4.36..39.38 rows=10 width=244)
         Recheck Cond: (unique1 < 10)
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0)
               Index Cond: (unique1 < 10)
   ->  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..7.90 rows=1 width=244)
          Index Cond: (unique2 = t1.unique2)

Dans ce plan, nous avons un nƓud de jointure en boucle imbriquĂ©e sur deux parcours de tables en entrĂ©e. L'indentation des lignes de sommaire des nƓuds reflĂšte la structure en arbre du plan. Le premier nƓud, ou nƓud « externe Â», utilise le mĂȘme parcours de bitmap que celui vu prĂ©cĂ©demment, et donc ses coĂ»t et nombre de lignes sont les mĂȘmes que ce que l'on aurait obtenu avec SELECT ... WHERE unique1 < 10, car la mĂȘme clause WHERE unique1 < 10 est appliquĂ©e Ă  ce nƓud. La clause t1.unique2 = t2.unique2 n'a pas encore d'intĂ©rĂȘt, elle n'affecte donc pas le nombre de lignes du parcours externe. Le nƓud de jointure en boucle imbriquĂ©e s'exĂ©cutera sur le deuxiĂšme nƓud, ou nƓud « interne Â», pour chaque ligne obtenue du nƓud externe. Les valeurs de colonne de la ligne externe courante peuvent ĂȘtre utilisĂ©es dans le parcours interne ; ici, la valeur t1.unique2 de la ligne externe est disponible, et on peut obtenir un plan et un coĂ»t similaires Ă  ce que l'on a vu plus haut pour le cas simple SELECT ... WHERE t2.unique2 = constant.(Le coĂ»t estimĂ© est ici un peu plus faible que celui vu prĂ©cĂ©demment, en prĂ©vision de la mise en cache des donnĂ©es durant les parcours d'index rĂ©pĂ©tĂ©s sur t2.) Les coĂ»ts du nƓud correspondant Ă  la boucle sont ensuite initialisĂ©s sur la base du coĂ»t du parcours externe, avec une rĂ©pĂ©tition du parcours interne pour chaque ligne externe (ici 10 * 7,90), plus un petit temps CPU pour traiter la jointure.

Dans cet exemple, le nombre de lignes en sortie de la jointure est identique au nombre de lignes des deux parcours, mais ce n'est pas vrai en rĂšgle gĂ©nĂ©rale car vous pouvez avoir des clauses WHERE mentionnant les deux tables et qui, donc, peuvent seulement ĂȘtre appliquĂ©es au point de jointure, et non pas aux parcours d'index. Voici un exemple :

EXPLAIN SELECT *
FROM tenk1 t1, tenk2 t2
WHERE t1.unique1 < 10 AND t2.unique2 < 10 AND t1.hundred < t2.hundred;

                                      QUERY PLAN
--------------------------------------------------------------------------------------
 Nested Loop  (cost=4.65..49.36 rows=33 width=488)
   Join Filter: (t1.hundred < t2.hundred)
   ->  Bitmap Heap Scan on tenk1 t1  (cost=4.36..39.38 rows=10 width=244)
         Recheck Cond: (unique1 < 10)
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0)
               Index Cond: (unique1 < 10)
   ->  Materialize  (cost=0.29..8.51 rows=10 width=244)
         ->  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..8.46 rows=10 width=244)
               Index Cond: (unique2 < 10)

La condition t1.hundred < t2.hundred ne peut ĂȘtre testĂ©e dans l'index tenk2_unique2, elle est donc appliquĂ©e au nƓud de jointure. Cela rĂ©duit l'estimation du nombre de lignes dans le nƓud de jointure, mais ne change aucun parcours d'entrĂ©e.

Notez qu'ici le planificateur a choisi de matĂ©rialiser la relation interne de la jointure en plaçant un nƓud Materialize au-dessus. Cela signifie que le parcours d'index de t2 ne sera rĂ©alisĂ© qu'une seule fois, mĂȘme si le nƓud de jointure par boucle imbriquĂ©e va lire dix fois les donnĂ©es, une fois par ligne de la relation externe. Le nƓud Materialize conserve les donnĂ©es en mĂ©moire lors de leur premiĂšre lecture, puis renvoie les donnĂ©es depuis la mĂ©moire Ă  chaque lecture supplĂ©mentaire.

Quand vous utilisez des jointures externes, vous pouvez voir des nƓuds de plan de jointure avec Ă  la fois des conditions « Join Filter Â» et « Filter Â» simples attachĂ©es. Les conditions « Join Filter Â» viennent des clauses de jointures externes ON, pour qu'une ligne ne satisfaisant pas la condition « Join Filter Â» puisse toujours ĂȘtre rĂ©cupĂ©rĂ©e comme une ligne NULL. Mais une condition « Filter Â» simple est appliquĂ©e aprĂšs la rĂšgle de jointure externe et supprime donc les lignes de maniĂšre inconditionnelle. Dans une jointure interne, il n'y a pas de diffĂ©rence sĂ©mantique entre ces types de filtres.

Si nous changeons un peu la sĂ©lectivitĂ© de la requĂȘte, on pourrait obtenir un plan de jointure trĂšs diffĂ©rent :

EXPLAIN SELECT *
FROM tenk1 t1, tenk2 t2
WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2;

                                        QUERY PLAN
------------------------------------------------------------------------------------------
 Hash Join  (cost=226.23..709.73 rows=100 width=488)
    Hash Cond: (t2.unique2 = t1.unique2)
    ->  Seq Scan on tenk2 t2  (cost=0.00..445.00 rows=10000 width=244)
    ->  Hash  (cost=224.98..224.98 rows=100 width=244)
          ->  Bitmap Heap Scan on tenk1 t1  (cost=5.06..224.98 rows=100 width=244)
                Recheck Cond: (unique1 < 100)
               ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0)
                     Index Cond: (unique1 < 100)

Ici, le planificateur a choisi d'utiliser une jointure de hachage, dans laquelle les lignes d'une table sont entrĂ©es dans une table de hachage en mĂ©moire, aprĂšs quoi l'autre table est parcourue et la table de hachage est sondĂ©e pour faire correspondre chaque ligne. Notez encore une fois comment l'indentation reflĂšte la structure du plan : le parcours d'index bitmap sur tenk1 est l'entrĂ©e du nƓud de hachage, qui construit la table de hachage. C'est alors retournĂ© au nƓud de jointure de hachage, qui lit les lignes depuis le plan du fils externe et cherche dans la table de hachage pour chaque ligne.

Un autre type de jointure possible est la jointure d'assemblage, illustrĂ©e ici :

EXPLAIN SELECT *
FROM tenk1 t1, onek t2
WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2;

                                        QUERY PLAN
------------------------------------------------------------------------------------------
 Merge Join  (cost=0.56..233.49 rows=10 width=488)
   Merge Cond: (t1.unique2 = t2.unique2)
   ->  Index Scan using tenk1_unique2 on tenk1 t1  (cost=0.29..643.28 rows=100 width=244)
         Filter: (unique1 < 100)
   ->  Index Scan using onek_unique2 on onek t2  (cost=0.28..166.28 rows=1000 width=244)

La jointure d'assemblage nĂ©cessite que les donnĂ©es en entrĂ©e soient triĂ©es sur la clĂ© de jointure. Dans cet exemple, chaque entrĂ©e est triĂ©e en utilisant un parcours d'index pour visiter les lignes dans le bon ordre ; un parcours sĂ©quentiel suivi d'un tri pourraient aussi ĂȘtre utilisĂ©s. (Un parcours sĂ©quentiel suivi d'un tri bat frĂ©quemment un parcours d'index pour trier de nombreuses lignes, du fait des accĂšs disques non sĂ©quentiels requis par le parcours d'index.)

Une façon de rechercher des plans diffĂ©rents est de forcer le planificateur Ă  oublier certaines stratĂ©gies qu'il aurait trouvĂ©es moins coĂ»teuses en utilisant les options d'activation (enable)/dĂ©sactivation (disable) dĂ©crites dans la Section 19.7.1 (c'est un outil complexe, mais utile ; voir aussi la Section 14.3). Par exemple, si nous n'Ă©tions pas convaincus que la jointure d'assemblage soit le meilleur type de jointure dans l'exemple prĂ©cĂ©dent, nous pourrions essayer

SET enable_mergejoin = off;
EXPLAIN SELECT *
FROM tenk1 t1, onek t2
 WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2;

                                         QUERY PLAN
 ------------------------------------------------------------------------------------------
 Hash Join  (cost=226.23..344.08 rows=10 width=488)
   Hash Cond: (t2.unique2 = t1.unique2)
   ->  Seq Scan on onek t2  (cost=0.00..114.00 rows=1000 width=244)
   ->  Hash  (cost=224.98..224.98 rows=100 width=244)
         ->  Bitmap Heap Scan on tenk1 t1  (cost=5.06..224.98 rows=100 width=244)
               Recheck Cond: (unique1 < 100)
               ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0)
                     Index Cond: (unique1 < 100)

ce qui montre que le planificateur pense que la jointure par hachage serait pratiquement 50% plus coûteuse qu'une jointure par assemblage dans ce cas. Bien sûr, la question suivante est de savoir s'il a raison sur ce point. Nous pourrions vérifier cela en utilisant EXPLAIN ANALYZE, comme expliqué ci-dessous.

Certains plans de requĂȘte impliquent des sous-plans, qui viennent des sous-SELECT de la requĂȘte originale. De telles requĂȘtes peuvent parfois ĂȘtre transformĂ©s en jointures ordinaires mais, quand ce n'est pas possible, nous obtenons des plans de ce type :

EXPLAIN VERBOSE SELECT unique1
FROM tenk1 t
WHERE t.ten < ALL (SELECT o.ten FROM onek o WHERE o.four = t.four);

                               QUERY PLAN
-------------------------------------------------------------------​------
 Seq Scan on public.tenk1 t  (cost=0.00..586095.00 rows=5000 width=4)
   Output: t.unique1
   Filter: (ALL (t.ten < (SubPlan 1).col1))
   SubPlan 1
     ->  Seq Scan on public.onek o  (cost=0.00..116.50 rows=250 width=4)
           Output: o.ten
           Filter: (o.four = t.four)

Cet exemple assez artificiel sert Ă  illustrer un ensemble de points : les valeurs au niveau du plan externe sont passĂ©es au sous-plan (ici, il s'agit de t.four) et les rĂ©sultats de la sous-requĂȘte sont disponibles sur le plan externe. Ces valeurs de rĂ©sultat sont affichĂ©es par EXPLAIN avec des notations comme (nom_sousplan).colN, qui fait rĂ©fĂ©rence Ă  la N-iĂšme colonne en sortie du sous-SELECT.

Dans l'exemple ci-dessus, l'opĂ©rateur ALL exĂ©cute le sous-plan pour chaque ligne de la requĂȘte externe (ce qui explique le coĂ»t estimĂ© trĂšs fort). Certaines requĂȘtes peuvent utiliser un sous-plan hachĂ© pour Ă©viter cela :

EXPLAIN SELECT *
FROM tenk1 t
WHERE t.unique1 NOT IN (SELECT o.unique1 FROM onek o);

                                         QUERY PLAN
-------------------------------------------------------------------​-------------------------
 Seq Scan on tenk1 t  (cost=61.77..531.77 rows=5000 width=244)
   Filter: (NOT (ANY (unique1 = (hashed SubPlan 1).col1)))
   SubPlan 1
     ->  Index Only Scan using onek_unique1 on onek o  (cost=0.28..59.27 rows=1000 width=4)
(4 rows)

Ici, le sous-plan est lancĂ© une seule fois et le rĂ©sultat est chargĂ© dans une table de hachage en mĂ©moire, qui est ensuite interrogĂ©e par l'opĂ©rateur externe ANY. Ceci nĂ©cessite que le sous-SELECT ne rĂ©fĂ©rence pas de variables de la requĂȘte externe, et que l'opĂ©rateur de comparaison ANY puisse utiliser le hachage.

Si, en plus de ne pas rĂ©fĂ©rencer les variables de la requĂȘte externe, le sous-SELECT ne peut pas renvoyer plus d'une ligne, il pourrait ĂȘtre implĂ©mentĂ© Ă  la place comme un initplan :

EXPLAIN VERBOSE SELECT unique1
FROM tenk1 t1 WHERE t1.ten = (SELECT (random() * 10)::integer);

                             QUERY PLAN
------------------------------------------------------------​--------
 Seq Scan on public.tenk1 t1  (cost=0.02..470.02 rows=1000 width=4)
   Output: t1.unique1
   Filter: (t1.ten = (InitPlan 1).col1)
   InitPlan 1
     ->  Result  (cost=0.00..0.02 rows=1 width=4)
           Output: ((random() * '10'::double precision))::integer

Un initplan est exĂ©cutĂ© une seule fois par exĂ©cution du plan externe, et les rĂ©sultats sont sauvegardĂ©s pour une rĂ©-utilisation par les lignes suivantes du plan externe. Donc dans cet exemple, random() est Ă©valuĂ© seulement une fois et toutes les valeurs de t1.ten sont comparĂ©es au mĂȘme entier choisi au hasard. C'est trĂšs diffĂ©rent de ce qui serait survenu sans la construction du sous-SELECT.

14.1.2. EXPLAIN ANALYZE #

Il est possible de vĂ©rifier l'exactitude des estimations du planificateur en utilisant l'option ANALYZE de EXPLAIN. Avec cette option, EXPLAIN exĂ©cute vraiment la requĂȘte, puis affiche le vrai nombre de lignes et les vrais temps passĂ©s dans chaque nƓud, avec ceux estimĂ©s par un simple EXPLAIN. Par exemple, nous pourrions avoir un rĂ©sultat tel que :

EXPLAIN ANALYZE SELECT *
FROM tenk1 t1, tenk2 t2
WHERE t1.unique1 < 10 AND t1.unique2 = t2.unique2;

                                                           QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------
 Nested Loop  (cost=4.65..118.50 rows=10 width=488) (actual time=0.017..0.051 rows=10 loops=1)
   ->  Bitmap Heap Scan on tenk1 t1  (cost=4.36..39.38 rows=10 width=244) (actual time=0.009..0.017 rows=10 loops=1)
          Recheck Cond: (unique1 < 10)
          Heap Blocks: exact=10
          ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..4.36 rows=10 width=0) (actual time=0.004..0.004 rows=10 loops=1)
                Index Cond: (unique1 < 10)
   ->  Index Scan using tenk2_unique2 on tenk2 t2  (cost=0.29..7.90 rows=1 width=244) (actual time=0.003..0.003 rows=1 loops=10)
          Index Cond: (unique2 = t1.unique2)
 Planning Time: 0.485 ms
 Execution Time: 0.073 ms

Notez que les valeurs « temps rĂ©el Â» sont en millisecondes alors que les estimations de « coĂ»t Â» sont exprimĂ©es dans des unitĂ©s arbitraires ; il y a donc peu de chances qu'elles correspondent. L'information qu'il faut gĂ©nĂ©ralement rechercher est si le nombre de lignes estimĂ©es est raisonnablement proche de la rĂ©alitĂ©. Dans cet exemple, les estimations Ă©taient toutes rigoureusement exactes, mais c'est en pratique plutĂŽt inhabituel.

Dans certains plans de requĂȘte, il est possible qu'un nƓud de sous-plan soit exĂ©cutĂ© plus d'une fois. Par exemple, le parcours d'index interne est exĂ©cutĂ© une fois par ligne externe dans le plan de boucle imbriquĂ©e ci-dessus. Dans de tels cas, la valeur loops renvoie le nombre total d'exĂ©cutions du nƓud, et le temps rĂ©el et les valeurs des lignes affichĂ©es sont une moyenne par exĂ©cution. Ceci est fait pour que les nombres soient comparables avec la façon dont les estimations de coĂ»ts sont affichĂ©es. Multipliez par la valeur de loops pour obtenir le temps total rĂ©ellement passĂ© dans le nƓud. Dans l'exemple prĂ©cĂ©dent, le parcours d'index sur tenk2 a pris un total de 0,030 milliseconde.

Dans certains cas, EXPLAIN ANALYZE affiche des statistiques d'exĂ©cution supplĂ©mentaires aprĂšs le temps et le nombre de lignes de l'exĂ©cution d'un nƓud du plan. Par exemple, les nƓuds de tri et de hachage fournissent des informations supplĂ©mentaires :

EXPLAIN ANALYZE SELECT *
FROM tenk1 t1, tenk2 t2
WHERE t1.unique1 < 100 AND t1.unique2 = t2.unique2 ORDER BY t1.fivethous;

                                                                 QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------
 Sort  (cost=713.05..713.30 rows=100 width=488) (actual time=2.995..3.002 rows=100 loops=1)
    Sort Key: t1.fivethous
   Sort Method: quicksort  Memory: 74kB
   ->  Hash Join  (cost=226.23..709.73 rows=100 width=488) (actual time=0.515..2.920 rows=100 loops=1)
          Hash Cond: (t2.unique2 = t1.unique2)
         ->  Seq Scan on tenk2 t2  (cost=0.00..445.00 rows=10000 width=244) (actual time=0.026..1.790 rows=10000 loops=1)
         ->  Hash  (cost=224.98..224.98 rows=100 width=244) (actual time=0.476..0.477 rows=100 loops=1)
               Buckets: 1024  Batches: 1  Memory Usage: 35kB
               ->  Bitmap Heap Scan on tenk1 t1  (cost=5.06..224.98 rows=100 width=244) (actual time=0.030..0.450 rows=100 loops=1)
                      Recheck Cond: (unique1 < 100)
                     Heap Blocks: exact=90
                     ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.013..0.013 rows=100 loops=1)
                            Index Cond: (unique1 < 100)
 Planning Time: 0.187 ms
 Execution Time: 3.036 ms

Le nƓud de tri donne la mĂ©thode de tri utilisĂ©e (en particulier, si le tri s'est effectuĂ© en mĂ©moire ou sur disque) ainsi que la quantitĂ© de mĂ©moire ou d'espace disque requis. Le nƓud de hachage montre le nombre de paquets de hachage, le nombre de lots ainsi la quantitĂ© maximale de mĂ©moire utilisĂ©e pour la table de hachage (si le nombre de lots est supĂ©rieur Ă  un, il y aura Ă©galement l'utilisation de l'espace disque impliquĂ©, mais cela n'est pas montrĂ© dans cet exemple).

Un autre type d'information supplĂ©mentaire est le nombre de lignes supprimĂ©es par une condition de filtrage :

EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE ten < 7;

                                                QUERY PLAN
----------------------------------------------------------------------------------------------------------
 Seq Scan on tenk1  (cost=0.00..470.00 rows=7000 width=244) (actual time=0.030..1.995 rows=7000 loops=1)
   Filter: (ten < 7)
   Rows Removed by Filter: 3000
 Planning Time: 0.102 ms
 Execution Time: 2.145 ms

Ces nombres peuvent ĂȘtre particuliĂšrement prĂ©cieux pour les conditions de filtres appliquĂ©es aux nƓuds de jointure. La ligne « Rows Removed Â» n'apparaĂźt que si au moins une ligne parcourue, ou une ligne potentiellement appairĂ©e dans le cas d'un nƓud de jointure, est rejetĂ©e par la condition de filtre.

Un cas similaire aux conditions de filtre apparaĂźt avec des parcours d'index « avec perte Â». Par exemple, regardez cette recherche de polygone contenant un point spĂ©cifique :

EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @> polygon '(0.5,2.0)';

                                              QUERY PLAN
------------------------------------------------------------------------------------------------------
 Seq Scan on polygon_tbl  (cost=0.00..1.09 rows=1 width=85) (actual time=0.023..0.023 rows=0 loops=1)
   Filter: (f1 @> '((0.5,2))'::polygon)
   Rows Removed by Filter: 7
 Planning Time: 0.039 ms
 Execution Time: 0.033 ms

Le planificateur pense (plutĂŽt correctement) que cette table d'Ă©chantillon est trop petite pour s'embĂȘter avec un parcours d'index, et utilise donc un parcours sĂ©quentiel dans lequel toutes les lignes sont rejetĂ©es par la condition de filtre. Mais si nous forçons l'utilisation d'un parcours d'index, nous voyons :

SET enable_seqscan TO off;

EXPLAIN ANALYZE SELECT * FROM polygon_tbl WHERE f1 @> polygon '(0.5,2.0)';

                                                        QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------
 Index Scan using gpolygonind on polygon_tbl  (cost=0.13..8.15 rows=1 width=85) (actual time=0.074..0.074 rows=0 loops=1)
   Index Cond: (f1 @> '((0.5,2))'::polygon)
   Rows Removed by Index Recheck: 1
 Planning Time: 0.039 ms
 Execution Time: 0.098 ms

L'index retourne une ligne candidate, qui est ensuite rejetĂ©e par une deuxiĂšme vĂ©rification de la condition de l'index. Cela arrive car un index GiST est « avec perte Â» pour les tests de contenance de polygone : il retourne en fait les lignes pour lesquelles les polygones chevauchent la cible, ce qui nĂ©cessite aprĂšs coup un test de contenance exacte sur ces lignes.

EXPLAIN a une option BUFFERS qui peut ĂȘtre utilisĂ©e avec ANALYZE pour obtenir encore plus de statistiques d'exĂ©cution:

EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000;

                                                           QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on tenk1  (cost=25.07..60.11 rows=10 width=244) (actual time=0.105..0.114 rows=10 loops=1)
    Recheck Cond: ((unique1 < 100) AND (unique2 > 9000))
   Heap Blocks: exact=10
   Buffers: shared hit=14 read=3
   ->  BitmapAnd  (cost=25.07..25.07 rows=10 width=0) (actual time=0.100..0.101 rows=0 loops=1)
         Buffers: shared hit=4 read=3
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.027..0.027 rows=100 loops=1)
                Index Cond: (unique1 < 100)
                Buffers: shared hit=2
         ->  Bitmap Index Scan on tenk1_unique2  (cost=0.00..19.78 rows=999 width=0) (actual time=0.070..0.070 rows=999 loops=1)
                Index Cond: (unique2 > 9000)
               Buffers: shared hit=2 read=3
 Planning:
   Buffers: shared hit=3
 Planning Time: 0.162 ms
 Execution Time: 0.143 ms

Les nombres fournis par BUFFERS aident Ă  identifier les parties de la requĂȘte les plus intensives en termes d'entrĂ©es sorties.

Il faut garder en tĂȘte que comme EXPLAIN ANALYZE exĂ©cute vraiment la requĂȘte, tous les effets secondaires se produiront comme d'habitude, mĂȘme si, quel que soit l'affichage de la requĂȘte, il est remplacĂ© par la sortie des donnĂ©es d'EXPLAIN. Si vous voulez analyser une requĂȘte modifiant les donnĂ©es sans changer les donnĂ©es en table, vous pouvez annuler les modifications aprĂšs, par exemple :

BEGIN;
EXPLAIN ANALYZE UPDATE tenk1 SET hundred = hundred + 1 WHERE unique1 < 100;

                                                           QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------
 Update on tenk1  (cost=5.06..225.23 rows=0 width=0) (actual time=1.634..1.635 rows=0 loops=1)
   ->  Bitmap Heap Scan on tenk1  (cost=5.06..225.23 rows=100 width=10) (actual time=0.065..0.141 rows=100 loops=1)
        Recheck Cond: (unique1 < 100)
        Heap Blocks: exact=90
         ->  Bitmap Index Scan on tenk1_unique1  (cost=0.00..5.04 rows=100 width=0) (actual time=0.031..0.031 rows=100 loops=1)
               Index Cond: (unique1 < 100)
 Planning Time: 0.151 ms
 Execution Time: 1.856 ms


ROLLBACK;

Comme vous pouvez le voir dans cet exemple, quand la requĂȘte contient une commande INSERT, UPDATE, DELETE ou MERGE, l'application des changements est faite au niveau du nƓud principal Insert, Update, Delete ou Merge du plan. Les nƓuds du plan sous celui-ci effectuent le travail de recherche des anciennes lignes et/ou le calcul des nouvelles donnĂ©es. Ainsi, au-dessus, on peut voir les mĂȘmes tris de parcours de bitmap dĂ©jĂ  vus prĂ©cĂ©demment, et leur sortie est envoyĂ©e Ă  un nƓud de mise Ă  jour qui stocke les lignes modifiĂ©es. Il est intĂ©ressant de noter que bien que le nƓud de modification de donnĂ©es puisse prendre une part considĂ©rable sur le temps d'exĂ©cution (ici, c'est la partie la plus gourmande), le planificateur n'ajoute rien au coĂ»t estimĂ© pour considĂ©rer ce travail. C'est dĂ» au fait que le travail Ă  effectuer est le mĂȘme pour chaque plan de requĂȘte correct, et n'affecte donc pas les dĂ©cisions du planificateur.

La phrase Planning time affichĂ©e par EXPLAIN ANALYZE correspond au temps pris pour gĂ©nĂ©rer et optimiser le plan de requĂȘtes Ă  partir de la requĂȘte analysĂ©e. Cela n'inclut pas l'analyse syntaxique et la réécriture.

Le Temps total d'exĂ©cution donnĂ© par EXPLAIN ANALYZE inclut le temps de dĂ©marrage et d'arrĂȘt de l'exĂ©cuteur, ainsi que le temps d'exĂ©cution de tous les triggers pouvant ĂȘtre dĂ©clenchĂ©s, mais n'inclut pas les temps d'analyse, de réécriture ou de planification. Le temps passĂ© Ă  exĂ©cuter les triggers BEFORE, s'il y en a, est inclus dans le temps passĂ© Ă  l'exĂ©cution des nƓuds Insert, Update ou Delete associĂ©s, mais le temps passĂ© Ă  exĂ©cuter les triggers AFTER n'est pas comptĂ©, car les triggers AFTER sont dĂ©clenchĂ©s aprĂšs l'achĂšvement du plan entier. Le temps total passĂ© dans chaque trigger (que ce soit BEFORE ou AFTER) est affichĂ© sĂ©parĂ©ment. Notez que les triggers de contrainte ne seront pas exĂ©cutĂ©s avant la fin de la transaction et par consĂ©quent ne seront pas affichĂ©s du tout par EXPLAIN ANALYZE.

Le temps affichĂ© sur le premier nƓud n'inclut pas le temps nĂ©cessaire pour convertir les donnĂ©es en sortie de la requĂȘte en leur forme affichable et ne les envoie pas au client. Bien que EXPLAIN ANALYZE n'enverra jamais les donnĂ©es au client, il est possible de lui demander de convertir les donnĂ©es en sortie de la requĂȘte en leur forme affichable et de mesurer le temps nĂ©cessaire pour ce faire, grĂące Ă  l'option SERIALIZE. Ce temps sera affichĂ© sĂ©parĂ©ment, et il sera aussi inclus dans le total de Execution time.

14.1.3. Avertissements #

Il existe deux raisons importantes pour lesquelles les temps d'exĂ©cution mesurĂ©s par EXPLAIN ANALYZE peuvent dĂ©vier de l'exĂ©cution normale de la mĂȘme requĂȘte. Tout d'abord, comme aucune ligne n'est rĂ©ellement envoyĂ©e au client, les coĂ»ts de conversion rĂ©seau ne sont pas inclus. Les coĂ»ts de conversion des entrĂ©es/sorties disque ne sont pas non plus inclus sauf si l'option SERIALIZE est demandĂ©e. Ensuite, le surcoĂ»t de mesure induit par EXPLAIN ANALYZE peut ĂȘtre significatif, plus particuliĂšrement sur les machines avec un appel systĂšme gettimeofday() lent. Vous pouvez utiliser l'outil pg_test_timing pour mesurer le surcoĂ»t du calcul du temps sur votre systĂšme.

Les rĂ©sultats de EXPLAIN ne devraient pas ĂȘtre extrapolĂ©s pour des situations autres que celles de vos tests en cours ; par exemple, les rĂ©sultats sur une petite table ne peuvent ĂȘtre appliquĂ©s Ă  des tables bien plus volumineuses. Les estimations de coĂ»t du planificateur ne sont pas linĂ©aires et, du coup, il pourrait bien choisir un plan diffĂ©rent pour une table plus petite ou plus grande. Un exemple extrĂȘme est celui d'une table occupant une page disque. Vous obtiendrez pratiquement toujours un parcours sĂ©quentiel, que des index soient disponibles ou non. Le planificateur rĂ©alise que cela va nĂ©cessiter la lecture d'un seul bloc disque pour traiter la table dans ce cas, il n'y a donc pas d'intĂ©rĂȘt Ă  Ă©tendre des lectures de blocs supplĂ©mentaires pour un index. (Nous voyons cela arriver dans l'exemple polygon_tbl au-dessus.)

Ici, ce sont des cas dans lesquels les valeurs rĂ©elles et estimĂ©es ne correspondent pas vraiment, mais qui ne sont pas non plus totalement fausses. Un tel cas peut se produire quand un nƓud d'exĂ©cution d'un plan est arrĂȘtĂ© par un LIMIT ou autre chose avec un effet similaire. Par exemple, dans la requĂȘte LIMIT utilisĂ©e prĂ©cĂ©demment,

EXPLAIN ANALYZE SELECT * FROM tenk1 WHERE unique1 < 100 AND unique2 > 9000 LIMIT 2;

                                                          QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=0.29..14.33 rows=2 width=244) (actual time=0.051..0.071 rows=2 loops=1)
   ->  Index Scan using tenk1_unique2 on tenk1  (cost=0.29..70.50 rows=10 width=244) (actual time=0.051..0.070 rows=2 loops=1)
         Index Cond: (unique2 > 9000)
         Filter: (unique1 < 100)
         Rows Removed by Filter: 287
 Planning Time: 0.077 ms
 Execution Time: 0.086 ms

les estimations de coĂ»t et de nombre de lignes pour le nƓud de parcours d'index sont affichĂ©es comme s'ils devaient s'exĂ©cuter jusqu'Ă  la fin. Mais en rĂ©alitĂ© le nƓud Limit arrĂȘte la rĂ©cupĂ©ration des lignes aprĂšs la deuxiĂšme ligne rĂ©cupĂ©rĂ©e, et donc le vrai nombre de lignes n'est que de 2 et le temps d'exĂ©cution est moindre que ne le suggĂ©rait le coĂ»t estimĂ©. Ce n'est pas une erreur d'estimation, juste une contradiction entre la façon dont l'estimation et les valeurs rĂ©elles sont affichĂ©es.

Les jointures d'assemblage ont Ă©galement leurs artefacts de mesure qui peuvent embrouiller une personne non avertie. Une jointure d'assemblage arrĂȘtera la lecture d'une entrĂ©e si l'autre entrĂ©e est Ă©puisĂ©e et que la prochaine valeur clĂ© dans la premiĂšre entrĂ©e est supĂ©rieure Ă  la derniĂšre valeur clĂ© de l'autre entrĂ©e ; dans un cas comme ça, il ne peut plus y avoir de correspondance et il est donc inutile de parcourir le reste de la premiĂšre entrĂ©e. Cela a donc pour consĂ©quence de ne pas lire entiĂšrement un des fils, avec des rĂ©sultats similaires Ă  ceux mentionnĂ©s pour LIMIT. De mĂȘme, si le fils externe (premier fils) contient des lignes avec des valeurs de clĂ© dupliquĂ©es, le fils externe (second fils) est sauvegardĂ© et les lignes correspondant Ă  cette valeur clĂ© sont parcourues de nouveau. EXPLAIN ANALYZE compte ces Ă©missions rĂ©pĂ©tĂ©es de mĂȘmes lignes internes comme si elles Ă©taient de vraies lignes supplĂ©mentaires. Quand il y a de nombreux doublons externes, le nombre rĂ©el de lignes affichĂ© pour le nƓud de plan du fils interne peut ĂȘtre significativement plus grand que le nombre de lignes qu'il y a vraiment dans la relation interne.

Les nƓuds BitmapAnd et BitmapOr affichent toujours un nombre de lignes rĂ©elles Ă  0, du fait des limitations d'implĂ©mentation.

Habituellement, la sortie d'EXPLAIN affichera chaque nƓud de plan gĂ©nĂ©rĂ© par le planificateur de requĂȘtes. NĂ©anmoins, il existe des cas oĂč l'exĂ©cuteur peut dĂ©terminer que certains nƓuds n'ont pas besoin d'ĂȘtre exĂ©cutĂ©s car ils ne produisent aucune ligne. (Actuellement, ceci peut n'arriver qu'aux nƓuds enfants du nƓud Append qui parcourt une table partitionnĂ©e.) Quand cela arrive, ces nƓuds sont omis de la sortie de la commande EXPLAIN et une annotation Subplans Removed: N apparaĂźt Ă  la place.