Développer avec PostgreSQL

Formation DEVPG

Dalibo SCOP

24.09

29 août 2024

Sur ce document

Formation Formation DEVPG
Titre Développer avec PostgreSQL
Révision 24.09
ISBN N/A
PDF https://dali.bo/devpg_pdf
EPUB https://dali.bo/devpg_epub
HTML https://dali.bo/devpg_html
Slides https://dali.bo/devpg_slides

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 12 à 16.

PostgreSQL : historique & communauté

PostgreSQL

Préambule

  • Quelle histoire !
    • parmi les plus vieux logiciels libres
    • et les plus sophistiqués
  • Souvent cité comme exemple
    • qualité du code
    • indépendance des développeurs
    • réactivité de la communauté

Au menu

  • Origines et historique du projet
  • Versions et feuille de route
  • Projets satellites
  • Sponsors et références
  • La communauté

Un peu d’histoire…

  • La licence
  • L’origine du nom
  • Les origines du projet
  • Les principes

Licence

  • Licence PostgreSQL
  • Droit, sans coûts de licence, de :
    • utiliser, copier, modifier, distribuer (et même revendre)
  • Reconnue par l’Open Source Initiative
  • Utilisée par un grand nombre de projets de l’écosystème

PostgreSQL ?!?!

  • 1985 : Michael Stonebraker recode Ingres
  • post « ingres » postingres postgres
  • postgres PostgreSQL

Principes fondateurs

  • Sécurité des données (ACID)
  • Respect des normes (ISO SQL)
  • Portabilité
  • Fonctionnalités intéressant le plus grand nombre
  • Performances
    • si pas de péril pour les données
  • Simplicité du code
  • Documentation

Origines

  • Années 1970 : Michael Stonebraker développe Ingres à Berkeley
  • 1985 : Postgres succède à Ingres
  • 1995 : Ajout du langage SQL
  • 1996 : Libération du code : Postgres devient PostgreSQL
  • 1996 : Création du PostgreSQL Global Development Group

Apparition de la communauté internationale

  • ~ 2000: Communauté japonaise (JPUG)
  • 2004 : PostgreSQLFr
  • 2006 : SPI
  • 2007 : Communauté italienne
  • 2008 : PostgreSQL Europe et US
  • 2009 : Boom des PGDay
  • 2011 : Postgres Community Association of Canada
  • 2017 : Community Guidelines
  • …et ça continue

Progression du code

  • 1,6 millions de lignes
    • dont 1/4 de commentaires
    • le reste surtout en C
  • Nombres de commit par mois :
Évolution du nombre de commit dans le dépôt PostgreSQL

Les versions de PostgreSQL

Quelle version utiliser ?

  • Historique
  • Numérotation
  • Mises à jour mineures et majeures
  • Les versions courantes
  • Quelle version en production ?
  • Les forks & dérivés

Historique

Versions & fonctionnalités

  • 1996 : v6.0 -> première version publiée
  • 2003 : v7.4 -> première version réellement stable
  • 2005 : v8.0 -> arrivée sur Windows
  • 2008 : v8.3 -> performances et fonctionnalités, organisation (commitfests)
  • 2010 : v9.0 -> réplication physique
  • 2016 : v9.6 -> parallélisation
  • 2017 : v10 -> réplication logique, partitionnement déclaratif
  • 2023 : v16 -> performances, fonctionnalités, administration…

Numérotation

  • Version récentes (10+)
    • X : version majeure (10, 11, … 16)
    • X.Y : version mineure (14.8, 15.3)
  • Avant la version 10 (toutes périmées !)
    • X.Y : version majeure (8.4, 9.6)
    • X.Y.Z : version mineure (9.6.24)

Mises à jour mineure

De M.m à M.m+n :

  • En général chaque trimestre
  • Et sans souci
    • Release notes
    • tests
    • mise à jour des binaires
    • redémarrage

Versions courantes

  • 1 version majeure par an
    • maintenue 5 ans
  • Dernières mises à jour mineures
  • Prochaine sortie de versions mineures prévue : 14 novembre 2024

Versions 9.4 à 11

  • jsonb
  • Row Level Security
  • Index BRIN, bloom
  • Fonctions OLAP
  • Parallélisation
  • SQL/MED : accès distants
  • Réplication logique
  • Partitionnement déclaratif
  • Réduction des inconvénients de MVCC
  • JIT
  • Index couvrants

Version 12

  • Octobre 2019 - Novembre 2024
  • Amélioration du partitionnement déclaratif
  • Amélioration des performances
    • sur la gestion des index
    • sur les CTE (option MATERIALIZED)
  • Colonnes générées
  • Nouvelles vues de visualisation de la progression des commandes
  • Refonte de la configuration de la réplication

Version 13

  • Septembre 2020 - Septembre 2025
  • Améliorations :
    • partitionnement déclaratif
    • réplication logique
  • Amélioration des performances :
    • index B-tree, objet statistique, tri et agrégat
  • Amélioration de l’autovacuum et du VACUUM :
    • gestion complète des tables en insertion seule
    • traitement parallélisé des index lors d’un VACUUM
  • Amélioration des sauvegardes :
    • génération d’un fichier manifeste, outil pg_verifybackup
  • Nouvelles vues de progression de commandes :
    • pg_stat_progress_basebackup, pg_stat_progress_analyze

Version 14

  • Septembre 2021 - Novembre 2026
  • Nouvelles vues système & améliorations
    • pg_stat_progress_copy, pg_stat_wal, pg_lock.waitstart, query_id
  • Lecture asynchrone des tables distantes
  • Paramétrage par défaut adapté aux machines plus récentes
  • Améliorations diverses :
    • réplications physique et logique
    • quelques facilités de syntaxe (triggers, tableaux en PL/pgSQL)
  • Performances :
    • connexions en lecture seule plus nombreuses
    • index…

Version 15

  • Octobre 2022 - Novembre 2027
  • Nombreuses améliorations incrémentales
    • dont en réplication logique
  • Commande MERGE
  • Performances :
    • DISTINCT parallélisable
    • pg_dump & sauvegardes, recovery, partitionnement
  • Changements notables :
    • public n’est plus accessible en écriture à tous
    • sauvegarde PITR exclusive disparaît

Version 16

  • Septembre 2023 - Novembre 2028
  • Plus de tris incrémentaux (DISTINCT…)
  • Réplication logique depuis un secondaire
  • Expressions régulières dans pg_hba.conf
  • Vues systèmes améliorées : pg_stat_io
  • Compression lz4 ou zstd pour pg_dump
  • Optimisation et améliorations diverses (parallélisation…)

Petit résumé

  • Versions 7.x :
    • fondations
    • durabilité
  • Versions 8.x :
    • fonctionnalités
    • performances
  • Versions 9.x :
    • réplication physique
    • extensibilité
  • Versions 10 à 16 :
    • réplication logique
    • parallélisation
    • partitionnement
  • … et la 17 approche

Quelle version utiliser en production ?

  • 12 et inférieures
    • Danger !
    • planifier une migration urgemment !
  • 12, 13, 14, 15, 16
    • mises à jour mineures uniquement
  • 16
    • nouvelles installations
    • nouveaux développements (ou future 17 ?)
  • Tableau comparatif des versions

Versions dérivées / Forks

Entre de nombreux autres :

  • Compatibilité Oracle :
    • EnterpriseDB
  • Data warehouse :
    • Greenplum, Netezza
  • Forks :
    • Amazon RedShift, Aurora, Neon…
    • attenton : support, extensions…
  • Extensions :
    • Citus
    • timescaledb
  • Packages avec des outils & support
  • Bases compatibles

Quelques projets satellites

PostgreSQL n’est que le moteur ! Besoin d’outils pour :

  • Administration
  • Sauvegarde
  • Supervision
  • Migration
  • SIG

Administration, Développement, Modélisation

Entre autres, dédiés ou pas :

  • Administration :
    • pgAdmin4
    • temBoard
  • Développement :
    • DBeaver
  • Modélisation :
    • pgModeler

Sauvegardes

Supervision

  • Nagios/Icinga2 :
    • check_pgactivity
    • check_postgres
  • Prometheus : postgres_exporter
  • PoWA

Audit

Migration

PostGIS

Logo Postgis
  • Projet indépendant, GPL, https://postgis.net/
  • Module spatial pour PostgreSQL
    • Extension pour types géométriques/géographiques & outils
    • La référence des bases de données spatiales
    • « quelles sont les routes qui coupent le Rhône ? »
    • « quelles sont les villes adjacentes à Toulouse ? »
    • « quels sont les restaurants situés à moins de 3 km de la Nationale 12 ? »

Sponsors & Références

Sponsors principaux

  • Sociétés se consacrant à PostgreSQL :
    • Crunchy Data (USA) : Tom Lane, Stephen Frost, Joe Conway…
    • EnterpriseDB (USA) : Bruce Momjian, Robert Haas, Dave Page…
    • 2nd Quadrant (R.U.) : Simon Riggs, Peter Eisentraut…
      • racheté par EDB
    • PostgresPro (Russie) : Oleg Bartunov, Alexander Korotkov
    • Cybertec (Autriche), Dalibo (France), Redpill Linpro (Suède), Credativ (Allemagne)…
  • Sociétés vendant un fork ou une extension :
    • Citusdata (Microsoft), Pivotal (VMWare), TimescaleDB

Autres sponsors

  • Autres sociétés :
    • VMWare, Rackspace, Heroku, Conova, Red Hat, Microsoft
    • NTT (streaming replication), Fujitsu, NEC
  • Cloud
    • nombreuses

Références

  • Météo France
  • IGN
  • RATP, SNCF
  • CNAF
  • MAIF, MSA
  • Le Bon Coin
  • Air France-KLM
  • Société Générale
  • Carrefour, Leclerc, Leroy Merlin
  • Instagram, Zalando, TripAdvisor
  • Yandex
  • CNES
  • …et plein d’autres

