Comprendre EXPLAIN

Module J2

Dalibo SCOP

24.12

18 décembre 2024

Sur ce document

Formation Module J2
Titre Comprendre EXPLAIN
Révision 24.12
PDF https://dali.bo/j2_pdf
EPUB https://dali.bo/j2_epub
HTML https://dali.bo/j2_html
Slides https://dali.bo/j2_slides
TP https://dali.bo/j2_tp
TP (solutions) https://dali.bo/j2_solutions

Licence Creative Commons CC-BY-NC-SA

Cette formation est sous licence CC-BY-NC-SA. Vous êtes libre de la redistribuer et/ou modifier aux conditions suivantes :

  • Paternité
  • Pas d’utilisation commerciale
  • Partage des conditions initiales à l’identique

Marques déposées

PostgreSQL® Postgres® et le logo Slonik sont des marques déposées par PostgreSQL Community Association of Canada.

Versions de PostgreSQL couvertes

Ce document ne couvre que les versions supportées de PostgreSQL au moment de sa rédaction, soit les versions 13 à 17.

Comprendre EXPLAIN

PostgreSQL

Introduction

  • Le matériel, le système et la configuration sont importants pour les performances
  • Mais il est aussi essentiel de se préoccuper des requêtes et de leurs performances

Au menu

  • Exécution globale d’une requête
  • Planificateur : utilité, statistiques et configuration
  • EXPLAIN
  • Nœuds d’un plan
  • Outils

Exécution globale d’une requête

  • L’exécution peut se voir sur deux niveaux
    • niveau système
    • niveau SGBD
  • De toute façon, composée de plusieurs étapes

Niveau système

  • Le client envoie une requête au serveur de bases de données
  • Le serveur l’exécute
  • Puis il renvoie le résultat au client

Traitement d’une requête

Traitement d’une requête SQL

Exceptions

  • Procédures stockées (appelées avec CALL)
  • Requêtes DDL
  • Instructions TRUNCATE et COPY
  • Pas de réécriture, pas de plans d’exécution…
    • une exécution directe

Quelques définitions

  • Prédicat
    • filtre de la clause WHERE
    • conditions de jointure
  • Sélectivité
    • % de lignes retournées après application d’un prédicat
  • Cardinalité
    • nombre de lignes d’une table
    • nombre de lignes retournées après filtrages

Jeu de tests

  • Tables
    • services : 4 lignes
    • services_big : 40 000 lignes
    • employes : 14 lignes
    • employes_big : ~500 000 lignes
  • Index
    • service*.num_service (clés primaires)
    • employes*.matricule (clés primaires)
    • employes*.date_embauche
    • employes_big.num_service (clé étrangère)

Jeu de tests (schéma)

Tables employés & services

Requête étudiée

SELECT matricule, nom, prenom, nom_service, fonction, localisation
FROM   employes emp
JOIN   services ser ON (emp.num_service = ser.num_service)
WHERE  ser.localisation = 'Nantes';

Plan de la requête étudiée

L’objet de ce module est de comprendre son plan d’exécution :

 Hash Join  (cost=1.06..2.28 rows=4 width=48)
   Hash Cond: (emp.num_service = ser.num_service)
   ->  Seq Scan on employes emp  (cost=0.00..1.14 rows=14 width=35)
   ->  Hash  (cost=1.05..1.05 rows=1 width=21)
         ->  Seq Scan on services ser  (cost=0.00..1.05 rows=1 width=21)
               Filter: ((localisation)::text = 'Nantes'::text)

Planificateur

Rappels :

  • SQL est un langage déclaratif
  • Planificateur : trouver le meilleur plan
  • Énumère tous les plans d’exécution possible
    • tous ou presque…
  • Statistiques + configuration + règles → coût
  • Coût le plus bas = meilleur plan

Règles

  • Règle 1 : récupérer le bon résultat

  • Règle 2 : le plus rapidement possible

    • en minimisant les opérations disques
    • en préférant les lectures séquentielles
    • en minimisant la charge CPU
    • en minimisant l’utilisation de la mémoire

