Techniques d’optimisation PostgreSQL non conventionnelles
(hakibenita.com)- Présentation de trois approches créatives pour améliorer les performances des requêtes PostgreSQL en sortant des méthodes classiques, au lieu d’ajouter des index ou de réécrire les requêtes
- Élimination des scans complets de table via des contraintes CHECK, optimisation de la faible cardinalité avec des index basés sur des fonctions, et mise en œuvre d’une contrainte d’unicité avec des index Hash
- 1. Utiliser le paramètre
constraint_exclusionpour éviter les scans inutiles dans les requêtes comportant des conditions erronées - 2. Utiliser des index basés sur des fonctions et des colonnes générées virtuelles (virtual generated column) pour réduire la taille des index et garantir la cohérence des requêtes
- 3. Combiner des index Hash et des contraintes d’exclusion (exclusion constraint) pour implémenter efficacement une contrainte d’unicité sur de gros champs texte, avec d’importantes économies d’espace de stockage
Éliminer les scans complets de table avec des contraintes CHECK
- Même si une contrainte CHECK n’autorise que les valeurs
'free'et'pro'dans la colonneplan, si une requête erronée est exécutée avec'Pro', PostgreSQL parcourt toute la table- Le plan d’exécution lit les 10000 lignes, alors que le résultat réel est de 0 ligne
- En définissant le paramètre
constraint_exclusionsur'on', PostgreSQL tient compte des contraintes et supprime entièrement le scan- Le temps d’exécution passe de 7,4 ms à 0,008 ms
- La valeur par défaut est
'partition', et sur des requêtes simples, la surcharge de planification peut au contraire augmenter- En revanche, dans un environnement de BI et de reporting, où les utilisateurs saisissent souvent des conditions incorrectes, le réglage sur
'on'peut être utile
- En revanche, dans un environnement de BI et de reporting, où les utilisateurs saisissent souvent des conditions incorrectes, le réglage sur
Optimiser la faible cardinalité avec des index basés sur des fonctions
- Sur la table
sale, qui contient 10 millions d’enregistrements de ventes, une requête d’agrégation du chiffre d’affaires quotidien prend 627 ms avec un scan complet - L’ajout d’un index B-Tree sur la colonne
sold_atréduit ce temps à 187 ms, mais la taille de l’index grimpe à 214 MB - En créant un index basé sur une fonction sur l’expression
date_trunc('day', sold_at), la taille tombe à 66 MB et le temps d’exécution s’améliore encore à 145 ms- La faible cardinalité permet une déduplication de l’index
- En revanche, l’expression de la requête doit correspondre exactement à la définition de l’index, ce qui impose de maintenir la cohérence des expressions
- Pour cela, on peut créer une VIEW contenant la même expression, ou
- ajouter une colonne générée virtuelle (virtual generated column), prise en charge à partir de PostgreSQL 18, afin d’automatiser cette cohérence
- Avec une colonne générée virtuelle, l’index est utilisé automatiquement, ce qui permet d’obtenir à la fois un petit index, des requêtes plus rapides et une cohérence des expressions
- Toutefois, dans PostgreSQL 18, la création d’index sur des colonnes virtuelles n’est pas encore prise en charge, un support étant prévu dans la version 19
Implémenter une contrainte d’unicité avec des index Hash
- Dans une table
urlsstockant de longues URL, la création d’un index unique basé sur B-Tree pour empêcher les doublons d’URL fait monter la taille de l’index à 154 MB - Un index Hash ne stocke pas les valeurs réelles mais uniquement leur hachage, ce qui le rend beaucoup plus compact
- PostgreSQL ne prend pas en charge par défaut les index Hash uniques, mais
- il est possible de contourner cette limite pour implémenter une contrainte d’unicité à l’aide d’une contrainte d’exclusion (exclusion constraint) sous la forme
EXCLUDE USING HASH (url WITH =)
- Avec cette méthode, une insertion en double provoque également une erreur, et les performances des requêtes sont supérieures à celles du B-Tree (0.022 ms vs 0.046 ms)
- La taille de l’index est de 32 MB, soit plus de 5 fois plus petit qu’un B-Tree
- Inconvénients :
- impossible de référencer la colonne avec une clé étrangère (contrainte
REFERENCESimpossible) - compatibilité limitée avec la syntaxe
INSERT ... ON CONFLICT - remplacement possible par
ON CONFLICT ON CONSTRAINTou la syntaxeMERGE
- impossible de référencer la colonne avec une clé étrangère (contrainte
- Les index Hash sont adaptés à la garantie d’unicité sur de gros champs texte et constituent une alternative efficace en espace lorsqu’aucune clé étrangère n’est nécessaire
1 commentaires
Commentaires sur Hacker News
L’index fait 214 Mo, soit environ la moitié de la taille de la table entière
C’est bien du point de vue de l’analyste, mais côté performances d’écriture cela crée un problème de write amplification
La conception des index change selon le ratio lecture/écriture, et c’est pour cela qu’on met en place un data warehouse ou des read replicas
Si l’on doit gérer énormément d’utilisateurs, mieux vaut éviter de mettre des index BI/OLAP sur une base OLTP
Si les schémas d’accès à la table sont réguliers, la table elle-même peut devenir l’index et offrir ce gain d’efficacité sans write amplification
Pour le premier exemple, je pense qu’il vaudrait mieux définir
Plancomme un type enumC’est plus léger que du texte, et en cas de filtre invalide on obtient une erreur au lieu d’un résultat vide, ce qui est plus sûr
Excellent article. J’utilise PostgreSQL et MySQL depuis des décennies, et même après cette lecture j’ai eu l’impression de ne connaître qu’une fraction de ce qui est possible
Le plus intéressant pour moi, c’était la syntaxe
MERGEmentionnée à la fin de l’articleD’ordinaire je gère les upserts avec
INSERT ... ON CONFLICT DO UPDATE, maisMERGEsemble plus puissant et utilisable dans davantage de situationsMERGEexiste depuis longtemps dans le standard SQL, mais Postgres a repoussé son adoption à cause de problèmes de non-atomicité dans le modèle MVCCC’est aussi expliqué dans cet article du blog pganalyze
Personnellement, je préfère
INSERT ... ON CONFLICTet je n’utiliseMERGEque lorsque c’est vraiment nécessaire, avec une gestion des erreurs très prudenteINSERT ... ON CONFLICTest plus prévisibleVoir cet article comparatif sur modern-sql.com
COPY INTOau format binaire est le plus rapide. L’overhead côté serveur est quasi nulLes index BRIN, qui ne sont pas abordés dans l’article, m’ont semblé intéressants
Si les données sont monotones croissantes, c’est un index très petit et très rapide, idéal dans ce cas
Par exemple, pour des données de timestamp reçues par un serveur, où l’ordre est parfois légèrement perturbé
Avec UUIDv7, il peut être nécessaire d’ajuster
pages_per_rangeJ’ai toujours trouvé dommage qu’on ne puisse pas appliquer de contrainte d’unicité sur un index hash
On dirait qu’un simple morceau de glue code pour le transformer en exclusion constraint suffirait, alors je me demande pourquoi cela n’existe toujours pas
La vérification d’unicité fondée sur un hash n’est pas prise en charge par l’index parce qu’elle ne gère pas les collisions
La solution proposée rencontre le même problème
Postgres considère qu’il y a doublon uniquement si le hash et la valeur réelle correspondent tous les deux
Le contenu de l’article était rafraîchissant. Les colonnes virtuelles et les index hash sont intéressants, mais j’ai encore l’impression qu’ils ne sont pas totalement intégrés à l’écosystème
Les index hash ont longtemps eu beaucoup de limitations, mais ils s’améliorent peu à peu, et la contrainte d’unicité automatique reste le chantier principal
Je me suis demandé s’il n’était pas possible de créer directement un index en utilisant une stored generated column
Elle est prise en charge depuis PostgreSQL 14, mais comme le résultat est physiquement stocké et occupe donc de l’espace supplémentaire, on préfère l’éviter
Depuis mon passage au cloud, je manipule moins souvent pgsql directement dans un environnement à serveurs fixes
Je me demande si la mise en évidence de la syntaxe SQL montrée dans l’article est une fonctionnalité intégrée ou un outil séparé
En revanche, c’est gênant que lors de la copie de longues requêtes, des espaces soient automatiquement ajoutés après les retours à la ligne