Le Bon Coin

  • Site de petites annonces
  • 4è site le plus consulté en France (2017)
  • 27 millions d’annonces en ligne, 800 000 nouvelles chaque jour
  • Instance PostgreSQL principale : 3 To de volume, 3 To de RAM
  • 20 serveurs secondaires

À la rencontre de la communauté

  • Cartographie du projet
  • Pourquoi participer
  • Comment participer

PostgreSQL, un projet mondial

Carte des hackers

PostgreSQL Core Team

Core team

Contributeurs

Contributeurs

Qui contribue du code ?

  • Principalement des personnes payées par leur société
  • 30 committers
    • Tom Lane
    • Andres Freund
    • Peter Eisentraut
    • Nikita Glukhov
    • Álvaro Herrera
    • Michael Paquier
    • Robert Haas
    • …et beaucoup d’autres
  • Commitfests : tous les 2 mois

Répartition des développeurs

Répartition des développeurs

Utilisateurs

  • Vous !
  • Le succès d’un logiciel libre dépend de ses utilisateurs.

Pourquoi participer

  • Rapidité des corrections de bugs
  • Préparer les migrations / tester les nouvelles versions
  • Augmenter la visibilité du projet
  • Créer un réseau d’entraide

Ressources web de la communauté

Documentation officielle

Serveurs francophones

Listes de discussions / Listes d’annonces

  • pgsql-announce
  • pgsql-general
  • pgsql-admin
  • pgsql-sql
  • pgsql-performance
  • pgsql-fr-generale
  • pgsql-advocacy
  • pgsql-bugs

IRC

  • Réseau LiberaChat
  • IRC anglophone :
    • #postgresql
    • #postgresql-eu
  • IRC francophone :
    • #postgresqlfr

Wiki

L’avenir de PostgreSQL

  • PostgreSQL est devenu la base de données de référence
  • Grandes orientations :
    • réplication logique
    • meilleur parallélisme
    • gros volumes
  • Prochaine version, la 17
  • Stabilité économique
  • De plus en plus de (gros) clients
  • Le futur de PostgreSQL dépend de vous !

Conclusion

  • Un projet de grande ampleur
  • Un SGBD complet
  • Souplesse, extensibilité
  • De belles références
  • Une solution stable, ouverte, performante et éprouvée
  • Pas de dépendance envers UN éditeur

Bibliographie

  • Documentation officielle (préface)
  • Articles fondateurs de M. Stonebracker (1987)
  • Présentation du projet PostgreSQL (Guillaume Lelarge, 2008)
  • Looking back at PostgreSQL (J.M. Hellerstein, 2019)

Questions

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

Quiz

Découverte des fonctionnalités

PostgreSQL

Au menu

  • Fonctionnalités du moteur

  • Objets SQL

  • Connaître les différentes fonctionnalités et possibilités

  • Découvrir des exemples concrets

Fonctionnalités du moteur

  • Standard SQL
  • ACID : la gestion transactionnelle
  • Niveaux d’isolation
  • Journaux de transactions
  • Administration
  • Sauvegardes
  • Réplication
  • Supervision
  • Sécurité
  • Extensibilité

Respect du standard SQL

  • Excellent support du SQL ISO
  • Objets SQL
    • tables, vues, séquences, routines, triggers
  • Opérations
    • jointures, sous-requêtes, requêtes CTE, requêtes de fenêtrage, etc.

ACID

Gestion transactionnelle : la force des bases de données relationnelles :

  • Atomicité (Atomic)
  • Cohérence (Consistent)
  • Isolation (Isolated)
  • Durabilité (Durable)

MVCC

  • MultiVersion Concurrency Control
  • Le « noyau » de PostgreSQL
  • Garantit les propriétés ACID
  • Permet les accès concurrents sur la même table
    • une lecture ne bloque pas une écriture
    • une écriture ne bloque pas une lecture
    • une écriture ne bloque pas les autres écritures…
    • …sauf pour la mise à jour de la même ligne

Transactions

  • Une transaction = ensemble atomique d’opérations
  • « Tout ou rien »
  • BEGIN obligatoire pour grouper des modifications
  • COMMIT pour valider
    • y compris le DDL
  • Perte des modifications si :
    • ROLLBACK / perte de la connexion / arrêt (brutal ou non) du serveur
  • SAVEPOINT pour sauvegarde des modifications d’une transaction à un instant t
  • Pas de transactions imbriquées

Niveaux d’isolation

  • Chaque transaction (et donc session) est isolée à un certain point
    • elle ne voit pas les opérations des autres
    • elle s’exécute indépendamment des autres
  • Nous pouvons spécifier le niveau d’isolation au démarrage d’une transaction
    • BEGIN ISOLATION LEVEL xxx;
  • Niveaux d’isolation supportés
    • read commited (défaut)
    • repeatable read
    • serializable

Fiabilité : journaux de transactions

  • Write Ahead Logs (WAL)
  • Chaque donnée est écrite 2 fois sur le disque !
  • Sécurité quasiment infaillible
  • Avantages :
    • WAL : écriture séquentielle
    • un seul sync sur le WAL
    • fichiers de données : en asynchrone
    • sauvegarde PITR et de la réplication fiables

Sauvegardes

  • Sauvegarde des fichiers à froid
    • outils système
  • Import/Export logique
    • pg_dump, pg_dumpall, pg_restore
  • Sauvegarde physique à chaud
    • pg_basebackup
    • sauvegarde PITR

Réplication

  • Réplication physique
    • instance complète
    • même architecture
  • Réplication logique (PG 10+)
    • table par table / colonne par colonne avec ou sans filtre (PG 15)
    • voire opération par opération
  • Asynchrones ou synchrone
  • Asymétriques

Extensibilité

  • Extensions
    • CREATE EXTENSION monextension ;
    • nombreuses : contrib, packagées… selon provenance
    • notion de confiance (v13+)
    • dont langages de procédures stockées !
  • Système des hooks
  • Background workers

Sécurité

  • Fichier pg_hba.conf
  • Filtrage IP
  • Authentification interne (MD5, SCRAM-SHA-256)
  • Authentification externe (identd, LDAP, Kerberos…)
  • Support natif de SSL

Objets SQL

  • Instances
  • Objets globaux :
    • Bases
    • Rôles
    • Tablespaces
  • Objets locaux :
    • Schémas
    • Tables
    • Vues
    • Index
    • Routines

Organisation logique

Organisation logique d’une instance

Instances

  • Une instance
    • un répertoire de données
    • un port TCP
    • une configuration
    • plusieurs bases de données
  • Plusieurs instances possibles sur un serveur

Rôles

  • Utilisateurs / Groupes
    • Utilisateur : Permet de se connecter
  • Différents attributs et droits

Tablespaces

  • Répertoire physique contenant les fichiers de données de l’instance
  • Une base peut
    • se trouver sur un seul tablespace
    • être répartie sur plusieurs tablespaces
  • Permet de gérer l’espace disque et les performances
  • Pas de quota

Bases

  • Conteneur hermétique
  • Un rôle ne se connecte pas à une instance
    • il se connecte forcément à une base
  • Une fois connecté, il ne voit que les objets de cette base
    • contournement : foreign data wrappers, dblink

Schémas

  • Espace de noms
  • Sous-ensemble de la base
  • Non lié à un utilisateur
  • Résolution des objets : search_path
  • pg_catalog, information_schema
    • pour catalogues système (lecture seule !)

Tables

Par défaut, une table est :

  • Permanente
    • si temporaire, vivra le temps de la session (ou de la transaction)
  • Journalisée
    • si unlogged, perdue en cas de crash, pas de réplication
  • Non partitionnée
    • partitionnement possible par intervalle, valeur ou hachage

Vues

  • Masquer la complexité
    • structure : interface cohérente vers les données, même si les tables évoluent
    • sécurité : contrôler l’accès aux données de manière sélective
  • Vues matérialisées
    • à rafraîchir à une certaine fréquence

Index

  • Algorithmes supportés
    • B-tree (par défaut)
    • Hash
    • GiST / SP-GiST
    • GIN
    • BRIN
    • Bloom
  • Type
    • Mono ou multicolonne
    • Partiel
    • Fonctionnel
    • Couvrant

Types de données

  • Types de base
    • natif : int, float
    • standard SQL : numeric, char, varchar, date, time, timestamp, bool
  • Type complexe
    • tableau
    • JSON (jsonb), XML
    • vecteur (données LLM, FTS)
  • Types métier
    • réseau, géométrique, etc.
  • Types créés par les utilisateurs
    • structure SQL, C, Domaine, Enum

Contraintes

  • CHECK
    • prix > 0
  • NOT NULL
    • id_client NOT NULL
  • Unicité
    • id_client UNIQUE
  • Clés primaires
    • UNIQUE NOT NULL ==> PRIMARY KEY (id_client)
  • Clés étrangères
    • produit_id REFERENCES produits(id_produit)
  • EXCLUDE
    • EXCLUDE USING gist (room WITH =, during WITH &&)

Colonnes à valeur générée

  • Valeur calculée à l’insertion
  • DEFAULT
  • Identité
    • GENERATED { ALWAYS | BY DEFAULT } AS IDENTITY
  • Expression
    • GENERATED ALWAYS AS ( generation_expr ) STORED

Langages

  • Procédures & fonctions en différents langages
  • Par défaut : SQL, C et PL/pgSQL
  • Extensions officielles : Perl, Python
  • Mais aussi Java, Ruby, Javascript…
  • Intérêts : fonctionnalités, performances

Fonctions & procédures

  • Fonction
    • renvoie une ou plusieurs valeurs
    • SETOF ou TABLE pour plusieurs lignes
  • Procédure (v11+)
    • ne renvoie rien
    • peut gérer le transactionnel dans certains cas

Opérateurs

  • Dépend d’un ou deux types de données
  • Utilise une fonction prédéfinie :
CREATE OPERATOR //
  (FUNCTION=division0,
  LEFTARG=integer,
  RIGHTARG=integer);