Outils de l’optimiseur

  • L’optimiseur s’appuie sur :
    • un mécanisme de calcul de coûts
    • des statistiques sur les données
    • le schéma de la base de données

Optimisations

  • À partir du modèle de données
    • suppression de jointures externes inutiles
  • Transformation des sous-requêtes
    • certaines sous-requêtes transformées en jointures
    • ex : critere IN (SELECT ...)
  • Appliquer les prédicats le plus tôt possible
    • réduit le jeu de données manipulé
  • Intègre le code des fonctions SQL simples (inline)
    • évite un appel de fonction coûteux

Décisions

L’optimiseur doit choisir :

  • Stratégie d’accès aux lignes
    • parcours de table, d’index, fonction, etc.
  • Stratégie d’utilisation des jointures
    • ordre
    • ordre des tables jointes
    • type (Nested Loop, Merge Join, Hash Join…)
  • Stratégie d’agrégation
    • brut, trié, haché
  • En version parallélisée ?
  • Tenir compte de la consommation mémoire

Parallélisation

  • Processus supplémentaires pour certains nœuds
    • parer à la limitation par le CPU
  • En lecture (sauf exceptions)
  • Parcours séquentiel
  • Jointures : Nested Loop / Hash Join / Merge Join
  • Agrégats
  • Parcours d’index (B-Tree uniquement)
  • Création d’index B-Tree
  • Certaines créations de table et vues matérialisées
  • DISTINCT (v15)

Limites actuelles de la parallélisation

  • Lourd à déclencher
  • Pas sur les écritures de données
  • Très peu d’opérations DDL gérées
  • Pas en cas de verrous
  • Pas sur les curseurs
  • En évolution à chaque version

Mécanisme de coûts & statistiques

  • Modèle basé sur les coûts
    • quantifier la charge pour répondre à une requête
  • Chaque opération a un coût :
    • lire un bloc selon sa position sur le disque
    • manipuler une ligne issue d’une lecture de table ou d’index
    • appliquer un opérateur

Coûts unitaires

  • Coûts à connaître :
    • accès au disque séquentiel / non séquentiel
    • traitement d’un enregistrement issu d’une table
    • traitement d’un enregistrement issu d’un index
    • application d’un opérateur
    • traitement d’un enregistrement dans un parcours parallélisé
    • mise en place d’un parcours parallélisé
    • mise en place du JIT, du parallélisme…
  • Chaque coût = un paramètre
    • modifiable dynamiquement avec SET

Statistiques

  • Combien de lignes va-t-on traiter ?
  • Toutes les décisions du planificateur se basent sur les statistiques
    • le choix du parcours
    • comme le choix des jointures
  • Mettre à jour les statistiques sur les données :
    • ANALYZE
  • Sans bonnes statistiques, pas de bons plans !

Utilisation des statistiques

  • Les statistiques indiquent :
    • la cardinalité d’un filtre → stratégie d’accès
    • la cardinalité d’une jointure → algorithme de jointure
    • la cardinalité d’un regroupement → algorithme de regroupement

Statistiques des tables et index

  • Dans pg_class
    • relpages : taille
    • reltuples : lignes

Statistiques : mono-colonne

  • Nombre de valeurs distinctes
  • Nombre d’éléments qui n’ont pas de valeur (NULL)
  • Largeur d’une colonne
  • Distribution des données
    • tableau des valeurs les plus fréquentes
    • histogramme de répartition des valeurs

Stockage des statistiques mono-colonne

  • Stockage dans pg_statistic
    • préférer la vue pg_stats
  • Une table nouvellement créée n’a pas de statistiques
  • Utilisation :
SELECT * FROM pg_stats
  WHERE  schemaname = 'public'
  AND tablename     = 'employes'
  AND attname       = 'date_embauche' \gx

Vue pg_stats

-[ RECORD 1 ]----------+---------------------------------------------------------
schemaname             | public
tablename              | employes
attname                | date_embauche
inherited              | f
null_frac              | 0
avg_width              | 4
n_distinct             | -0.5
most_common_vals       | {2006-03-01,2006-09-01,2000-06-01,2005-03-06,2006-01-01}
most_common_freqs      | {0.214286,0.214286,0.142857,0.142857,0.142857}
histogram_bounds       | {2003-01-01,2006-06-01}
correlation            | 1
most_common_elems      | ¤
most_common_elem_freqs | ¤
elem_count_histogram   | ¤

