Indexation & SQL Avancé

Formation PERF2

Dalibo SCOP

24.09

29 août 2024

Sur ce document

Formation Formation PERF2
Titre Indexation & SQL Avancé
Révision 24.09
ISBN N/A
PDF https://dali.bo/perf2_pdf
EPUB https://dali.bo/perf2_epub
HTML https://dali.bo/perf2_html
Slides https://dali.bo/perf2_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.

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

Installation de PostgreSQL depuis les paquets communautaires

Travaux pratiques

Index « simples »

Sélectivité

Index partiels

Index fonctionnels

Cas d’index non utilisés

Travaux pratiques (solutions)

Indexation avancée

PostgreSQL

Index Avancés

De nombreuses fonctionnalités d’indexation sont disponibles dans PostgreSQL :

  • Index B-tree
    • multicolonnes
    • fonctionnels
    • partiels
    • couvrants
  • Classes d’opérateurs
  • Indexation GIN, GiST, BRIN, hash, bloom
  • Indexation de motifs
  • Indexation multicolonne

Index B-tree (rappels)

  • Index B-tree fonctionnel, multicolonne, partiel, couvrant :
    • requête cible :
    SELECT col4 FROM ma_table
    WHERE col3<12 and f(col1)=7 and col2 LIKE 'toto%' ;
    • index dédié :
    CREATE INDEX idx_adv ON ma_table (f(col1), col2 varchar_pattern_ops)
    INCLUDE (col4) WHERE col3<12 ;
  • Rappel : un index est coûteux à maintenir !

Index GIN

Un autre type d’index

  • Définition
  • Utilisation avec des données non structurées
  • Utilisation avec des données scalaires
  • Mise à jour

GIN : définition & données non structurées

GIN : Generalized Inverted iNdex

  • Index inversé généralisé
    • les champs sont décomposés en éléments (par API)
    • l’index associe une valeur à la liste de ses adresses
  • Pour chaque entrée du tableau
    • liste d’adresses où le trouver
  • Utilisation principale : données non scalaires
    • tableaux, listes, non structurées…

GIN et les tableaux

Quels tableaux contiennent une valeur donnée ?

  • Opérateurs : @>, <@, =, &&, ?, ?|, ?&
SELECT * FROM matable WHERE a @> ARRAY[42] ;
CREATE INDEX ON tablo USING gin (a);
  • Champs concaténés : transformer en tableaux
SELECT * FROM voitures
WHERE regexp_split_to_array(caracteristiques,',')
          @> '{"toit ouvrant","climatisation"}'  ;
CREATE INDEX idx_attributs_array ON voitures
USING gin ( regexp_split_to_array(caracteristiques,',') ) ;

GIN pour les JSON et les textes

  • JSON :
    • opérateur jsonb_path_ops ou jsonb_ops
  • Indexation de trigrammes
    • extensions pg_trgm (opérateur dédié gin_trgm_ops)
  • Recherche Full Text
    • indexe les vecteurs

GIN & données scalaires

  • Données scalaires aussi possibles
    • avec l’extension btree_gin
  • GIN compresse quand les données se répètent
    • alternative à bitmap !

GIN : mise à jour

  • Création lourde
    • maintenance_work_mem élevé
  • Mise à jour lente
    • d’où l’option fastupdate
    • … à désactiver si temps de réponse instable !

Index GiST

GiST : Generalized Search Tree

  • Arbre de recherche généralisé
  • Indexation non plus des valeurs mais de la véracité de prédicats
  • Moins performants que B-tree (moins sélectifs)
  • Moins lourds que GIN

GiST : cas d’usage

Le GiST indexe à peu près n’importe quoi

  • géométries (PostGIS)
  • intervalles, adresses IP, FTS…

D’autres usages recouvrent en partie ceux de GIN.

GiST & KNN

  • KNN = K-Nearest neighbours (K-plus proches voisins)
    • c’est-à-dire :
    SELECT
    ORDER BY ma_colonne <-> une_référence LIMIT 10 ;
  • Très utile pour la recherche de mots ressemblants, géographique
    • Exemple :
    SELECT   p, p <-> point(18,36)
    FROM     mes_points
    ORDER BY p <-> point(18, 36)
    LIMIT    4 ;

GiST & Contraintes d’exclusion

Contrainte d’exclusion : une extension du concept d’unicité

  • Unicité :
    • n-uplet1 = n-uplet2 interdit dans une table
  • Contrainte d’exclusion :
    • n-uplet1 op n-uplet2 interdit dans une table

    • op est n’importe quel opérateur indexable par GiST

    • Exemple :

    CREATE TABLE circles
        ( c circle,
          EXCLUDE USING gist (c WITH &&));
    • Exemple : réservations de salles

GIN, GiST & pg_trgm

  • Indexation des recherches LIKE '%critère%'

  • Similarité basée sur des trigrammes