Triggers

  • Opérations : INSERT, UPDATE, DELETE, TRUNCATE
  • Trigger sur :
    • une colonne, et/ou avec condition
    • une vue
    • DDL
  • Tables de transition
  • Effet sur :
    • l’ensemble de la requête (FOR STATEMENT)
    • chaque ligne impactée (FOR EACH ROW)
  • N’importe quel langage supporté

Questions

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

Quiz

Introduction aux plans d’exécution

PostgreSQL

Introduction

  • Qu’est-ce qu’un plan d’exécution ?
  • Quels outils peuvent aider

Au menu

  • Exécution globale d’une requête
  • Optimiseur
  • EXPLAIN
  • Nœuds d’un plan
  • Outils

Niveau SGBD

Traitement d’une requête SQL

Optimiseur

  • SQL est un langage déclaratif
  • Une requête décrit le résultat à obtenir
    • mais pas la façon pour l’obtenir
  • C’est à l’optimiseur de déduire le moyen de parvenir au résultat demandé : comment ?

Principe de l’optimiseur

Le modèle vise à minimiser un coût :

  • Énumérer tous les plans d’exécution
    • ou presque tous…
  • Statistiques + configuration + règles → coût calculé
  • Coût le plus bas = meilleur plan

Exemple de requête et son résultat

SELECT nom, prenom, num_service
FROM employes
WHERE nom LIKE 'B%'
ORDER BY num_service;
    nom    |  prenom  | num_service
-----------+----------+-------------
 Berlicot  | Jules    |           2
 Brisebard | Sylvie   |           3
 Barnier   | Germaine |           4

Décisions de l’optimiseur

  • Comment accéder aux lignes ?
    • parcours de table, d’index, de fonction, etc.
  • Comment joindre les tables ?
    • ordre
    • type
  • Comment agréger ?
    • brut, tri, hachage…

Mécanisme de calcul de coûts

  • Chaque opération a un coût :
    • lire un bloc selon sa position sur le disque
    • manipuler une ligne
    • appliquer un opérateur
  • et généralement un paramètre associé

Statistiques

  • Connaître le coût de traitement d’une ligne est bien
    • mais combien de lignes à traiter ?
  • Statistiques sur les données
    • mises à jour : ANALYZE
  • Sans bonnes statistiques, pas de bons plans !

Exemple - parcours d’index

CREATE TABLE t1 (c1 integer, c2 integer);
INSERT INTO t1 SELECT i, i FROM generate_series(1, 1000) i;
CREATE INDEX ON t1(c1);
ANALYZE t1;
EXPLAIN SELECT * FROM t1 WHERE c1=1 ;
                         QUERY PLAN
---------------------------------------------------------------
Index Scan using t1_c1_idx on t1 (cost=0.28..8.29 rows=1 width=8)
  Index Cond: (c1 = 1)

Exemple - parcours de table

UPDATE t1 SET c1=1 ;   /* 1000 lignes identiques */

ANALYZE t1 ;           /* ne pas oublier ! */
EXPLAIN SELECT * FROM t1 WHERE c1=1;
                    QUERY PLAN
------------------------------------------------------
 Seq Scan on t1  (cost=0.00..21.50 rows=1000 width=8)
   Filter: (c1 = 1)

Exemple - parcours d’index forcé

SET enable_seqscan TO off ;

EXPLAIN SELECT * FROM t1 WHERE c1=1;
                         QUERY PLAN
---------------------------------------------------------------
Index Scan using t1_c1_idx on t1 (cost=0.28..57.77 rows=1000 width=8)
  Index Cond: (c1 = 1)
RESET enable_seqscan ;

Qu’est-ce qu’un plan d’exécution ?

  • Représente les différentes opérations pour répondre à la requête
  • Sous forme arborescente
  • Composé des nœuds d’exécution
  • Plusieurs opérations simples mises bout à bout

Nœud d’exécution

  • Nœud
    • opération simple : lectures, jointures, tris, etc.
    • unité de traitement
    • produit et consomme des données
  • Enchaînement des opérations
    • chaque nœud produit les données consommées par le nœud parent
    • le nœud final retourne les données à l’utilisateur

Récupérer un plan d’exécution

  • Commande EXPLAIN
    • suivi de la requête complète
  • Uniquement le plan finalement retenu

Exemple de requête

EXPLAIN SELECT * FROM t1  WHERE c2<10 ORDER BY c1;

Plan pour cette requête

                       QUERY PLAN
---------------------------------------------------------
 Sort  (cost=21.64..21.67 rows=9 width=8)
   Sort Key: c1
   ->  Seq Scan on t1  (cost=0.00..21.50 rows=9 width=8)
         Filter: (c2 < 10)

Informations sur la ligne nœud

->  Seq Scan on t1  (cost=0.00..21.50 rows=9 width=8)
      Filter: (c2 < 10)
  • cost : coûts de récupération
    • de la première ligne
    • de toutes les lignes
  • rows
    • nombre de lignes en sortie du nœud
  • width
    • largeur moyenne d’un enregistrement (octets)

Informations sur les lignes suivantes

Sort  (cost=21.64..21.67 rows=9 width=8)
  Sort Key: c1
Seq Scan on t1  (cost=0.00..21.50 rows=9 width=8)
  Filter: (c2 < 10)
  • Sort
    • Sort Key : clé de tri
  • Seq Scan
    • Filter : filtre (si besoin)
  • Dépend
    • du type de nœud
    • des options de EXPLAIN
    • des paramètres de configuration
    • de la version de PostgreSQL

Option ANALYZE

EXPLAIN (ANALYZE)  /* exécution !! */
SELECT   *   FROM t1   WHERE c2<10   ORDER BY c1;
                        QUERY PLAN
---------------------------------------------------------------
 Sort  (cost=21.64..21.67 rows=9 width=8)
       (actual time=0.493..0.498 rows=9 loops=1)
   Sort Key: c1
   Sort Method: quicksort  Memory: 25kB
   ->  Seq Scan on t1  (cost=0.00..21.50 rows=9 width=8)
               (actual time=0.061..0.469 rows=9 loops=1)
         Filter: (c2 < 10)
         Rows Removed by Filter: 991
 Planning Time: 0.239 ms
 Execution Time: 0.606 ms

Option BUFFERS

EXPLAIN (ANALYZE, BUFFERS)
SELECT   *   FROM t1   WHERE c2<10   ORDER BY c1;
                        QUERY PLAN
---------------------------------------------------------
 Sort  (cost=17.64..17.67 rows=9 width=8)
       (actual time=0.126..0.127 rows=9 loops=1)
   Sort Key: c1
   Sort Method: quicksort  Memory: 25kB
   Buffers: shared hit=3 read=5
   ->  Seq Scan on t1  (cost=0.00..17.50 rows=9 width=8)
                       (actual time=0.017..0.106 rows=9 loops=1)
         Filter: (c2 < 10)
         Rows Removed by Filter: 991
         Buffers: shared read=5

Option SETTINGS

SET enable_seqscan TO off ;
SET work_mem TO '100MB';
EXPLAIN (SETTINGS)
SELECT   *   FROM t1   WHERE c2<10   ORDER BY c1;
                         QUERY PLAN
---------------------------------------------------------------
Index Scan using t1_c1_idx on t1  (cost=0.28..57.77 rows=9 width=8)
  Filter: (c2 < 10)
Settings: enable_seqscan = 'off', work_mem = '100MB'
RESET ALL ;

Option WAL

EXPLAIN (ANALYZE, WAL)
INSERT INTO t1 SELECT i, i FROM generate_series(1,1000) i ;
                      QUERY PLAN
----------------------------------------------------
 Insert on t1  (cost=0.00..10.00 rows=1000 width=8)
          (actual time=8.078..8.079 rows=0 loops=1)
   WAL: records=2017 fpi=3 bytes=162673
   ->  Function Scan on generate_series i
       (cost=0.00..10.00 rows=1000 width=8)
       (actual time=0.222..0.522 rows=1000 loops=1)
 Planning Time: 0.076 ms
 Execution Time: 8.141 ms

Option GENERIC_PLAN

Quel plan générique pour les requêtes préparées ?

EXPLAIN (GENERIC_PLAN)
SELECT * FROM t1 WHERE c1 < $1 ;
  • PostgreSQL 16

Autres options

  • COSTS OFF
    • masquer les coûts
  • TIMING OFF
    • désactiver le chronométrage & des informations vues/calculées par l’optimiseur
  • VERBOSE
    • affichage verbeux : schémas, colonnes, workers
  • SUMMARY
    • affichage du temps de planification et exécution (si applicable)
  • FORMAT
    • sortie en texte, JSON, XML, YAML

Paramètre track_io_timing

SET track_io_timing TO on;
EXPLAIN (ANALYZE, BUFFERS)
SELECT   *   FROM t1   WHERE c2<10   ORDER BY c1 ;
                            QUERY PLAN
---------------------------------------------------------
 Sort  (cost=52.14..52.21 rows=27 width=8) (actual time=1.359..1.366 rows=27 loops=1)

   Buffers: shared hit=3 read=14
   I/O Timings: read=0.388
   ->  Seq Scan on t1  (cost=0.00..51.50 rows=27 width=8) (actual time=0.086..1.233 rows=27 loops=1)
         Filter: (c2 < 10)
         Rows Removed by Filter: 2973
         Buffers: shared read=14
         I/O Timings: read=0.388
 Planning:
   Buffers: shared hit=43 read=14
   I/O Timings: read=0.469
 Planning Time: 1.387 ms
 Execution Time: 1.470 ms

Détecter les problèmes

  • Temps d’exécution de chaque opération
  • Différence entre l’estimation du nombre de lignes et la réalité
  • Boucles
    • appels, même rapides, nombreux
  • Opérations utilisant beaucoup de blocs (BUFFERS)
  • Opérations lentes de lecture/écriture (track_io_timing)

Nœuds d’exécution les plus courants (introduction)

  • Parcours
  • Jointures
  • Agrégats
  • Tri