Statistiques : multicolonnes

  • Pas par défaut
  • CREATE STATISTICS
  • Trois types de statistique
    • nombre de valeurs distinctes
    • dépendances fonctionnelles
    • liste MCV

Statistiques sur les expressions

CREATE STATISTICS employe_big_extract
ON extract('year' FROM date_embauche) FROM employes_big;
  • Résout le problème des statistiques difficiles à estimer
  • Pas créées par défaut
  • Ne pas oublier ANALYZE
  • (Avant v14 : index fonctionnel nécessaire)

Catalogues pour les statistiques étendues

Vues disponibles :

  • pg_stats_ext
  • pg_stats_ext_exprs (pour les expressions, v14)

ANALYZE

  • ANALYZE [ VERBOSE ] [ table [ ( colonne [, ...] ) ] [, ...] ]
    • sans argument : base entière
    • avec argument : table complète ou certaines colonnes
  • Un échantillon de table → statistiques
  • Table vide : conserve les anciennes statistiques
  • Nouvelle table : valeur par défaut

Fréquence d’analyse

  • Dépend principalement de la fréquence des requêtes DML
  • Autovacuum fait l’ANALYZE mais…
    • pas sur les tables temporaires
    • pas assez rapidement parfois
  • Cron
    • psql
    • ou vacuumdb --analyze-only

Échantillon statistique

  • default_statistics_target = 100
    • × 300 → 30 000 lignes au hasard
  • Configurable par colonne
ALTER TABLE matable ALTER COLUMN nomchamp SET STATISTICS 300 ;
  • Configurable par statistique étendue (v13+)
ALTER STATISTICS nom SET STATISTICS valeur ;
  • ANALYZE ensuite
  • Coût : temps de planification

Lecture d’un plan

Lecture d’un plan d’exécution

Rappel des options d’EXPLAIN

  • ANALYZE : exécution (danger !)
    • BUFFERS : blocs read/hit/written/dirtied, shared/local/temp
    • WAL : écritures dans les journaux
    • SERIALIZE : coût de sérialisation (v17)
    • SETTINGS : paramètrage en cours
  • VERBOSE : champs manipulés, workers
  • MEMORY : mémoire consommée par le planificateur (v17)
  • GENERIC_PLAN : plan générique (requête préparée, v16)
  • Désactivables avec OFF :
    • COSTS, TIMING, SUMMARY (dont planification)
  • FORMAT : sortie en texte, XML, JSON, YAML

Statistiques, cardinalités & coûts

  • Détermine à partir des statistiques
    • cardinalité des prédicats
    • cardinalité des jointures
  • Coût d’accès déterminé selon
    • des cardinalités
    • volumétrie des tables

Nœuds d’exécution les plus courants

  • Un plan est composé de nœuds
  • qui produisent des données
  • ou en consomment et en retournent
  • Chaque nœud consomme les données produites par le(s) nœud(s) parent(s)
  • Le nœud final retourne les données à l’utilisateur

Nœuds de type parcours

  • Parcours de table
  • Parcours d’index
  • Autres parcours

Parcours de table

  • Seq Scan
  • Parallel Seq Scan

Parcours de table : Seq Scan

Seq Scan

Parcours de table : paramètres

  • Seq Scan
    • seq_page_cost (défaut : 1)
    • cpu_tuple_cost & cpu_operator_cost
    • enable_seqscan
  • Parallel Seq Scan
    • parallel_tuple_cost, min_parallel_table_scan_size
    • et les autres paramètres de la parallélisation

Parcours d’index

  • Index Scan
  • Index Only Scan
  • Bitmap Index Scan
  • et leurs versions parallélisées (B-Tree)

Index Scan

Index Scan

Index Only Scan : principe

Index Only Scan