CREATE EXTENSION pg_trgm;
SELECT similarity('bonjour','bnojour');

 similarity
------------
   0.333333
  • Indexation (GIN ou GiST) :
CREATE INDEX test_trgm_idx ON test_trgm
  USING gist (text_data gist_trgm_ops);

Indexation multicolonne : GIN, GiST & bloom

  • Multicolonne dans n’importe quel ordre
  • GIN ou GiST
    • extensions btree_gist ou btree_gin
  • Ou bloom ?
  • Quel est le meilleur ?
    • ça dépend…

Index BRIN

BRIN : Block Range INdex

CREATE INDEX brin_demo_brin_idx ON brin_demo USING brin (age)
WITH (pages_per_range=16) ;
  • Valeurs corrélées à leur emplacement physique
  • Calcule des plages de valeur par groupe de blocs
    • index très compact
  • Pour :
    • grosses volumétries
    • corrélation entre emplacement et valeur
    • penser à CLUSTER

Index hash

  • Basés sur un hash
  • Tous types de données, quelle que soit la taille
  • Ne gèrent que =
    • donc ni <>, !=
  • Mais plus compacts
  • Ne pas utiliser avant v10 (non journalisés)

Outils

  • Pour identifier des requêtes
  • Pour identifier des prédicats et des requêtes liées
  • Pour valider un index

Identifier les requêtes

  • pgBadger
  • pg_stat_statements
  • PoWA

Identifier les prédicats et des requêtes liées

  • Extension pg_qualstats
    • avec PoWa

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

Étude des index à créer

  • PoWA peut utiliser HypoPG

Quiz

Travaux pratiques

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

Indexation de motifs avec les varchar_patterns et pg_trgm

Index GIN comme bitmap

Index GIN et critères multicolonnes

HypoPG

Travaux pratiques (solutions)

Extensions PostgreSQL pour la performance

PostgreSQL

Préambule

Ce module présente des extensions plus spécifiquement destinées à améliorer les performances.

pg_trgm

  • Indexation des recherches LIKE '%critère%'

  • Similarité basée sur des trigrammes

CREATE EXTENSION pg_trgm;
SELECT similarity('bonjour','bnojour');

 similarity
------------
   0.333333
  • Indexation (GIN ou GiST) :
CREATE INDEX test_trgm_idx ON test_trgm
  USING gist (text_data gist_trgm_ops);

pg_stat_statements

Capture en temps réel des requêtes

  • Normalisation
  • Indicateurs :
    • nombre d’exécutions,
    • nombre d’enregistrements retournés
    • temps cumulé d’exécution et d’optimisation
    • lectures/écritures en cache, demandées au système, tris
    • temps de lecture/écriture (track_io_timing)
    • écritures dans les journaux de transactions (v13)
    • temps de planning (désactivé par défaut, v13)
    • utilisation du JIT (v15)

pg_stat_statements : mise en place

shared_preload_libraries = 'pg_stat_statements'
CREATE EXTENSION pg_stat_statements ; -- dans 1 ou plusieurs bases
SELECT * FROM pg_stat_statements ;
  • Vue en mémoire partagée (volumétrie contrôlée)
  • Pas d’échantillonnage, seulement des compteurs cumulés
    • pg_stat_statements_reset() ou PoWA

pg_stat_statements : exemple 1

Requêtes les plus longues en temps cumulé :

SELECT r.rolname, d.datname, s.calls, s.total_exec_time,
       s.total_exec_time / s.calls AS avg_time, s.query
  FROM pg_stat_statements s
  JOIN pg_roles r     ON (s.userid=r.oid)
  JOIN pg_database d  ON (s.dbid = d.oid)
 ORDER BY s.total_exec_time DESC
 LIMIT 10 ;

pg_stat_statements : exemple 2

Requêtes les plus fréquemment appelées :

SELECT r.rolname, d.datname, s.calls, s.total_exec_time,
       s.total_exec_time / s.calls  AS avg_time, s.query
  FROM pg_stat_statements s
  JOIN pg_roles r     ON (s.userid=r.oid)
  JOIN pg_database d  ON (s.dbid = d.oid)
 ORDER BY s.calls DESC
 LIMIT 10;

pg_stat_statements : exemple 3

Requêtes les plus consommatrices et hit ratio :

SELECT calls, total_exec_time, rows,
       100.0*shared_blks_hit
       /nullif(shared_blks_hit+shared_blks_read, 0) AS "hit %",
       query
FROM  pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 5 ;

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;

pg_buffercache

Qu’y a-t’il dans le cache de PostgreSQL ?

Fournit une vue :

  • Pour chaque page (donc pour l’ensemble de l’instance)
    • fichier (donc objet) associé
    • OID base
    • fork (0 : table, 1 : FSM, 2 : VM)
    • numéro de bloc
    • isdirty
    • usagecount