Parcours

  • Table
    • Seq Scan, Parallel Seq Scan
  • Index
    • Index Scan, Bitmap Scan, Index Only Scan
    • et les variantes parallélisées
  • Autres
    • Function Scan, Values Scan

Jointures

  • Algorithmes
    • Nested Loop
    • Hash Join
    • Merge Join
  • Parallélisation possible
  • Pour EXISTS, IN et certaines jointures externes
    • Semi Join
    • Anti Join

Agrégats

  • Un résultat au total
    • Aggregate
  • Un résultat par regroupement
    • Hash Aggregate
    • Group Aggregate
    • Mixed Aggregate
  • Parallélisation
    • Partial Aggregate
    • Finalize Aggregate

Opérations unitaires

  • Sort
  • Incremental Sort
  • Limit
  • Unique (DISTINCT)
  • Append (UNION ALL), Except, Intersect
  • Gather (parallélisme)
  • Memoize (14+)

Outils graphiques

  • pgAdmin
  • explain.depesz.com
  • explain.dalibo.com

pgAdmin

  • Vision graphique d’un EXPLAIN
  • Une icône par nœud
  • La taille des flèches dépend de la quantité de données
  • Le détail de chaque nœud est affiché en survolant les nœuds

pgAdmin - copie d’écran

EXPLAIN par pgAdmin

explain.depesz.com

  • Site web avec affichage amélioré du EXPLAIN ANALYZE
  • Lignes colorées pour indiquer les problèmes
  • Installable en local

explain.depesz.com - exemple

explain.depesz.com

explain.dalibo.com

  • Reprise de pev d’Alex Tatiyants, par Pierre Giraud (Dalibo)
  • Page web avec affichage graphique d’un EXPLAIN [ANALYZE]
  • Repérage des nœuds longs, lourds…
  • Affichage flexible
  • explain.dalibo.com
  • Installable en local

explain.dalibo.com - exemple

EXPLAIN par pev

Conclusion

  • Un optimiseur très avancé
  • Ne vous croyez pas plus malin que lui
  • Mais il est important de savoir comment il fonctionne

Questions

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

Quiz

Travaux pratiques

Tous les TP se basent sur la configuration par défaut de PostgreSQL, sauf précision contraire.

Manipuler explain

Manipuler explain (base magasin)

Travaux pratiques (solutions)

PostgreSQL : Optimisations SQL

PostgreSQL

Introduction

L’optimisation doit porter sur :

  • Le matériel
    • serveur, distribution, kernel, stockage, réseau…
  • Le moteur de la base :
    • postgresql.conf & co
    • l’organisation des fichiers de PostgreSQL
  • L’application
    • schéma, requêtes, vues…

Axes d’optimisation

« 80% des effets sont produits par 20% des causes. » (Principe de Pareto)

  • Il est illusoire d’optimiser une application sans connaître les sources de ralentissement
  • Cibler l’optimisation :
    • trouver ces « 20% »
    • ne pas micro-optimiser ce qui n’a pas d’influence

Quelles requêtes optimiser ?

Seul un certain nombre de requêtes sont critiques

  • Identification (outil de profiling)
    • à optimiser prioritairement
  • Différencier
    • longues en temps cumulé = coûteuses en ressources serveur
    • longues et interactives = mauvais ressenti des utilisateurs

Recherche des axes d’optimisation

Quelques profilers :

SQL et requêtes

Le SQL :

  • est un langage déclaratif :
    • on décrit le résultat, pas la façon de l’obtenir
    • c’est le travail de la base de déterminer le traitement à effectuer
  • décrit un traitement ensembliste :
    • ≠ traitement procédural
    • « on effectue des opérations sur des relations pour obtenir des relations »
  • est normalisé

Opérateurs relationnels

Les opérateurs purement relationnels :

  • Projection = SELECT
    • choix des colonnes
  • Sélection = WHERE
    • choix des enregistrements
  • Jointure = FROM/JOIN
    • choix des tables
  • Bref : tout ce qui détermine sur quelles données on travaille

Opérateurs non-relationnels

Les autres opérateurs sont non-relationnels :

  • ORDER BY
  • GROUP BY/DISTINCT
  • HAVING
  • sous-requête, vue
  • fonction (classique, d’agrégat, analytique)
  • jointure externe

Données utiles

Le volume de données récupéré a un impact sur les performances.

  • N’accéder qu’aux tables nécessaires
  • N’accéder qu’aux colonnes nécessaires
    • viser Index Only Scan
    • se méfier : stockage TOAST
  • Plus le volume de données à traiter est élevé, plus les opérations seront lentes :
    • tris et Jointures
    • éventuellement stockage temporaire sur disque pour certains algorithmes

Limiter le nombre de requêtes

SQL : langage ensembliste

  • Ne pas faire de traitement unitaire par enregistrement
  • Utiliser les jointures, ne pas accéder à chaque table une par une
  • Une seule requête, parcours de curseur
  • Fréquent avec les ORM

Sous-requêtes dans un IN

Un Semi Join peut être très efficace (il ne lit pas tout)

SELECT * FROM t1
  WHERE val1  IN (  SELECT val2 …  )
  • Sinon attention s’il y a beaucoup de valeurs dans la sous-requête !
    • dédoublonner :
    SELECT * FROM t1
      WHERE val1 IN (  SELECT  DISTINCT  val2 …  )
    • surtout : réécriture avec EXISTS (si index disponible)

Sous-requêtes liées

À éviter :

SELECT a,b
  FROM t1
  WHERE val IN (  SELECT f(b)  )
  • un appel de fonction ou sous-requête par ligne !
  • est-ce voulu ?
  • transformer en clause WHERE
  • penser à la clause LATERAL

Sous-requêtes : équivalences IN/EXISTS/LEFT JOIN

Ces sous-requêtes sont strictement équivalentes (Semi-join) :

SELECT * FROM t1
WHERE fk IN ( SELECT pk FROM t2 WHERE … )

SELECT * FROM t1
WHERE EXISTS  ( SELECT 1 FROM t2 WHERE t2.pk = t1.fk AND … )

SELECT t1.*
FROM   t1   LEFT JOIN t2   ON (t1.fk=t2.pk)
WHERE
     t2.id IS NULL

(Et Anti-join pour les variantes avec NOT)

  • Attention à NOT IN : préférer NOT EXISTS

Les vues

Une vue est une requête pré-déclarée en base.

  • Équivalent relationnel d’une fonction
  • Si utilisée dans une autre requête, elle est traitée comme une sous-requête
  • et inlinée
  • Pas de problème si elle est relationnelle…

Éviter les vues non-relationnelles

  • Attention aux vues avec DISTINCT, GROUP BY etc.
    • tous les problèmes des sous-requêtes déjà vus
    • impossible de l’inliner
    • barrière d’optimisation
    • …et mauvaises performances
  • Les vues sont dangereuses en termes de performance
    • masquent la complexité
  • Penser aux vues matérialisées si la requête est lourde

Accès aux données

L’accès aux données est coûteux.

  • Quelle que soit la base
  • Dialogue entre client et serveur
    • plusieurs aller/retours potentiellement
  • Analyse d’un langage complexe
    • SQL PostgreSQL : gram.y de 19000 lignes
  • Calcul de plan :
    • langage déclaratif => converti en impératif à chaque exécution

Coût des connexions

Se connecter coûte cher :

  • Authentification, permissions
  • Latence réseau
  • Négociation SSL
  • Création de processus & contexte d’exécution
  • Acquisition de verrous

→ Maintenir les connexions côté applicatif, ou utiliser un pooler.

Penser relationnel

  • Les spécifications sont souvent procédurales, voire objet !
  • Prendre du recul
  • Réfléchir de façon ensembliste
    • on travaille sur des ensembles de données
    • penser aux CTE (WITH)

Pas de DDL applicatif

Le schéma est la modélisation des données

  • Une application n’a pas à y toucher lors de son fonctionnement normal + exception : tables temporaires
  • SQL manipule les données en flux continu :
    • chaque étape d’un plan d’exécution n’attend pas la fin de la précédente
    • donc : une table temporaire est souvent une perte de temps

Optimiser chaque accès

Les moteurs SQL sont très efficaces, et évoluent en permanence

  • Ils ont de nombreuses méthodes de tri, de jointure, choisies en fonction du contexte
  • En SQL :
    • optimisation selon volume & configuration
    • évolution avec le moteur
  • Dans l’application cliente : vous devrez le maintenir et l’améliorer
  • Faites le maximum côté SQL :
    • agrégats, fonctions analytiques, tris, numérotations, CASE, etc.
    • Commentez avec -- et /* */

Ne faire que le nécessaire

Prendre de la distance vis-à-vis des spécifications fonctionnelles (bis) :

  • Ex : mise à jour ou insertion ?
    • tenter la mise à jour, et regarder combien d’enregistrements ont été mis à jour
    • surtout pas de COUNT(*)
    • éventuellement un test de l’existence d’un seul enregistrement
    • gérer les exceptions plutôt que de vérifier préalablement que les conditions sont remplies (si l’exception est rare)
    • et se renseigner sur la syntaxe

Index

  • Objets destinés à l’optimisation des accès
  • À poser par les développeurs :
CREATE INDEX ON ma_table (nom colonne) ;

Impact des transactions

  • Verrous : relâchés à la fin de la transaction
    • COMMIT
    • ROLLBACK
  • Validation des données sur le disque au COMMIT
    • écriture synchrone : coûteux
  • Contournements :
    • tables temporaires/unlogged ?
    • parfois : synchronous_commit = off (…si perte possible)
  • → Faire des transactions qui correspondent au fonctionnel
    • pas trop nombreuses
    • courtes, pas de travail inutile une fois des verrous posés

