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.
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 =
.(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
constant
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
(
,
qui fait référence à la nom_sousplan
).colN
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
.
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
.
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:
apparaĂźt Ă la place.
N