pg_prewarm

  • Charge des blocs en cache :
-- cache de PG
SELECT pg_prewarm ('pgbench_accounts', 'buffer') ;
-- cache de Linux (asynchrone)
SELECT pg_prewarm ('pgbench_accounts', 'prefetch') ;
-- cache (tous OS)
SELECT pg_prewarm ('pgbench_accounts', 'read') ;
  • Ne pas oublier les index !
  • N’interdit pas l’éviction
  • Récupération du cache au redémarrage (v11)
    • avec un petit paramétrage

Langages procéduraux

  • 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

Avantages & inconvénients

  • PL/pgSQL plus performant pour l’accès aux données
  • Chaque langage a son point fort
  • Performances :
    • latence (pas d’allers-retours)
    • accès aux données depuis les fonctions
    • bibliothèques de chaque langage
    • index
  • Langages trusted / untrusted

hll

  • COUNT(DISTINCT) est notoirement lent
  • Principe d’HyperLogLog :
    • travail sur des hachages, avec perte
    • estimation statistique
    • non exact, mais beaucoup plus rapide
  • Exemple :
SELECT mois, hll_cardinality(hll_add_agg(hll_hash_text( id )))
FROM voyages ;
  • Type hll pour pré-agréger des données

Quiz

Travaux pratiques

Indexation de pattern avec les varchar_patterns et pg_trgm

auto_explain

pg_stat_statements

PL/Python, import de page web et compression

PL/Perl et comparaison de performances

hll

Travaux pratiques (solutions)

Partitionnement sous PostgreSQL

PostgreSQL
  • Ses principes et intérêts
  • Historique
  • Les différents types

Principe & intérêts du partitionnement

  • Faciliter la maintenance de gros volumes
    • VACUUM (FULL), réindexation, déplacements, sauvegarde logique…
  • Performances
    • parcours complet sur de plus petites tables
    • statistiques par partition plus précises
    • purge par partitions entières
    • pg_dump parallélisable
    • tablespaces différents (données froides/chaudes)
  • Attention à la maintenance sur le code

Partitionnement applicatif

  • …ou la réinvention de la roue
  • Gestion au niveau applicatif, table par table
  • Complexité pour le développeur
  • Intégrité des données ?

Méthodes de partitionnement intégrées à PostgreSQL

  • Partitionnement par héritage (historique)
  • Partitionnement déclaratif (>=v10, préférer v13+)

Partitionnement par héritage

  • Historique ou pour cas très spécifique
  • Syntaxe :
CREATE TABLE primates (debout boolean) INHERITS (mammiferes) ;
  • Table mère :
    • définie normalement, peut contenir des lignes
  • Tables filles :
    • héritent des propriétés de la table mère
    • …mais pas des contraintes, index et droits
    • colonnes supplémentaires possibles
  • Insertion applicative ou par trigger (lent !)

Partitionnement déclaratif

  • Préférer version 13+
  • Mise en place et administration simplifiées (intégrées au moteur)
  • Gestion automatique des lectures et écritures (et rapide)
  • Partitions
    • attacher/détacher une partition
    • contrainte implicite de partitionnement
    • expression possible pour la clé de partitionnement
    • sous-partitions possibles
    • partition par défaut

Partitionnement par liste

Partitionnement par liste

Partitionnement par liste : implémentation

  • Liste de valeurs par partition

    • statut, client, pays, année ou mois…
  • Clé de partitionnement forcément mono-colonne

  • Syntaxe :

CREATE TABLE t1(c1 integer, c2 text) PARTITION BY LIST (c1) ;

CREATE TABLE t1_a PARTITION OF t1 FOR VALUES IN (1, 2, 3);
CREATE TABLE t1_b PARTITION OF t1 FOR VALUES IN (4, 5);

Partitionnement par intervalle

Partitionnement par intervalle

Partitionnement par intervalle : implémentation

  • Clé de partitionnement mono- ou multicolonne
    • dates, id…
  • Bornes :
    • supérieure exclue
    • MINVALUE / MAXVALUE pour infinis
  • Syntaxe :
CREATE TABLE t2(c1 integer, c2 text) PARTITION BY RANGE (c1);

CREATE TABLE t2_1 PARTITION OF t2 FOR VALUES FROM (1) TO (100);
CREATE TABLE t2_2 PARTITION OF t2 FOR VALUES FROM (100) TO (MAXVALUE);

Partitionnement par hachage

Partitionnement par hachage

Partitionnement par hachage : principe

Pour une répartition uniforme des données :

  • Hachage de valeurs par partition

    • indiquer un modulo et un reste
  • Clé de partitionnement mono- ou multicolonnes

  • Syntaxe :

