En principe, le planificateur ne tient pas compte de l’ordre des
tables dans la clause FROM et peut décider de commencer la
requête par n’importe laquelle. Cependant, la complexité des plans
d’exécution à étudier explose avec le nombre de tables et de
combinaisons de jointures. À partir d’un certain nombre de tables,
PostgreSQL n’en prend en compte qu’un certain nombre, et calcule un plan
juste pour elles. Les tables suivantes sont jointes au premier résultat
dans un deuxième temps. Le paramètre join_collapse_limit
définit ce nombre de tables prises en compte dans le FROM
(vision simplifiée). Par défaut, il ne vaut que 8.
Si le critère de filtrage le plus utile est sur la neuvième table,
au-delà donc du premier ensemble de tables considérées, l’optimiseur
n’en tiendra pas compte, et le plan peut être catastrophique !
Il existe un paramètre très voisin, from_collapse_limit,
qui définit à quelle profondeur « aplatir » les sous-requêtes,
c’est-à-dire remonter les tables depuis une sous-requête dans le
FROM, tant qu’on ne pas dépasse pas cette valeur. On le
monte habituellement à la même valeur que
join_collapse_limit, et vice-versa.
Exemple :
Comme exemple de plan désastreux, à cause d’un critère de filtrage
sur une table non prise en compte, voir ce plan . La
requête est celle-ci (base magasin ) :
SET search_path TO magasin,facturation ;
SELECT SUM (reglements.montant) AS somme_reglements
FROM factures
INNER JOIN reglements USING (numero_facture)
INNER JOIN commandes USING (numero_commande)
INNER JOIN clients cl USING (client_id)
INNER JOIN types_clients USING (type_client)
INNER JOIN lignes_commandes lc USING (numero_commande)
INNER JOIN lots l ON (l.numero_lot = lc.numero_lot_expedition)
INNER JOIN transporteurs USING (transporteur_id)
INNER JOIN contacts ct ON (ct.contact_id = cl.contact_id)
WHERE transporteurs.nom = 'Royal Air Drone'
AND ct.login = 'Beatty_Brahem' ;
Le critère final (sur contacts.login) est beaucoup plus
discriminant que celui sur transporteurs.nom mais il n’est
pas pris en compte car c’est la neuvième table. Monter
join_collapse_limit de 8 à 9 permet de prendre ce critère
en compte, et le plan est
alors bien meilleur . (Pour l’exemple complet, voir https:/dali.bo/j2_html#requête-avec-beaucoup-de-tables-1 )
Une autre solution serait de remonter plus haut dans le
FROM la jointure sur la table contacts. Son
critère discriminant serait ainsi pris en compte dès le début. Mais il
n’est pas toujours possible de modifier le code, qui d’ailleurs peut
être dynamique.
Les paramètres join_collapse_limit et
from_collapse_limit sont trop peu connus, alors qu’ils
peuvent améliorer radicalement les performances si vous joignez plus de
huit tables.
Pour éviter de se soucier de ce problème, il est fréquent de monter
les valeurs de join_collapse_limit et
from_collapse_limit à 10 ou 12.
Si vous avez de nombreuses requêtes avec beaucoup de tables et que
vous montez join_collapse_limit, les temps de planification
peuvent fortement augmenter, ainsi que la consommation CPU. Ce ne sera
pas forcément contrebalancé par des requêtes toutes plus rapides. Il est
donc préférable de positionner ces valeurs élevées uniquement au niveau
de la session, de l’écran, ou de l’utilisateur, et s’il est montré que
ces requêtes sont parfois contre-performantes.
Concrètement, pour ne changer la valeur que dans une session,
utilisez SET :
SET join_collapse_limit = … ;
SET from_collapse_limit = … ;
ou pour un utilisateur précis, un rôle de reporting par
exemple, utilisez :
ALTER ROLE … SET join_collapse_limit = …
Par contre, dans une base dédiée à la BI, ou un utilisateur dédié,
avec peu de requêtes, qui portent sur de nombreuses tables, il ne faut
pas hésiter à monter join_collapse_limit au nombre maximal
de tables susceptibles d’être rencontrées. Des coûts en planification de
l’ordre de la seconde ne sont pas forcément rédhibitoires dans ce
contexte.
À l’inverse, descendre join_collapse_limit à 1 permet de
dicter l’ordre d’exécution au planificateur dans la clause
FROM (voir
l’exemple de la documentation officielle ). C’est une mesure de
dernier recours.
join_collapse_limit
et from_collapse_limit
En principe, le planificateur ne tient pas compte de l’ordre des
tables dans la clause FROM et peut décider de commencer la
requête par n’importe laquelle. Cependant, la complexité des plans
d’exécution à étudier explose avec le nombre de tables et de
combinaisons de jointures. À partir d’un certain nombre de tables,
PostgreSQL n’en prend en compte qu’un certain nombre, et calcule un plan
juste pour elles. Les tables suivantes sont jointes au premier résultat
dans un deuxième temps. Le paramètre join_collapse_limit
définit ce nombre de tables prises en compte dans le FROM
(vision simplifiée). Par défaut, il ne vaut que 8.
Si le critère de filtrage le plus utile est sur la neuvième table,
au-delà donc du premier ensemble de tables considérées, l’optimiseur
n’en tiendra pas compte, et le plan peut être catastrophique !
Il existe un paramètre très voisin, from_collapse_limit,
qui définit à quelle profondeur « aplatir » les sous-requêtes,
c’est-à-dire remonter les tables depuis une sous-requête dans le
FROM, tant qu’on ne pas dépasse pas cette valeur. On le
monte habituellement à la même valeur que
join_collapse_limit, et vice-versa.
Exemple :
Comme exemple de plan désastreux, à cause d’un critère de filtrage
sur une table non prise en compte, voir ce plan . La
requête est celle-ci (base magasin ) :
SET search_path TO magasin,facturation ;
SELECT SUM (reglements.montant) AS somme_reglements
FROM factures
INNER JOIN reglements USING (numero_facture)
INNER JOIN commandes USING (numero_commande)
INNER JOIN clients cl USING (client_id)
INNER JOIN types_clients USING (type_client)
INNER JOIN lignes_commandes lc USING (numero_commande)
INNER JOIN lots l ON (l.numero_lot = lc.numero_lot_expedition)
INNER JOIN transporteurs USING (transporteur_id)
INNER JOIN contacts ct ON (ct.contact_id = cl.contact_id)
WHERE transporteurs.nom = 'Royal Air Drone'
AND ct.login = 'Beatty_Brahem' ;
Le critère final (sur contacts.login) est beaucoup plus
discriminant que celui sur transporteurs.nom mais il n’est
pas pris en compte car c’est la neuvième table. Monter
join_collapse_limit de 8 à 9 permet de prendre ce critère
en compte, et le plan est
alors bien meilleur . (Pour l’exemple complet, voir https:/dali.bo/j2_html#requête-avec-beaucoup-de-tables-1 )
Une autre solution serait de remonter plus haut dans le
FROM la jointure sur la table contacts. Son
critère discriminant serait ainsi pris en compte dès le début. Mais il
n’est pas toujours possible de modifier le code, qui d’ailleurs peut
être dynamique.
Les paramètres join_collapse_limit et
from_collapse_limit sont trop peu connus, alors qu’ils
peuvent améliorer radicalement les performances si vous joignez plus de
huit tables.
Pour éviter de se soucier de ce problème, il est fréquent de monter
les valeurs de join_collapse_limit et
from_collapse_limit à 10 ou 12.
Si vous avez de nombreuses requêtes avec beaucoup de tables et que
vous montez join_collapse_limit, les temps de planification
peuvent fortement augmenter, ainsi que la consommation CPU. Ce ne sera
pas forcément contrebalancé par des requêtes toutes plus rapides. Il est
donc préférable de positionner ces valeurs élevées uniquement au niveau
de la session, de l’écran, ou de l’utilisateur, et s’il est montré que
ces requêtes sont parfois contre-performantes.
Concrètement, pour ne changer la valeur que dans une session,
utilisez SET :
SET join_collapse_limit = … ;
SET from_collapse_limit = … ;
ou pour un utilisateur précis, un rôle de reporting par
exemple, utilisez :
ALTER ROLE … SET join_collapse_limit = …
Par contre, dans une base dédiée à la BI, ou un utilisateur dédié,
avec peu de requêtes, qui portent sur de nombreuses tables, il ne faut
pas hésiter à monter join_collapse_limit au nombre maximal
de tables susceptibles d’être rencontrées. Des coûts en planification de
l’ordre de la seconde ne sont pas forcément rédhibitoires dans ce
contexte.
À l’inverse, descendre join_collapse_limit à 1 permet de
dicter l’ordre d’exécution au planificateur dans la clause
FROM (voir
l’exemple de la documentation officielle ). C’est une mesure de
dernier recours.
GEQO, l’optimiseur génétique
Principe du GEQO :
Quand il y a beaucoup de tables, et que l’on monte
join_collapse_limit, le nombre colossal de plans
envisageables rend une recherche exhaustive du meilleur plan beaucoup
trop lourde. PostgreSQL déclenche alors un autre mécanisme au-delà de 12
tables (valeur du paramètre geqo_threshold) : l’optimiseur
génétique GEQO
(GEnetic Query Optimizer ).
Comme tout algorithme génétique, il fonctionne par introduction de
mutations aléatoires sur un état initial donné. Il permet de planifier
rapidement une requête complexe, et d’obtenir un plan d’exécution dans
un délai raisonnable. Ce plan là n’est pas forcément optimal, mais assez
proche pour être acceptable.
Le
code source de PostgreSQL décrit le principe, résumé aussi dans ce
schéma :
Principe d’un algorithme génétique
(schéma de la documentation officielle, licence PostgreSQL)
Ce mécanisme est configuré par des paramètres dont les noms
commencent par geqo, notamment :
geqo (par défaut à on) permet
d’activer/désactiver GEQO ;
geqo_threshold (défaut : 12) est le nombre minimum
d’éléments à joindre dans un FROM avant de déclencher le
mécanisme GEQO au lieu du planificateur exhaustif ;
geqo_seed (la « graine » pour la génération aléatoire,
0 par défaut) permet de forcer la recherche d’autres plans ;
geqo_effort (défaut : 5) influe sur d’autres
paramètres, comme la taille de l’échantillon de départ, la sélection…
qui ne seront pas évoqués ici.
Il est déconseillé de toucher à ces paramètres sans savoir ce que
l’on fait.
Noter que malgré l’introduction de ces mutations aléatoires, le
moteur arrive tout de même à conserver un fonctionnement
déterministe . En effet, tant que la « graine » du générateur
aléatoire (geqo_seed) et les autres paramètres contrôlant
GEQO restent inchangés, les
plans générés seront toujours les mêmes , toutes choses égales par
ailleurs bien sûr (statistiques, paramètres…).
Paramétrage du GEQO :
Le paramétrage par défaut convient généralement.
Surtout, il est déconseillé de désactiver geqo ou de
monter geqo_threshold au-delà de 12, sous peine d’explosion
du temps de planification et de la consommation mémoire avec de
nombreuses jointures.
Pour des requêtes dépassant ce nombre de jointures, il faudra souvent
monter join_collapse_limit plus ou moins haut. Le GEQO
permet d’éviter que le temps de planification augmente de manière
exponentielle. Noter que si join_collapse_limit est monté
entre 9 et 11, le temps de planification peut fortement augmenter car le
GEQO n’intervient pas. Une fois le seuil d’intervention du GEQO dépassé,
la montée du temps de planification en fonction de
join_collapse_limit reste beaucoup plus maîtrisée.
Si le plan sélectionné n’est pas satisfaisant, il est possible d’en
explorer d’autres en changeant le paramètre geqo_seed (voir
la documentation
officielle ). C’est surtout un moyen de voir si de meilleurs plans
(avec un coût bien plus bas) peuvent être trouvés, car un
SET geqo_seed = … n’est pas une mesure pérenne (les données
changent et les requêtes sont souvent dynamiques). Dans l’idéal, le GEQO
retombe toujours sur des plans de coûts voisins quelque soit
geqo_seed.
Si ça n’est pas le cas, les plans obtenus risquent de trop varier
d’un appel à l’autre. Il est alors possible de monter
geqo_effort (défaut : 5) jusque 10, pour augmenter son
pool de plans notamment. Le temps de planification montera au
moins proportionnellement, pour un résultat normalement plus stable et
plus fiable.
S’il faut en arriver là, il faut bien contrôler que le temps de
planification reste tolérable. Il y a peut-être aussi besoin de vérifier
la pertinence de requêtes avec des dizaines de jointures.