Verrouillage et contention

  • Chaque transaction prend des verrous :
    • sur les objets (tables, index, etc.) pour empêcher au moins leur suppression ou modification de structure pendant leur travail
    • sur les enregistrements
    • libérés à la fin de la transaction : les transactions très longues peuvent donc être problématiques
  • Sous PostgreSQL, on peut quand même lire un enregistrement en cours de modification : on voit l’ancienne version (MVCC)

Deadlocks

« Verrous mortels » : comment les éviter ?

  • Théorie : prendre toujours les verrous dans le même ordre
  • Pratique, ça n’est pas toujours possible ou commode
  • Conséquence : une des transactions est tuée
    • erreurs, ralentissements

Base distribuée

Écrire sur plusieurs nœuds ?

  • Complexité (applicatif/exploitation)
    • → risque d’erreur (programmation, fausse manipulation)
    • reprise d’incident complexe
  • Essayez avec un seul serveur plus gros
    • après avoir optimisé bien sûr
    • PostgreSQL peut vous étonner

Bibliographie

  • Quelques références :
    • The Art of SQL, Stéphane Faroult
    • Refactoring SQL Applications, Stéphane Faroult
    • SQL Performance Explained, Markus Winand
    • Introduction aux bases de données, Chris Date
    • The Art of PostgreSQL, Dimitri Fontaine
    • Vidéos de Stéphane Faroult sous Youtube

Quiz

Techniques d’indexation

Un index à l’ancienne

Introduction

  • Qu’est-ce qu’un index ?
  • Comment indexer une base ?
  • Les index B-tree dans PostgreSQL

Objectifs

  • Comprendre ce qu’est un index
  • Maîtriser le processus de création d’index
  • Connaître les différents types d’index B-tree et leurs cas d’usages

Introduction aux index

  • Uniquement destinés à l’optimisation
  • À gérer d’abord par le développeur
    • Markus Winand : SQL Performance Explained

Utilité d’un index

  • Un index permet de :
    • trouver un enregistrement dans une table directement
    • récupérer une série d’enregistrements dans une table
    • voire tout récupérer dans l’index (Index Only Scan)
  • Un index facilite :
    • certains tris
    • certains agrégats
  • Obligatoires et automatique pour clés primaires & unicité
    • conseillé pour clés étrangères (FK)

Index et lectures

Un index améliore les SELECT

  • Sans index :
=# SELECT * FROM test WHERE id = 10000;
Temps : 1760,017 ms
  • Avec index :
=# CREATE INDEX idx_test_id ON test (id);

=# SELECT * FROM test WHERE id = 10000;
Temps : 27,711 ms

Index : inconvénients

  • L’index n’est pas gratuit !
  • Ralentit les écritures
    • maintenance
  • Place disque
  • Compromis à trouver

Index : contraintes pratiques à la création

  • Lourd…
-- bloque les écritures !
CREATE INDEX ON matable ( macolonne ) ;
-- ne bloque pas, peut échouer
CREATE INDEX CONCURRENTLY ON matable ( macolonne ) ;
  • Si fragmentation :
REINDEX INDEX nomindex ;
REINDEX TABLE CONCURRENTLY nomtable ;
  • Paramètres :
    • maintenance_work_mem (sinon : fichier temporaire !)
    • max_parallel_maintenance_workers

Types d’index dans PostgreSQL

  • Défaut : B-tree classique (équilibré)
  • UNIQUE (préférer la contrainte)
  • Mais aussi multicolonne, fonctionnel, partiel, couvrant
  • Index spécialisés : hash, GiST, GIN, BRIN, HNSW….

Fonctionnement d’un index

Un index à l’ancienne

Structure d’un index

  • Structure associant des clés (termes) à des localisations (pages)
  • Structure de données spécialisée, plusieurs types
  • Séparée de la table
  • Analogies :
    • fiches en carton des bibliothèques avant l’informatique (B-tree)
    • index d’un livre technique (GIN)

Un index n’est pas magique

  • Un index ne résout pas tout
  • Importance de la conception du schéma de données
  • Importance de l’écriture de requêtes SQL correctes

Index B-tree

  • Type d’index le plus courant
    • et le plus simple
  • Utilisable pour les contraintes d’unicité
  • Supporte les opérateurs : <, <=, =, >=, >
  • Supporte le tri
  • Ne peut pas indexer des colonnes de plus de 2,6 ko

Exemple de structure d’index

SELECT name FROM ma_table WHERE id = 22

Organisation d’un index B-tree

Index multicolonnes

  • Possibilité d’indexer plusieurs colonnes :
    CREATE INDEX ON ma_table (id, name) ;
  • Ordre des colonnes primordial
    • accès direct aux premières colonnes de l’index
    • pour les autres, PostgreSQL lira tout l’index ou ignorera l’index

Nœuds des index

  • Index Scan
  • Bitmap Scan
  • Index Only Scan
    • idéal pour les performances
  • et les variantes parallélisées

Méthodologie de création d’index

  • On indexe pour une requête
    • ou idéalement une collection de requêtes
  • Et pas « une table »

L’index ? Quel index ?

  • Identifier les requêtes nécessitant un index
  • Créer les index permettant de répondre à ces requêtes
  • Valider le fonctionnement, en rejouant la requête avec :
     EXPLAIN (ANALYZE, BUFFERS)

Index et clés étrangères

  • Indexation des colonnes faisant référence à une autre
  • Performances des DML
  • Performances des jointures

Index inutilisé

C’est souvent tout à fait normal

  • Utiliser l’index est-il rentable ?
  • La requête est-elle compatible ?
  • Bug de l’optimiseur : rare

Index utilisable mais non utilisé

  • L’optimiseur pense qu’il n’est pas rentable
    • sélectivité trop faible
    • meilleur chemin pour remplir d’autres critères
    • index redondant
    • Index Only Scan nécessite un VACUUM fréquent
  • Les estimations de volumétries doivent être assez bonnes !
    • statistiques récentes, précises

Index inutilisable à cause d’une fonction

  • Pas le bon type (CAST plus ou moins explicite)
EXPLAIN SELECT * FROM clients WHERE client_id = 3::numeric;
  • Utilisation de fonctions, comme :
SELECT * FROM ma_table WHERE to_char(ma_date, 'YYYY')='2014' ;

Index inutilisable à cause d’un LIKE ‘…%’

SELECT * FROM fournisseurs WHERE commentaire LIKE 'ipsum%';
  • Solution :
CREATE INDEX idx1 ON ma_table (col_varchar varchar_pattern_ops) ;

Index inutilisable car invalide

  • CREATE INDEX … CONCURRENTLY peut échouer

Indexation B-tree avancée

De nombreuses possibilités d’indexation avancée :

  • Index partiels
  • Index fonctionnels
  • Index couvrants
  • Classes d’opérateur

Index partiels

  • N’indexe qu’une partie des données :
CREATE INDEX on evenements (type) WHERE  traite IS FALSE ;
  • Ne sert que si la clause est logiquement équivalente !
    • ou partie de la clause (inégalités, IN)
  • Intérêt : index beaucoup plus petit

Index partiels : cas d’usage

  • Données chaudes et froides
  • Index dédié à une requête avec une condition fixe

Index partiels : utilisation

  • Éviter les index de type :
CREATE INDEX ON matable ( champ_filtre ) WHERE champ_filtre =
  • Préférer :
CREATE INDEX ON matable ( champ_resultat ) WHERE champ_filtre =

Index fonctionnels : principe

  • Un index sur a est inutilisable pour :
  SELECTWHERE upper(a)='DUPOND'
  • Indexer le résultat de la fonction :
   CREATE INDEX mon_idx ON ma_table (upper(a)) ;

Index fonctionnels : conditions

  • Critère identique à la fonction dans l’index
  • Fonction impérativement IMMUTABLE !
    • délicat avec les conversions de dates/heures
  • Ne pas espérer d’Index Only Scan

Index fonctionnels : maintenance

  • Ne pas oublier ANALYZE après création d’un index fonctionnel
    • les statistiques peuvent même être l’intérêt majeur (<v14)
  • La fonction ne doit jamais tomber en erreur
  • Si modification de la fonction
    • réindexation

Index couvrants : principe

  • But : obtenir un Index Only Scan
CREATE UNIQUE INDEX clients_idx1
ON clients (id_client) INCLUDE (nom_client) ;
  • Répondent à la clause WHERE
  • ET contiennent toutes les colonnes demandées par la requête :
SELECT id_client,nom_client FROM clients WHERE id_client > 100 ;
  • …si l’index n’est pas trop gros
    • à comparer à un index multicolonne

Classes d’opérateurs

  • Un index utilise des opérateurs de comparaison
  • Texte : différentes collations = différents tris… complexes
    • Index inutilisable sur :
    WHERE col_varchar LIKE 'chaine%'
  • Solution : opérateur varchar_pattern_ops :
    • force le tri caractère par caractère, sans la collation
    CREATE INDEX idx1
    ON ma_table (col_varchar varchar_pattern_ops)
  • Plus généralement :
    • nombreux autres opérateurs pour d’autres types d’index

Conclusion

  • Responsabilité de l’indexation
  • Compréhension des mécanismes
  • Différents types d’index, différentes stratégies

Quiz

Travaux pratiques

Index « simples »

Sélectivité

Index partiels

Index fonctionnels

Cas d’index non utilisés

Travaux pratiques (solutions)

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é
    • CTE : barrière avant la v12 !
  • 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/Sort 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
  • GENERIC_PLAN : plan générique (requête préparée, v16)
  • SETTINGS : paramètres configurés pour l’optimisation
  • WAL : nombre d’enregistrements et nombre d’octets écrits dans les journaux
  • COSTS : par défaut
  • TIMING : par défaut
  • VERBOSE : colonnes considérées
  • SUMMARY : temps de planification
  • FORMAT : sortie en text, 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 (v13)

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
  • 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

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)

SQL : Ce qu’il ne faut pas faire

PostgreSQL

Des mauvaises pratiques

  • Modélisation
  • Écriture de requêtes
  • Conception de l’application

Problèmes de modélisation

  • Rappels sur le modèle relationnel
  • Formes normales
  • Atomicité !