CREATE TABLE t3(c1 integer, c2 text) PARTITION BY HASH (c1);

CREATE TABLE t3_a PARTITION OF t3 FOR VALUES WITH (modulus 3,remainder 0);
CREATE TABLE t3_b PARTITION OF t3 FOR VALUES WITH (modulus 3,remainder 1);
CREATE TABLE t3_c PARTITION OF t3 FOR VALUES WITH (modulus 3,remainder 2);

Clé de partitionnement multicolonne

  • Clé sur plusieurs colonnes :
    • si partitionnement par intervalle ou hash (pas par liste)
    • et si 1er champ toujours présent
  • Syntaxe :
CREATE TABLE t1(c1 integer, c2 text, c3 date)
PARTITION BY RANGE (c1, c3) ;

CREATE TABLE t1_a PARTITION OF t1
FOR VALUES FROM (1,'2017-08-10') TO (100, '2017-08-11') ;

Sous-partitionnement

S’il y a deux chemins d’accès privilégiés :

CREATE TABLE objets (id int, statut int, annee int, t text)
 PARTITION BY LIST (statut) ;

CREATE TABLE objets_123
 PARTITION OF objets FOR VALUES IN (1, 2, 3)
 PARTITION BY LIST (annee) ;

CREATE TABLE objets_123_2023
 PARTITION OF objets_123 FOR VALUES IN (2023) ;
CREATE TABLE objets_123_2024
 PARTITION OF objets_123 FOR VALUES IN (2024) ;

CREATE TABLE objets_45
 PARTITION OF objets FOR VALUES IN (4,5) ;
  • Plus souple que le partitionnement multicolonne

Partition par défaut

  • Pour le partitionnement par liste ou par intervalle
  • Toutes les données n’allant pas dans les partitions définies iront dans la partition par défaut
CREATE TABLE t2_autres PARTITION OF t2 DEFAULT ;
  • La conserver petite

Attacher une partition

ALTER TABLE … ATTACH PARTITIONFOR VALUES … ;
  • La table doit préexister
  • Vérification du respect de la contrainte par les données existantes
    • parcours complet de la table
    • potentiellement lent !
    • …sauf si contrainte CHECK identique déjà ajoutée
  • Si la partition par défaut a des données qui iraient dans cette partition :
    • erreur à l’ajout de la nouvelle partition
    • détacher la partition par défaut
    • ajouter la nouvelle partition
    • déplacer les données de l’ancienne partition par défaut
    • ré-attacher la partition par défaut

Détacher une partition

ALTER TABLE … DETACH PARTITION
  • Simple et rapide
  • Mais nécessite un verrou exclusif
    • option CONCURRENTLY (v14+)

Supprimer une partition

DROP TABLE nom_partition ;

Fonctions de gestion et vues système

  • Sous psql : \dP
  • pg_partition_tree ('logs') : liste entière des partitions
  • pg_partition_root ('logs_2019') : racine d’une partition
  • pg_partition_ancestors ('logs_201901') : parents d’une partition

Clé primaire et clé de partitionnement

La clé primaire doit contenir toutes les colonnes de la clé de partitionnement.

  • Idem pour une contrainte unique
  • Pas un problème si on partitionne selon la clé
  • Plus gênant dans d’autres cas (date, statut…)

Indexation

  • Propagation automatique
  • Index supplémentaires par partition possibles
  • Clés étrangères entre tables partitionnées

Planification & performances

  • Mettre la clé dans la requête autant que possible
  • ou : cibler les partitions directement
  • Temps de planification
    • nombre de tables, d’index, leurs statistisques…
    • max ~ 100 partitions
  • À activer ?
    • enable_partitionwise_aggregate
    • enable_partitionwise_join

Opérations de maintenance

  • Changement de tablespace
  • autovacuum/analyze
    • sur les partitions comme sur toute table
  • VACUUM, VACUUM FULL, ANALYZE
    • sur table mère : redescendent sur les partitions
  • REINDEX
    • avant v14 : uniquement par partition
  • ANALYZE
    • prévoir aussi sur la table mère (manuellement…)

Sauvegardes

Sauvegarde physique : peu de différence

Avec pg_dump :

  • --jobs : efficace
  • --load-via-partition-root
  • exclusion de partitions (v16+) :
    • --table-and-children
    • --exclude-table-and-children
    • --exclude-table-data-and-children

Limitations du partitionnement déclaratif et versions

  • Pas de création automatique des partitions
  • Planification : 100 partitions max conseillé
  • Pas d’héritage multiple, schéma fixe
  • Partitions distantes : sans propagation d’index
  • PostgreSQL >= 13 conseillée !
    • limitations sur versions précédentes
    • contournement : travailler par partition

Tables distantes & sharding

  • Tables distantes comme partitions : sharding
  • (v14+) Interrogation simultanée asynchrone