Index Only Scan : utilité & limites

  • Très performant
  • Besoin d’un VACUUM récent
    • sinon trop de Heap Fetches
  • Ne fonctionne pas pour les index fonctionnels
    • ajouter une colonne générée ?

Bitmap Scan

Bitmap Scan

Parcours d’index : paramètres importants

  • random_page_cost (4 ou moins ?)
  • cpu_index_tuple_cost
  • effective_cache_size (⅔ de la RAM ?)
  • Selon le disque :
    • effective_io_concurrency
    • maintenance_io_concurrency
  • min_parallel_index_scan_size
  • enable_indexscan, enable_indexonlyscan, enable_bitmapscan

Autres parcours

  • Function Scan
  • Values Scan
  • …et d’autres

Nœuds de jointure

  • PostgreSQL implémente les 3 algorithmes de jointures habituels
    • Nested Loop : boucle imbriquée
    • Hash Join : hachage de la table interne
    • Merge Join : tri-fusion
  • Parallélisation
  • Pour EXISTS, IN et certaines jointures externes
    • Hash Semi Join & Hash Anti Join
  • Paramètres :
    • work_mem ( et hash_mem_multiplier )
    • seq_page_cost & random_page_cost.
    • enable_nestloop, enable_hashjoin, enable_mergejoin

Nœuds de tris et de regroupements

  • Deux nœuds de tri :
    • Sort
    • Incremental Sort
  • Regroupement/agrégation :
    • Aggregate
    • Hash Aggregate
    • Group Aggregate
    • Mixed Aggregate
    • Partial/Finalize Aggregate
  • Paramètres :
    • enable_hashagg
    • work_mem & hash_mem_multiplier

Les autres nœuds

  • Limit
  • Unique (DISTINCT)
  • Append (UNION ALL), Except, Intersect
  • Gather (parallélisme)
  • InitPlan, Subplan, etc.
  • Memoize (14+)

Problèmes les plus courants

  • L’optimiseur se trompe parfois
    • mauvaises statistiques
    • écriture particulière de la requête
    • problèmes connus de l’optimiseur

Statistiques pas à jour

Les statistiques sont-elles à jour ?

  • Traitement lourd
    • faire tout de suite ANALYZE
  • Table trop grosse
    • régler l’échantillonnage
    • régler l’autovacuum sur cette table
  • Retard de mise à jour suite à crash ou restauration

Colonnes corrélées

SELECT * FROM corr1 WHERE c1=1 AND c2=1
  • Si c1 = 1 pour 20 % des lignes
  • et c2 = 1 pour 10 % des lignes
  • Alors le planificateur calcule : 2 % des lignes (20 % × 10 %)
    • Mais en réalité ?
  • Pour corriger :
CREATE STATISTICS corr1_c1_c2 ON c1,c2 FROM corr1 ;

La jointure de trop

  • PostgreSQL choisit l’ordre des jointures
    • uniquement pour les X premières tables
    • où X = join_collapse_limit (défaut : 8)
  • Les jointures supplémentaires sont ajoutées après
  • … d’où plans non optimaux
  • → augmenter join_collapse_limit si nécessaire (12-15)
    • ainsi que from_collapse_limit

Prédicats et statistiques

SELECT *
FROM employes_big
WHERE extract('year' from date_embauche) = 2006 ;
  • L’optimiseur n’a pas de statistiques sur le résultat de la fonction extract
  • Il estime la sélectivité du prédicat à 0,5 % …
  • CREATE STATISTIC (v14)

Problème avec LIKE

SELECT * FROM t1 WHERE c2 LIKE 'x%';
  • PostgreSQL peut utiliser un index dans ce cas
  • MAIS si l’encodage n’est pas C
    • déclarer l’index avec une classe d’opérateur
    • varchar_pattern_ops / text_pattern_ops, etc.
    CREATE INDEX ON matable (champ_texte varchar_pattern_ops);
  • Outils pour LIKE '%mot%' :
    • pg_trgm,
    • Full Text Search

DELETE lent

  • DELETE lent
  • Généralement un problème de clé étrangère