Que veut dire « relationnel » ?

  • PostgreSQL est un SGBD-R, un système de gestion de bases de données relationnel
  • Le schéma est d’une importance capitale
  • « Relationnel » n’est pas « relation entre tables »
  • Les tables SONT les relations (entre attributs)

Quelques rappels sur le modèle relationnel

  • Le but est de modéliser un ensemble de faits
  • Le modèle relationnel a été introduit à l’époque des bases de données hiérarchiques
    • pointeur : incohérence à terme
    • formalisme : relations, modélisation évitant les incohérences suite à modification
    • formes normales
  • Un modèle n’est qu’un modèle : il ne traduit pas la réalité, simplement ce qu’on souhaite en représenter
  • Identifier rapidement les problèmes les plus évidents

Formes normales

Il existe une définition mathématique précise de chacune des 7 formes normales.

  • La troisième forme normale peut toujours être atteinte
  • La forme suivante (forme normale de Boyce-Codd, ou FNBC) ne peut pas toujours être atteinte
  • La cible est donc habituellement la 3FN
  • Chris Date :
    • « Chaque attribut dépend de la clé, de TOUTE la clé, et QUE de la clé »
    • « The key, the whole key, nothing but the key »

Atomicité

  • Un attribut (colonne) doit être atomique :

    • Modifier l’attribut sans en toucher un autre
    • Donnée correcte (délicat !)
    • Recherche efficace : accédé en entier dans une clause WHERE
  • Non respect = violation de la première forme normale

Atomicité - mauvais exemple

Immatriculation Modèle Caractéristiques
NH-415-DG twingo 4 roues motrices,toit ouvrant, climatisation
EO-538-WR clio boite automatique,abs,climatisation
INSERT INTO voitures
VALUES ('AD-057-GD','clio','toit ouvrant,abs');

Atomicité - propositions

  • Champs dédiés :
    Column      |  Type   |            Description
----------------+---------+------------------------------------
immatriculation | text    | Clé primaire
modele          | text    |
couleur         | color   | Couleur vehicule (bleu,rouge,vert)
abs             | boolean | Option anti-blocage des roues
type_roue       | boolean | tole/aluminium
motricite       | boolean | 2 roues motrices / 4 roues motrices
  • Plusieurs valeurs : contrainte CHECK/enum/table de référence

  • Beaucoup de champs : clé/valeur (plusieurs formes possibles)

Contraintes absente

  • Parfois (souvent ?) ignorées pour diverses raisons :
    • faux gains de performance
    • flexibilité du modèle de données
    • compatibilité avec d’autres SGBD (MySQL/MyISAM…)
    • commodité de développement

Conséquences de l’absence de contraintes

  • Conséquences
    • problèmes d’intégrité des données
    • fonctions de vérification de cohérence des données
  • Les contraintes sont utiles à l’optimiseur :
    • déterminent l’unicité des valeurs
    • éradiquent des lectures de tables inutiles sur des LEFT JOIN
    • utilisent les contraintes CHECK pour exclure une partition

Suspension des contraintes le temps d’une transaction

  • Solution :
    • contraintes DEFERRABLE !

Stockage Entité-Clé-Valeur

  • Entité-Attribut-Valeur (ou Entité-Clé-Valeur)
  • Quel but ?
    • flexibilité du modèle de données
    • adapter sans délai ni surcoût le modèle de données
  • Conséquences :
    • création d’une table : identifiant / nom_attribut / valeur
    • requêtes abominables et coûteuses

Stockage Entité-Clé-Valeur : exemple

Comment lister tous les DBA ?

id_pers nom_attr val_attr
1 nom Prunelle
1 prenom Léon
1 telephone 0123456789
1 fonction dba

Stockage Entité-Clé-Valeur : requête associée

SELECT id, att_nom.val_attr       AS nom,
           att_prenom.val_attr    AS prenom,
           att_telephone.val_attr AS tel
FROM personnes p
JOIN personne_attributs AS att_nom
 ON (p.id=att_nom.id_pers AND att_nom.nom_attr='nom')
JOIN personne_attributs AS att_prenom
 ON (p.id=att_prenom.id_pers AND att_prenom.nom_attr='prenom')
JOIN personne_attributs AS att_telephone
 ON (p.id=att_telephone.id_pers AND att_telephone.nom_attr='telephone')
JOIN personne_attributs AS att_fonction
 ON (p.id=att_fonction.id_pers AND att_fonction.nom_attr='fonction')
WHERE att_fonction.val_attr='dba';

Stockage Entité-Clé-Valeur, hstore, JSON

  • Solutions :
    • revenir sur la conception du modèle de données
    • utiliser un type de données plus adapté : hstore, jsonb
  • On économise jointures et place disque.

Attributs multicolonnes

  • Pourquoi
    • stocker plusieurs attributs pour une même ligne
    • exemple : les différents numéros de téléphone d’une personne
  • Pratique courante
    • ex : telephone_1, telephone_2
  • Conséquences
    • et s’il faut rajouter encore une colonne ?
    • maîtrise de l’unicité des valeurs ?
    • requêtes complexes à maintenir
  • Solutions
    • créer une table dépendante
    • ou un type tableau

Nombreuses lignes de peu de colonnes

  • Énormément de lignes, peu de colonnes
    • Cas typique : séries temporelles
  • Volumétrie augmentée par les entêtes
  • Regrouper les valeurs dans un ARRAY ou un type composite
  • Partitionner

Tables aux très nombreuses colonnes

Tables à plusieurs dizaines, voire centaines de colonnes :

  • Les entités sont certainement trop grosses dans la modélisation
  • Il y a probablement dépendance entre certaines colonnes (Only the key)
  • On accède à beaucoup d’attributs inutiles (tout est stocké au même endroit)

Choix d’un type numérique

  • Pour : représenter des valeurs décimales
  • Pratique courante :
    • real ou double (float)
    • money
    • … erreurs d’arrondis !
  • Solution :
    • numeric pour les calculs précis (financiers notamment)

Colonne de type variable

Plus rarement, on rencontre aussi :

  • Une colonne de type varchar contenant
    • quelquefois un entier
    • quelquefois une date
    • un NULL
    • une chaîne autre
    • etc.
  • À éviter comme la peste !
  • Plusieurs sens = plusieurs champs

Problèmes courants d’écriture de requêtes

  • Utilisation de NULL
  • Ordre implicite des colonnes
  • Requêtes spaghetti
  • Moteur de recherche avec LIKE

NULL

  • NULL signifie habituellement :
    • Valeur non renseignée
    • Valeur inconnue
  • Absence d’information
  • Une table remplie de NULL est habituellement signe d’un problème de modélisation.
  • NOT NULL recommandé

Ordre implicite des colonnes

  • Objectif
    • s’économiser d’écrire la liste des colonnes dans une requête
  • Problèmes
    • si l’ordre des colonnes change, les résultats changent
    • résultats faux
    • données corrompues
  • Solutions
    • nommer les colonnes impliquées

Code spaghetti

Le problème est similaire à tout autre langage :

  • Code spaghetti pour le SQL
    • Écriture d’une requête à partir d’une autre requête
    • Ou évolution d’une requête au fil du temps avec des ajouts
  • Non optimisable
  • Vite ingérable
    • Ne pas la patcher !
    • Ne pas hésiter à reprendre la requête à zéro, en repensant sa sémantique
    • Souvent, un changement de spécification est un changement de sens, au niveau relationnel, de la requête

Recherche textuelle

  • Objectif
    • ajouter un moteur de recherche à l’application
  • Pratique courante
    • utiliser l’opérateur LIKE
  • Problèmes
    • requiert des index spécialisés
    • recherche uniquement le terme exact
  • Solutions
    • pg_trgm
    • Full Text Search

Conclusion

  • La base est là pour vous aider
  • Le modèle relationnel doit être compris et appliqué
  • Avant de contourner un problème, chercher s’il n’existe pas une fonctionnalité dédiée

Quiz

Travaux pratiques

Normalisation de schéma

Entité-clé-valeur

Indexation de champs tableau

Pagination et index

Clauses WHERE et pièges

Travaux pratiques (solutions)

PL/pgSQL : les bases

PostgreSQL

Préambule

  • Vous apprendrez :
    • à choisir si vous voulez écrire du PL
    • à choisir votre langage PL
    • les principes généraux des langages PL autres que PL/pgSQL
    • les bases de PL/pgSQL

Au menu

  • Présentation du PL et des principes
  • Présentations de PL/pgSQL et des autres langages PL
  • Installation d’un langage PL
  • Détails sur PL/pgSQL

Objectifs

  • Comprendre les cas d’utilisation d’une routine PL/pgSQL
  • Choisir son langage PL en connaissance de cause
  • Comprendre la différence entre PL/pgSQL et les autres langages PL
  • Écrire une routine simple en PL/pgSQL
    • et même plus complexe

Introduction

Qu’est-ce qu’un PL ?

  • PL = Procedural Language
  • 3 langages activés par défaut :
    • C
    • SQL
    • PL/pgSQL

Quels langages PL sont disponibles ?

  • Installé par défaut :
    • PL/pgSQL
  • Intégrés au projet :
    • PL/Perl
    • PL/Python
    • PL/Tcl
  • Extensions tierces :
    • PL/java, PL/R, PL/v8 (Javascript), PL/sh …
    • extensible à volonté

Langages trusted vs untrusted

  • Trusted = langage de confiance :
    • ne permet que l’accès à la base de données
    • donc pas aux systèmes de fichiers, aux sockets réseaux, etc.
    • SQL, PL/pgSQL, PL/Perl, PL/Tcl
  • Untrusted:
    • PL/Python, C…
    • PL/TclU, PL/PerlU

Les langages PL de PostgreSQL

  • Les langages PL fournissent :
    • des fonctionnalités procédurales dans un univers relationnel
    • des fonctionnalités avancées du langage PL choisi
    • des performances de traitement souvent supérieures à celles du même code côté client