Extensions & outils

  • Extension pg_partman
    • automatisation
  • Extensions dédiées à un domaine :
    • timescaledb
    • citus

Conclusion

Le partitionnement déclaratif a de gros avantages pour le DBA

  • …mais les développeurs doivent savoir l’utiliser
  • Préférer une version récente de PostgreSQL

Quiz

Travaux pratiques

Partitionnement

Partitionner pendant l’activité

Travaux pratiques (solutions)

Types avancés

PostgreSQL offre des types avancés :

  • UUID
  • Tableaux
  • Composés :
    • hstore
    • JSON : json, jsonb
    • XML
  • Pour les objets binaires :
    • bytea
    • Large Objects

UUID

UUID : principe

  • Ex: d67572bf-5d8c-47a7-9457-a9ddce259f05
  • Un identifiant universellement unique sur 128 bits
  • Type : uuid
  • Avec des inconvénients…

UUID : avantages

  • Faciles à générer
    • gen_random_uuid() et d’autres
  • Pouvoir désigner des entités arbitrairement
  • Pouvoir exporter des données de sources différentes
    • au contraire des séquences traditionnelles
  • Pouvoir fusionner des bases
  • Pour toutes les clés primaires ?

UUID : inconvénients

  • Lisibilité
  • Temps de génération (mineur)
  • Taille
    • 16 octets…
  • Surtout : fragmentation des index
    • mauvais pour le cache
    • sauf UUID v7 (tout récent)

UUID : utilisation sous PostgreSQL

  • uuid :
    • type simple (16 octets)
    • garantit le format
  • Génération :
    • gen_random_uuid (v4, aléatoire)
    • extension uuid-ossp (v1,3,4,5)
    • extension (pg_idkit…) ou fonction en SQL (v7)

UUID : une indexation facile

CREATE UNIQUE INDEX ON nomtable USING btree (champ_uuid) ; 

UUID : résumé

  • Si vous avez besoin des UUID, préférez la version 7.
  • Les séquences habituelles restent recommandables en interne.

Types tableaux

Tableaux : principe

  • Collection ordonnée d’un même type
  • Types integer[], text[], etc.
SELECT ARRAY [1,2,3] ;

SELECT '{1,2,3}'::integer[] ;
-- lignes vers tableau
SELECT array_agg (i) FROM generate_series (1,3) i ;

-- tableau vers lignes
SELECT unnest ( '{1,2,3}'::integer[] ) ;
CREATE TABLE demotab ( id int, liste integer[] ) ;

Tableaux : recherche de valeurs

-- Recherche de valeurs (toutes)
SELECT * FROM demotab
WHERE liste @> ARRAY[15,11] ;

-- Au moins 1 élément commun ?
SELECT * FROM demotab
WHERE liste && ARRAY[11,55] ;

-- 1 tableau exact
SELECT * FROM demotab
WHERE liste = '{11,55}'::int[] ;

Tableaux : performances

Quel intérêt par rapport à 1 valeur par ligne ?

  • Allège certains modèles
    • données non triées (ex : liste de n°s de téléphone)
    • et sans contraintes
  • Grosse économie en place (time series)
    • 24 octets par ligne
    • et parfois mécanisme TOAST
  • Mais…
    • mise à jour compliquée/lente
    • indexation moins simple

Tableaux : indexation

  • B-tree inutilisable
  • GIN plus adapté pour recherche d’une valeur
    • opérateurs && , @>
    • mais plus lourd

Types composés

Types composés : généralités

  • Un champ = plusieurs attributs
  • De loin préférable à une table Entité/Attribut/Valeur
  • Uniquement si le modèle relationnel n’est pas assez souple
  • 3 types dans PostgreSQL :
    • hstore : clé/valeur
    • json : JSON, stockage texte, validation syntaxique, fonctions d’extraction
    • jsonb : JSON, stockage binaire, accès rapide, fonctions d’extraction, de requêtage, indexation avancée

hstore

hstore : principe

Stocker des données non structurées

  • Extension
  • Stockage Clé/Valeur, uniquement texte
  • Binaire
  • Indexable
  • Plusieurs opérateurs disponibles

hstore : exemple

CREATE EXTENSION hstore ;

CREATE TABLE animaux (nom text, caract hstore);
INSERT INTO animaux VALUES ('canari','pattes=>2,vole=>oui');
INSERT INTO animaux VALUES ('loup','pattes=>4,carnivore=>oui');
INSERT INTO animaux VALUES ('carpe','eau=>douce');

CREATE INDEX idx_animaux_donnees ON animaux
                                 USING gist (caract);
SELECT *, caract->'pattes' AS nb_pattes FROM animaux
WHERE caract@>'carnivore=>oui';
 nom  |              caract               | nb_pattes