Delete  (actual time=111.251..111.251 rows=0 loops=1)
  ->  Hash Join  (actual time=1.094..21.402 rows=9347 loops=1)
        ->  Seq Scan on lot_a30_descr_lot
            (actual time=0.007..11.248 rows=34934 loops=1)
        ->  Hash  (actual time=0.501..0.501 rows=561 loops=1)
              ->  Bitmap Heap Scan on lot_a10_pdl
                  (actual time=0.121..0.326 rows=561 loops=1)
                    Recheck Cond: (id_fantoir_commune = 320013)
                    ->  Bitmap Index Scan on...
                        (actual time=0.101..0.101 rows=561 loops=1)
                          Index Cond: (id_fantoir_commune = 320013)
Trigger for constraint fk_lotlocal_lota30descrlot:
  time=1010.358 calls=9347
Trigger for constraint fk_nonbatia21descrsuf_lota30descrlot:
  time=2311695.025 calls=9347
Total runtime: 2312835.032 ms

Dédoublonnage

SELECT   DISTINCT t1.*   FROM t1 JOIN t2 ON (t1.id=t2.t1_id);
  • DISTINCT est souvent utilisé pour dédoublonner les lignes
    • souvent utilisé de manière abusive
    • tri !!
    • barrière à l’optimisation
  • Penser à :
    • DISTINCT ON
    • GROUP BY
  • Une clé primaire permet de dédoublonner efficacement

Index inutilisés

  • Statistiques pas à jour/peu précises/oubliées
  • Trop de lignes retournées
  • Ordre des colonnes de l’index (B-tree)
  • Index trop gros
  • Prédicat avec transformation
WHERE col1 + 2 > 5WHERE col1 > 5 - 2
  • Opérateur non supporté par l’index
WHERE col1 <> 'valeur';
  • Paramètres
    • random_page_cost
    • effective_cache_size

Écriture du SQL

  • NOT IN avec une sous-requête
    • remplacer par NOT EXISTS
  • UNION entraîne un tri systématique
    • préférer UNION ALL
  • Sous-requête dans le SELECT
    • utiliser LATERAL

Absence de hints

  • Certains regrettent l’absence de hints
  • C’est la politique du projet :
    • vouloir ne signifie pas avoir besoin
    • PostgreSQL est un projet libre qui a le luxe de se défaire de la pression du marché
    • cela permet d’être plus facilement et rapidement mis au courant des problèmes de l’optimiseur
  • Ne pensez pas être plus intelligent que le planificateur
  • Mais il ne peut faire qu’avec ce qu’il a

Outils d’optimisation

  • auto_explain
  • pg_hint_plan
  • plantuner
  • HypoPG

auto_explain

  • Tracer les plans des requêtes lentes automatiquement
  • Contrib officielle
  • Mise en place globale (traces) :
    • globale :
    shared_preload_libraries='auto_explain'   -- redémarrage !
    ALTER DATABASE erp SET auto_explain.log_min_duration = '3s' ;
    • session :
    LOAD 'auto_explain' ;
    SET auto_explain.log_analyze TO true;

Extension plantuner

  • Pour :
    • interdire certains index
    • forcer à zéro les statistiques d’une table vide
  • Intéressant en développement pour tester les plans
    • pas en production !

Extension pg_plan_hint

  • Pour :
    • forcer l’utilisation d’un nœud entre deux tables
    • imposer une valeur de paramètre
    • appliquer automatiquement ces hints à des requêtes

Extension HypoPG

  • Extension PostgreSQL
  • Création d’index hypothétiques pour tester leur intérêt
    • avant de les créer pour de vrai
  • Limitations : surtout B-Tree, statistiques

Conclusion

  • Planificateur très avancé
  • Ne pensez pas être plus intelligent que lui
  • Il faut bien comprendre son fonctionnement

Questions

N’hésitez pas, c’est le moment !

Quiz

Travaux pratiques

La version en ligne des solutions de ces TP est disponible sur https://dali.bo/j2_solutions.

Préambule

Optimisation d’une requête (partie 1)

Optimisation d’une requête (partie 2)

Requête avec beaucoup de tables

Corrélation entre colonnes

Travaux pratiques (solutions)