Intérêts de PL/pgSQL en particulier

  • Inspiré de l’ADA, proche du Pascal
  • Ajout de structures de contrôle au langage SQL
  • Dédié au traitement des données et au SQL
  • Peut effectuer des traitements complexes
  • Hérite de tous les types, fonctions et opérateurs définis par les utilisateurs
  • Trusted
  • Facile à utiliser

Les autres langages PL ont toujours leur intérêt

  • Avantages des autres langages PL par rapport à PL/pgSQL :
    • beaucoup plus de possibilités
    • souvent plus performants pour la résolution de certains problèmes
  • Mais :
    • pas spécialisés dans le traitement de requêtes
    • types différents
    • interpréteur séparé

Routines / Procédures stockées / Fonctions

  • Procédure stockée
    • pas de retour
    • contrôle transactionnel : COMMIT / ROLLBACK
  • Fonction
    • peut renvoyer des données (même des lignes)
    • utilisable dans un SELECT
    • peut être de type TRIGGER, agrégat, fenêtrage
  • Routine
    • procédure ou fonction

Installation

Installation des binaires nécessaires

  • SQL, C et PL/pgSQL
    • compilés et installés par défaut
  • Paquets du PGDG pour la plupart des langages :
    yum|dnf install postgresql16-plperl
    apt     install postgresql-plpython3-16
  • Autres langages :
    • à compiler soi-même

Activer un langage

Activer un langage passe par la création de l’extension :

CREATE EXTENSION plperl ;     -- pour tous
-- versions untrusted
CREATE EXTENSION plperlu ;    -- pour le superutilisateur
CREATE EXTENSION plpython3u ;
  • Liste : \dL ou pg_language

Exemples de fonctions & procédures

Fonction PL/pgSQL simple

Une fonction simple en PL/pgSQL :

CREATE FUNCTION addition (entier1 integer, entier2 integer)
RETURNS integer
LANGUAGE plpgsql
IMMUTABLE
AS '
DECLARE
  resultat integer;
BEGIN
  resultat := entier1 + entier2;
  RETURN resultat;
END ' ;

Exemple de fonction SQL

Même fonction en SQL pur :

CREATE FUNCTION addition (entier1 integer, entier2 integer)
RETURNS integer
LANGUAGE sql
IMMUTABLE
AS '     SELECT entier1 + entier2 ; ' ;
  • Intérêt : inlining & planification
  • Syntaxe allégée (v14+) :
CREATE OR REPLACE FUNCTION addition (entier1 integer, entier2 integer)
RETURNS integer
LANGUAGE sql
IMMUTABLE
RETURN entier1 + entier2 ;

Exemple de fonction PL/pgSQL utilisant la base

CREATE OR REPLACE FUNCTION nb_lignes_table (sch text, tbl text)
 RETURNS bigint
 STABLE
AS '
DECLARE     n bigint ;
BEGIN
    SELECT n_live_tup
    INTO n
    FROM pg_stat_user_tables
    WHERE schemaname = sch AND relname = tbl ;
    RETURN n ;
END ; '
LANGUAGE plpgsql ;

Exemple de fonction PL/Perl complexe

  • Permet d’insérer une facture associée à un client
  • Si le client n’existe pas, une entrée est créée
  • Utilisation fréquente de spi_exec

Exemple de fonction PL/pgSQL complexe

  • Même fonction en PL/pgSQL que précédemment
  • L’accès aux données est simple et naturel
  • Les types de données SQL sont natifs
  • La capacité de traitement est limitée par le langage
  • Attention au nommage des variables et paramètres

Exemple de procédure

CREATE OR REPLACE PROCEDURE vide_tables (dry_run BOOLEAN)
AS '
BEGIN
    TRUNCATE TABLE pgbench_history ;
    TRUNCATE TABLE pgbench_accounts CASCADE ;
    TRUNCATE TABLE pgbench_tellers  CASCADE ;
    TRUNCATE TABLE pgbench_branches CASCADE ;
    IF dry_run THEN
        ROLLBACK ;
    END IF ;
END ;
 ' LANGUAGE plpgsql ;

Exemple de bloc anonyme en PL/pgSQL

  • Bloc procédural anonyme en PL/pgSQL :
DO $$
DECLARE r record;
BEGIN
    FOR r IN (SELECT schemaname, relname
              FROM pg_stat_user_tables
              WHERE coalesce(last_analyze, last_autoanalyze) IS NULL
              ) LOOP
        RAISE NOTICE 'Analyze %.%', r.schemaname, r.relname ;
        EXECUTE 'ANALYZE ' || quote_ident(r.schemaname)
                           || '.' || quote_ident(r.relname) ;
    END LOOP;
END$$;

Utiliser une fonction ou une procédure

Invocation d’une fonction ou procédure

  • Appeler une procédure : ordre spécifique CALL
CALL ma_procedure('arg1');
  • Appeler une fonction : dans une requête
SELECT ma_fonction('arg1', 'arg2') ;

SELECT * FROM ma_fonction('arg1', 'arg2') ;

INSERT INTO matable
SELECT ma_fonction( champ1, champ2 )   FROM ma_table2 ;

CALL ma_procedure( mafonction() );

CREATE INDEX ON ma_table ( ma_fonction(ma_colonne) );

Contrôle transactionnel dans les procédures

  • COMMIT et ROLLBACK : possibles dans les procédures
  • Pas de BEGIN
    • automatique après la fin d’une transaction dans le code
  • Un seul niveau de transaction
    • pas de sous-transactions
    • pas d’appel depuis une transaction
  • Incompatible avec une clause EXCEPTION

Création et maintenance des fonctions et procédures

Création

  • CREATE FUNCTION
  • CREATE PROCEDURE

Structure d’une routine PL/pgSQL

  • Reprenons le code montré plus haut :
CREATE FUNCTION addition (entier1 integer, entier2 integer)
RETURNS integer
LANGUAGE plpgsql
IMMUTABLE
AS '
DECLARE
  resultat integer;
BEGIN
  resultat := entier1 + entier2 ;
  RETURN resultat ;
END';

Structure d’une routine PL/pgSQL (suite)

  • DECLARE
    • déclaration des variables locales
  • BEGIN
    • début du code de la routine
  • END
    • la fin
  • Instructions séparées par des points-virgules
  • Commentaires commençant par -- ou compris entre /* et */

Blocs nommés

  • Labels de bloc possibles
  • Plusieurs blocs d’exception possibles dans une routine
  • Permet de préfixer des variables avec le label du bloc
  • De donner un label à une boucle itérative
  • Et de préciser de quelle boucle on veut sortir, quand plusieurs d’entre elles sont imbriquées

Modification du code d’une routine

  • CREATE OR REPLACE FUNCTION
  • CREATE OR REPLACE PROCEDURE
  • Une routine est définie par son nom et ses arguments
  • Si type de retour différent, la fonction doit d’abord être supprimée puis recréée

Modification des méta-données d’une routine

  • ALTER FUNCTION / ALTER PROCEDURE
  • Une routine est définie par son nom et ses arguments
  • Permet de modifier nom, propriétaire, schéma et autres options

Suppression d’une routine

  • Une routine est définie par son nom et ses arguments :
DROP FUNCTION addition (integer, integer) ;
DROP PROCEDURE public.vide_tables (boolean);
DROP PROCEDURE public.vide_tables ();

Utilisation des guillemets

  • Les guillemets deviennent très rapidement pénibles
    • préférer $$
    • ou $fonction$, $toto$

Paramètres et retour des fonctions et procédures

Version minimaliste

CREATE FUNCTION fonction (entier integer, texte text)
RETURNS int  AS

Paramètres IN, OUT, INOUT & retour

CREATE FUNCTION cree_utilisateur (
  nom text,                 -- IN
  type_id int DEFAULT 0     -- IN
) RETURNS id_utilisateur int  AS
CREATE FUNCTION explose_date (
  IN  d date,
  OUT jour int, OUT mois int, OUT annee int
 ) AS
  • VARIADIC : nombre variable

Type en retour : 1 valeur simple

  • Fonctions uniquement
RETURNS type     -- int, text, etc
  • Tous les types de base & utilisateur
  • Rien : void

Type en retour : 1 ligne, plusieurs champs (exemple)

Comment obtenir ceci ?

SELECT * FROM explose_date ('31-12-2020');
 jour | mois | annee
------+------+-------
   31 |    0 |  2020

Type en retour : 1 ligne, plusieurs champs

3 options :

  • Type composé dédié
CREATE TYPE ma_structure AS ( … ) ;
CREATE FUNCTION …  RETURNS ma_structure ;
  • Paramètres OUT
CREATE FUNCTION explose_date (IN d date,
                              OUT jour int, OUT mois int, OUT annee int) AS
  • RETURNS TABLE
CREATE FUNCTION explose_date_table (d date)
RETURNS TABLE  (jour integer, mois integer, annee integer) AS…

Retour multiligne

  • 1 seul champ ou plusieurs ?
RETURNS SETOF type   -- int, text, type personnalisé
RETURNS TABLE ( col1 type, col2 type … )
  • Ligne à ligne ou en bloc ?
RETURN NEXT
RETURN QUERY   SELECT
RETURN QUERY   EXECUTE
  • Le résultat est stocké puis envoyé

Gestion des valeurs NULL

Comment gérer les paramètres à NULL ?

  • STRICT :
    • 1 paramètre NULL : retourne NULL immédiatement
  • Défaut :
    • gestion par la fonction

Variables en PL/pgSQL

Clause DECLARE

  • Dans le source, partie DECLARE :
  DECLARE
    i  integer;
    j  integer := 5;
    k  integer NOT NULL DEFAULT 1;
    ch text    COLLATE "fr_FR";
  • Blocs DECLARE/BEGIN/END imbriqués possible
    • restriction de scope de variable

Constantes

  • Clause supplémentaire CONSTANT :
  DECLARE
    eur_to_frf   CONSTANT numeric := 6.55957 ;
    societe_nom  CONSTANT text    := 'Dalibo SARL';