------+-----------------------------------+-----------
 loup | "pattes"=>"4", "carnivore"=>"oui" | 4

JSON

JSON & PostgreSQL

{
  "firstName": "Jean",
  "lastName": "Dupont",
  "age": 27,
  "address": {
    "streetAddress": "43 rue du Faubourg Montmartre",
    "city": "Paris",
    "postalCode": "75009"
  }
}
  • json : format texte
  • jsonb : format binaire, à préférer
  • jsonpath : SQL/JSON paths (requêtage)

Type json

  • Type texte
    • avec validation du format
  • Réservé au stockage à l’identique
  • Préférer jsonb

Type jsonb

  • JSON au format Binaire
  • Indexation GIN
  • Langage JSONPath

Validation du format JSON

SELECT  '{"auteur": "JRR Tolkien","titre":"Le Hobbit"}' IS JSON ;
-- true
SELECT  '{"auteur": "JRR Tolkien","titre":"Le Hobbit}' IS JSON ;
-- false
  • Aussi : IS JSON WITH/WITHOUT UNIQUE KEYS, IS JSON ARRAY, IS JSON OBJECT, IS JSON SCALAR
  • PostgreSQL 16+

JSON : Exemple d’utilisation

CREATE TABLE personnes (datas jsonb );
INSERT INTO personnes  (datas) VALUES ('
{
  "firstName": "Jean",
  "lastName": "Valjean",
  "address": {
    "streetAddress": "43 rue du Faubourg Montmartre",
    "city": "Paris",
    "postalCode": "75002"
  },
  "phoneNumbers": [
    { "number": "06 12 34 56 78" },
    { "type": "bureau", "number": "07 89 10 11 12"}
  ],
  "children": ["Cosette"]
} '),  ('{
  "firstName": "Georges",
  "lastName": "Durand",
  "address": {
    "streetAddress": "27 rue des Moulins",
    "city": "Châteauneuf",
    "postalCode": "45990"
  },
  "phoneNumbers": [
    { "number": "06 21 34 56 78" },
    { "type": "bureau", "number": "07 98 10 11 12"}
  ],
  "children": []
} '),  ('{
  "firstName": "Jacques",
  "lastName": "Dupont",
  "isAlive": true,
  "age": 27,
  "address": {
    "streetAddress": "43 rue du Faubourg Montmartre",
    "city": "Paris",
    "state": "",
    "postalCode": "75002"
  },
  "phoneNumbers": [
    {
      "type": "personnel",
      "number": "+33 1 23 45 67 89"
    },
    {
      "type": "bureau",
      "number": "07 00 00 01 23"
    }
  ],
  "children": [],
  "spouse": "Martine Durand"
} ');

JSON : construction

SELECT '{"nom": "Lagaffe", "prenom": "Gaston"}'::jsonb ;

SELECT jsonb_build_object ('nom', 'Lagaffe', 'prenom', 'Gaston') ;

JSON : Affichage des attributs

SELECT datas->>'firstName' AS prenom,    -- text
       datas->'address'    AS addr       -- jsonb
FROM personnes ;
SELECT  datas #>> '{address,city}',      -- text
        datas #> '{address,city}'        -- jsonb
FROM personnes ; -- text
SELECT datas['address']['city'] as villes from personnes ;  -- jsonb, v14
SELECT datas['address']->>'city' as villes from personnes ;  -- text, v14
  • Attention : pas de contrôle de l’existence de la clé !
    • et attention à la casse

Modifier un JSON

-- Concaténation (attention aux NULL)
SELECT '{"nom": "Durand"}'::jsonb ||
  '{"address" : {"city": "Paris", "postalcode": "75002"}}'::jsonb ;

-- Suppression
SELECT  '{"nom": "Durand",
         "address": {"city": "Paris", "postalcode": "75002"}}'::jsonb
        - 'nom' ;

SELECT  '{"nom": "Durand",
         "address": {"city": "Paris", "postalcode": "75002"}}'::jsonb
        #- '{address,postalcode}' ;

-- Modification
SELECT jsonb_set ('{"nom": "Durand", "address": {"city": "Paris"}}'::jsonb,
       '{address}',
      '{"ville": "Lyon" }'::jsonb) ;

JSON, tableaux et agrégation

  • Manipuler des tableaux :
    • jsonb_array_elements(), jsonb_array_elements_text()
    • jsonb_agg() (de JSON ou de scalaires)
    • jsonb_agg()_strict (sans les NULL)
    SELECT jsonb_array_elements (datas->'phoneNumbers')->>'number'
    FROM   personnes ;

Conversions jsonb / relationnel

  • Construire un ensemble de tuples depuis un objet JSON :
    • jsonb_each()
  • Avec des types préexistants :
    • jsonb_populate_record(), jsonb_populate_recordset
  • Types définis à la volée :
    • jsonb_to_record(),jsonb_to_recordset()
  • Construire un JSON depuis une requête :
    • to_jsonb (<requête>)
  • Attention aux types
    • jsonb_typeof()

JSON : performances

Inconvénients par rapport à un modèle normalisé :

  • Perte d’intégrité (types, contraintes, FK…)
  • Complexité du code
  • Pas de statistiques sur les clés JSON !
  • Pas forcément plus léger en disque
    • clés répétées
  • Lire 1 attribut = lire tout le JSON
    • voire accès table TOAST
  • Mise à jour : tout ou rien
  • Indexation délicate

Recherche dans un champ JSON (1)

Sans JSONPath :

SELECT * FROM personnes …
 WHERE datas->>'lastName' = 'Durand';  -- textes

 WHERE datas->'lastName'  = '"Durand"'::jsonb ; -- JSON
 WHERE datas @> '{"nom": "Durand"}'::jsonb ; -- contient
 WHERE datas ? 'spouse' ; -- attibut existe ?

 WHERE datas ?| '{spouse,children}'::text[] ; -- un des champs ?

 WHERE datas ?& '{spouse,children}'::text[] ; -- tous les champs ?

Mais comment indexer ?

Recherche dans un champ JSON (2) : SQL/JSON et JSONPath

  • SQL:2016 introduit SQL/JSON et le langage JSONPath
  • JSONPath :
    • langage de recherche pour JSON
    • concis, flexible, plus rapide
    • depuis PostgreSQL 12, étendu à chaque version

Recherche dans un champ JSON (3) : Exemples SQL/JSON & JSONPath

SELECTFROM
 -- recherche
 WHERE datas @? '$.lastName ? (@ == "Valjean")' ;
 WHERE datas @@ '$.lastName  == "Valjean"' ;
SELECT jsonb_path_query(datas,'$.phoneNumbers[*] ? (@.type == "bureau")')
FROM   personnes
WHERE  datas @@ '$.lastName == "Durand"' ;
-- Affichage + filtrage
SELECT datas->>'lastName',
       jsonb_path_query(datas,'$.address ? (@.city == "Paris")')
FROM   personnes ;

jsonb : indexation (1/2)

  • Index fonctionnel sur un attribut précis
    • bonus : statistiques
    CREATE INDEX idx_prs_nom ON personnes ((datas->>'lastName')) ;
    ANALYZE personnes ;
  • Colonne générée (dénormalisée) :
    • champ normal, indexable, facile à utiliser, rapide à lire
    • statistiques
    ALTER TABLE personnes
    ADD COLUMN lastname text
    GENERATED ALWAYS AS  ((datas->>'lastName')) STORED ;
  • Préférer @> et @? que des fonctions

jsonb : indexation (2/2)

  • Indexation « schemaless » grâce au GIN :
    • attention au choix de l’opérateur
    -- opérateur par défaut
    --  pour critères : ?, ?|, ?& , @> , @?, @@)
    CREATE INDEX idx_prs ON personnes USING gin(datas) ;
    -- index plus performant
    --  mais que @> , @?, @@
    CREATE INDEX idx_prs ON personnes USING gin(datas jsonb_path_ops) ;

Pour aller plus loin…

Lire la documentation

XML

XML : présentation

  • Type xml
    • stocke un document XML
    • valide sa structure
  • Quelques fonctions et opérateurs disponibles :
    • XMLPARSE, XMLSERIALIZE, query_to_xml, xmlagg
    • xpath (XPath 1.0 uniquement)

Objets binaires

Objets binaires : les types

  • Souvent une mauvaise idée…

  • 2 méthodes

    • bytea : type binaire
    • Large Objects : manipulation comme un fichier

bytea

  • Un type comme les autres
    • bytea : tableau d’octets
    • en texte : bytea_output = hex ou escape
  • Récupération intégralement en mémoire !
  • Toute modification entraîne la réécriture complète du bytea
  • Maxi 1 Go (à éviter)
    • en pratique intéressant pour quelques Mo
  • Import :
SELECT pg_read_binary_file ('/chemin/fichier');

Large Object

  • À éviter…
    • préférer bytea
  • Maxi 4 To (éviter…)
  • Objet indépendant des tables
    • OID à stocker dans les tables
    • se compresse mal
  • Suppression manuelle !
    • trigger
    • lo_unlink & vacuumlo
  • Fonction de manipulation, modification
    • lo_create, lo_import
    • lo_seek, lo_open, lo_read, lo_write

Quiz

Travaux pratiques

UUID

jsonb

Large Objects

Travaux pratiques (solutions)

Fonctionnalités avancées pour la performance

Préambule

Comme tous les SGBD, PostgreSQL fournit des fonctionnalités avancées.

Ce module présente des fonctionnalités internes au moteur généralement liées aux performances.