Types de variables

  • Récupérer le type d’une autre variable avec %TYPE :
    quantite    integer ;
    total       quantite%TYPE ;
  • Récupérer le type de la colonne d’une table :
    quantite    ma_table.ma_colonne%TYPE ;

Type ROW - 1

  • Pour renvoyer plusieurs valeurs à partir d’une fonction
  • Utiliser un type composite :
    CREATE TYPE ma_structure AS (
        un_entier integer,
        une_chaine text,
        …);
    CREATE FUNCTION ma_fonction () RETURNS ma_structure …;

Type ROW - 2

  • Utiliser le type composite défini par la ligne d’une table
    CREATE FUNCTION ma_fonction () RETURNS integer
    AS $$
    DECLARE
      ligne ma_table%ROWTYPE;

    $$

Type RECORD

  • RECORD identique au type ROW
    • …sauf que son type n’est connu que lors de son affectation
  • RECORD peut changer de type au cours de l’exécution de la routine
  • Curseur et boucle sur une requête

Type RECORD : exemple

CREATE FUNCTION ma_fonction () RETURNS integer
AS $$
DECLARE
  ligne RECORD;
BEGIN
  -- récupération de la 1è ligne uniquement
  SELECT * INTO ligne FROM ma_première_table;
  -- ou : traitement ligne à ligne
  FOR ligne IN SELECT * FROM ma_deuxième_table  LOOP

  END LOOP ;
  RETURN … ;
END $$ ;

Exécution de requête dans un bloc PL/pgSQL

Requête dans un bloc PL/pgSQL

  • Toutes opérations sur la base de données
  • Et calculs, comparaisons, etc.
  • Toute expression écrite en PL/pgSQL sera passée à SELECT pour interprétation par le moteur
  • PREPARE implicite, avec cache

Affectation d’une valeur à une variable

  • Utiliser l’opérateur := :
    un_entier := 5;
  • Utiliser SELECT INTO :
    SELECT 5 INTO un_entier;

Exécution d’une requête

  • Affectation de la ligne :

    SELECT *
    INTO ma_variable_ligne  -- type ROW ou RECORD
    FROM …;
  • INTO STRICT pour garantir unicité

    • INTO seul : juste 1è ligne !
  • Plus d’un enregistrement :

    • écrire une boucle
  • Ordre statique :

    • colonnes, clause WHERE, tables figées

Exécution d’une requête sans besoin du résultat

  • PERFORM : résultat ignoré
PERFORM * FROM ma_table WHERE une_colonne>0 ;
PERFORM mafonction (argument1) ;
  • Variable FOUND
    • si une ligne est affectée par l’instruction
  • Nombre de lignes :
GET DIAGNOSTICS variable = ROW_COUNT;

SQL dynamique

EXECUTE d’une requête

EXECUTE 'chaine' [INTO [STRICT] cible] [USING (paramètres)] ;
  • Exécute la requête dans chaine
  • chaine peut être construite à partir d’autres variables
  • cible : résultat (une seule ligne)

EXECUTE & requête dynamique : injection SQL

Si nom vaut : « 'Robert' ; DROP TABLE eleves ; »

que renvoie ceci ?

EXECUTE 'SELECT * FROM eleves WHERE nom = '|| nom ;

EXECUTE & requête dynamique : 3 possibilités

EXECUTE 'UPDATE tbl SET '
    || quote_ident(nom_colonne)
    || ' = '
    || quote_literal(nouvelle_valeur)
    || ' WHERE cle = '
    || quote_literal(valeur_cle) ;
EXECUTE format('UPDATE matable SET %I = %L '
   'WHERE clef = %L', nom_colonne, nouvelle_valeur, valeur_clef);
EXECUTE format('UPDATE table SET %I = $1 '
   'WHERE clef = $2', nom_colonne) USING nouvelle_valeur, valeur_clef;

EXECUTE & requête dynamique (suite)

EXECUTE 'chaine' [INTO STRICT cible] [USING (paramètres)] ;
  • STRICT : 1 résultat
    • sinon NO_DATA_FOUND ou TOO_MANY_ROWS
  • Sans STRICT :
    • 1ère ligne ou NO_DATA_FOUND
  • Nombre de lignes :
    • GET DIAGNOSTICS integer_var = ROW_COUNT

Outils pour construire une requête dynamique

  • quote_ident ()
    • pour mettre entre guillemets un identifiant d’un objet PostgreSQL (table, colonne, etc.)
  • quote_literal ()
    • pour mettre entre guillemets une valeur (chaîne de caractères)
  • quote_nullable ()
    • pour mettre entre guillemets une valeur (chaîne de caractères), sauf NULL qui sera alors renvoyé sans les guillemets
  • || : concaténer
  • Ou fonction format(…), équivalent de sprintf en C

Structures de contrôle en PL/pgSQL

  • But du PL : les traitements procéduraux

Tests conditionnels - 2

Exemple :

IF nombre = 0 THEN
  resultat := 'zero';
ELSEIF nombre > 0 THEN
   resultat := 'positif';
ELSEIF nombre < 0 THEN
   resultat := 'négatif';
ELSE
   resultat := 'indéterminé';
END IF;

Tests conditionnels : CASE

    CASE nombre
    WHEN nombre = 0  THEN 'zéro'
    WHEN variable > 0  THEN 'positif'
    WHEN variable < 0  THEN 'négatif'
    ELSE 'indéterminé'
    END CASE

ou :

CASE current_setting ('server_version_num')::int/10000
    WHEN 8,9,10,11      THEN RAISE NOTICE 'Version non supportée !!' ;
    WHEN 12,13,14,15,16 THEN RAISE NOTICE 'Version supportée' ;
    ELSE                RAISE NOTICE 'Version inconnue (fin 2023)' ;
END CASE ;

Boucle LOOP/EXIT/CONTINUE : syntaxe

  • Boucle :
    • LOOP / END LOOP
    • label possible
  • En sortir :
    • EXIT [label] [WHEN expression_booléenne]
  • Commencer une nouvelle itération de la boucle
    • CONTINUE [label] [WHEN expression_booléenne]

Boucle LOOP/EXIT/CONTINUE : exemple

LOOP
  resultat := resultat + 1;
  EXIT WHEN resultat > 100;
  CONTINUE WHEN resultat < 50;
  resultat := resultat + 1;
END LOOP;

Boucle WHILE

    WHILE condition LOOP

    END LOOP;
  • Boucle jusqu’à ce que la condition soit fausse
  • Label possible

Boucle FOR : syntaxe

    FOR variable in [REVERSE] entier1..entier2 [BY incrément]
    LOOP

    END LOOP;
  • variable va obtenir les différentes valeurs entre entier1 et entier2
  • Label possible

Boucle FOR … IN … LOOP : parcours de résultat de requête

    FOR ligne IN ( SELECT * FROM ma_table ) LOOP

    END LOOP;
  • Pour boucler dans les lignes résultats d’une requête
  • ligne de type RECORD, ROW, ou liste de variables séparées par des virgules
  • Utilise un curseur en interne
  • Label possible

Boucle FOREACH

    FOREACH variable [SLICE n] IN ARRAY expression LOOP

    END LOOP ;
  • Pour boucler sur les éléments d’un tableau
  • variable va obtenir les différentes valeurs du tableau retourné par expression
  • SLICE permet de jouer sur le nombre de dimensions du tableau à passer à la variable
  • Label possible

Autres propriétés des fonctions

  • Sécurité
  • Optimisations
  • Parallélisation

Politique de sécurité

  • SECURITY INVOKER : défaut

  • SECURITY DEFINER

    • « sudo de la base de données »
    • potentiellement dangereux
    • ne pas laisser à public !

Optimisation des fonctions

  • Fonctions uniquement
  • À destination de l’optimiseur
  • COST cout_execution
    • coût estimé pour l’exécution de la fonction
  • ROWS nb_lignes_resultat
    • nombre estimé de lignes que la fonction renvoie

Parallélisation

  • Fonctions uniquement
  • La fonction peut-elle être exécutée en parallèle ?
    • PARALLEL UNSAFE (défaut)
    • PARALLEL RESTRICTED
    • PARALLEL SAFE

Utilisation de fonctions dans les index

  • Fonctions uniquement !
  • IMMUTABLE | STABLE | VOLATILE
  • Ce mode précise la « volatilité » de la fonction.
  • Permet de réduire le nombre d’appels
  • Index : fonctions immutables uniquement (sinon problèmes !)

Conclusion

  • Grand nombre de structure de contrôle (test, boucle, etc.)
  • Facile à utiliser et à comprendre

Pour aller plus loin

  • Documentation officielle
    • « Chapitre 40. PL/pgSQL - Langage de procédures SQL »
  • Module de formation Dalibo P2
    • variadic, routines polymorphes
    • triggers, tables de transition
    • curseurs
    • gestion des erreurs
    • sécurité
    • optimisation

Questions

FOR q IN (SELECT * FROM questions ) LOOP

  répondre (q) ;

END LOOP ;

Quiz

Travaux pratiques

L’exercice sur les index fonctionnels utilise la base magasin. La base magasin (dump de 96 Mo, pour 667 Mo sur le disque au final) peut être téléchargée et restaurée comme suit dans une nouvelle base magasin :

createdb magasin
curl -kL https://dali.bo/tp_magasin -o /tmp/magasin.dump
pg_restore -d magasin  /tmp/magasin.dump
# le message sur public préexistant est normal
rm -- /tmp/magasin.dump

Toutes les données sont dans deux schémas nommés magasin et facturation.

Hello

Division

SELECT sur des tables dans les fonctions

Multiplication

Salutations

Inversion de chaîne

Jours fériés

Index fonctionnels

Travaux pratiques (solutions)


  1. La trace se retrouve encore dans le nom de la librairie C pour les clients, la libpq.↩︎

  2. Situation où deux sessions ou plus modifient des données en tables au même moment.↩︎