Au menu

  • Tables temporaires
  • Tables non journalisées
  • JIT
  • Recherche Full Text

Tables temporaires

CREATE TEMP TABLE  travail (…) ;
  • N’existent que pendant la session
  • Non journalisées
  • Ne pas en abuser !
  • Ignorées par autovacuum : ANALYZE et VACUUM manuels !
  • Paramétrage :
    • temp_buffers : cache disque pour les objets temporaires, par session, à augmenter ?

Tables non journalisées (unlogged)

  • La durabilité est parfois accessoire :
    • tables temporaires et de travail
    • caches…
  • Tables non journalisées
    • non répliquées, non restaurées
    • remises à zéro en cas de crash
  • Respecter les contraintes

Tables non journalisées  : mise en place

CREATE UNLOGGED TABLE ma_table (col1 int …) ;

Bascule d’une table en/depuis unlogged

ALTER TABLE table_normale SET UNLOGGED ;
  • réécriture
ALTER TABLE table_unlogged SET LOGGED ;
  • passage du contenu dans les WAL !

Colonnes générées

CREATE TABLE paquet (
  code       text          PRIMARY KEY, …
  livraison  timestamptz   DEFAULT now() + interval '3d',
  largeur    int,    longueur int,   profondeur int,
  volume     int
      GENERATED ALWAYS AS ( largeur * longueur * profondeur )
      STORED    CHECK (volume > 0.0)
  ) ;
  • DEFAULT : expressions très simples, modifiables
  • GENERATED
    • fonctions « immutables », ne dépendant que de la ligne
    • danger sinon (ex : pas pour dates de mises à jour)
    • difficilement modifiables
    • peuvent porter des contraintes

JIT : la compilation à la volée

  • Compilation Just In Time des requêtes
  • Utilise le compilateur LLVM
  • Vérifier que l’installation est fonctionnelle
  • Activé par défaut
    • sauf en v11 ; et absent auparavant

JIT : qu’est-ce qui est compilé ?

  • Tuple deforming
  • Évaluation d’expressions :
    • WHERE
    • agrégats, GROUP BY
  • Appels de fonctions (inlining)
  • Mais pas les jointures

JIT : algorithme « naïf »

  • jit (défaut : on)
  • jit_above_cost (défaut : 100 000)
  • jit_inline_above_cost (défaut : 500 000)
  • jit_optimize_above_cost (défaut : 500 000)
  • À comparer au coût de la requête… I/O comprises
  • Seuils arbitraires !

Quand le JIT est-il utile ?

  • Goulot d’étranglement au niveau CPU (pas I/O)
  • Requêtes complexes (calculs, agrégats, appels de fonctions…)
  • Beaucoup de lignes, filtres
  • Assez longues pour « rentabiliser » le JIT
  • Analytiques, pas ERP

Recherche Plein Texte

Full Text Search : Recherche Plein Texte

  • Recherche « à la Google » ; fonctions dédiées
  • On n’indexe plus une chaîne de caractère mais
    • les mots (« lexèmes ») qui la composent
    • on peut rechercher sur chaque lexème indépendamment
  • Les lexèmes sont soumis à des règles spécifiques à chaque langue
    • notamment termes courants
    • permettent une normalisation, des synonymes…

Full Text Search : exemple

  • Décomposition :
SELECT to_tsvector ('french',
                   'Longtemps je me suis couché de bonne heure');
               to_tsvector
-----------------------------------------
 'bon':7 'couch':5 'heur':8 'longtemp':1
  • Recherche sur 2 mots :
SELECT * FROM textes
WHERE to_tsvector('french',contenu) @@ to_tsquery('Valjean & Cosette');
  • Recherche sur une phrase : phrase_totsquery

Full Text Search : dictionnaires

  • Configurations liées à la langue
    • basées sur des dictionnaires (parfois fournis)
    • dictionnaires filtrants (unaccent)
    • synonymes
  • Extensible grâce à des sources extérieures
  • Configuration par défaut : default_text_search_config

Full Text Search : stockage & indexation

  • Stocker to_tsvector (champtexte)
    • colonne mise à jour par trigger
    • ou colonne générée (v12)
  • Indexation GIN ou GiST

Full Text Search sur du JSON

  • Vectorisation possible des JSON
SELECT info FROM commandes c
WHERE to_tsvector ('french', c.info) @@ to_tsquery('papier') ;
                            info
----------------------------------------------------------------
 {"items": {"qté": 5, "produit": "Rame papier normal A4"},
            "client": "Benoît Delaporte"}
 {"items": {"qté": 5, "produit": "Pochette Papier dessin A3"},
            "client": "Lucie Dumoulin"}

Quiz

Travaux pratiques

Tables non journalisées

Indexation Full Text

Travaux pratiques (solutions)