Lors de l'enseignement de formations PostgreSQL, à la fois sur des sujets de base et avancés, je découvre souvent que les participants ont très peu d'idée de la puissance des index d'expression (s'ils en sont conscients). Alors laissez-moi vous donner un bref aperçu.
Donc, disons que nous avons une table, avec une plage d'horodatages (oui, nous avons la fonction generate_series qui peut générer des dates) :
CREATE TABLE t AS SELECT d, repeat(md5(d::text), 10) AS padding FROM generate_series(timestamp '1900-01-01', timestamp '2100-01-01', interval '1 day') s(d); VACUUM ANALYZE t;
Le tableau comprend également une colonne de remplissage, pour le rendre un peu plus grand. Maintenant, faisons une simple requête de plage, en sélectionnant un seul mois parmi les ~ 200 ans inclus dans le tableau. Si vous expliquez la requête, vous verrez quelque chose comme ceci :
EXPLAIN SELECT * FROM t WHERE d BETWEEN '2001-01-01' AND '2001-02-01'; QUERY PLAN ------------------------------------------------------------------------ Seq Scan on t (cost=0.00..4416.75 rows=32 width=332) Filter: ((d >= '2001-01-01 00:00:00'::timestamp without time zone) AND (d <= '2001-02-01 00:00:00'::timestamp without time zone)) (2 rows)
et sur mon ordinateur portable, cela prend environ 20 ms. Pas mal, étant donné que cela doit parcourir toute la table avec environ 75 000 lignes.
Mais créons un index sur la colonne timestamp (tous les index ici sont du type par défaut, c'est-à-dire btree, sauf mention explicite) :
CREATE INDEX idx_t_d ON t (d);
Et maintenant, essayons de relancer la requête :
QUERY PLAN ------------------------------------------------------------------------ Index Scan using idx_t_d on t (cost=0.29..9.97 rows=34 width=332) Index Cond: ((d >= '2001-01-01 00:00:00'::timestamp without time zone) AND (d <= '2001-02-01 00:00:00'::timestamp without time zone)) (2 rows)
et cela s'exécute en 0,5 ms, donc environ 40 fois plus rapide. Mais il s'agissait bien sûr d'un simple index, créé directement sur la colonne, et non d'un index d'expression. Supposons donc que nous devions plutôt sélectionner les données de chaque 1er jour de chaque mois, en effectuant une requête comme celle-ci
SELECT * FROM t WHERE EXTRACT(day FROM d) = 1;
qui cependant ne peut pas utiliser l'index, car il doit évaluer une expression sur la colonne alors que l'index est construit sur la colonne elle-même, comme indiqué sur EXPLAIN ANALYZE :
QUERY PLAN ------------------------------------------------------------------------ Seq Scan on t (cost=0.00..4416.75 rows=365 width=332) (actual time=0.045..40.601 rows=2401 loops=1) Filter: (date_part('day'::text, d) = '1'::double precision) Rows Removed by Filter: 70649 Planning time: 0.209 ms Execution time: 43.018 ms (5 rows)
Ainsi, non seulement cela doit faire une analyse séquentielle, mais il doit également faire l'évaluation, augmentant la durée de la requête à 43 ms.
La base de données ne peut pas utiliser l'index pour plusieurs raisons. Les index (au moins les index btree) reposent sur l'interrogation de données triées, fournies par la structure arborescente, et bien que la requête de plage puisse en bénéficier, la deuxième requête (avec l'appel `extract`) ne le peut pas.
Remarque :Un autre problème est que l'ensemble d'opérateurs pris en charge par les index (c'est-à-dire qui peuvent être évalués directement sur les index) est très limité. Et la fonction « extraire » n'est pas prise en charge, de sorte que la requête ne peut pas contourner le problème de commande en utilisant une analyse d'index bitmap.
En théorie, la base de données pourrait essayer de transformer la condition en conditions de plage, mais c'est extrêmement difficile et spécifique à l'expression. Dans ce cas, nous aurions à générer un nombre infini de ces plages "par jour", car le planificateur ne connaît pas vraiment les horodatages min/max dans le tableau. Donc la base de données n'essaie même pas.
Mais alors que la base de données ne sait pas comment transformer les conditions, les développeurs le font souvent. Par exemple avec des conditions telles que
(column + 1) >= 1000
ce n'est pas difficile de le réécrire comme ça
column >= (1000 - 1)
qui fonctionne très bien avec les index.
Mais que se passe-t-il si une telle transformation n'est pas possible, comme par exemple pour l'exemple de requête
SELECT * FROM t WHERE EXTRACT(day FROM d) = 1;
Dans ce cas, le développeur devrait faire face au même problème avec un min/max inconnu pour la colonne d, et même alors, cela générerait beaucoup de plages.
Eh bien, ce billet de blog concerne les index d'expression, et jusqu'à présent, nous n'avons utilisé que des index réguliers, construits directement sur la colonne. Créons donc le premier index d'expression :
CREATE INDEX idx_t_expr ON t ((extract(day FROM d))); ANALYZE t;
qui nous donne ensuite ce plan d'explication
QUERY PLAN ------------------------------------------------------------------------ Bitmap Heap Scan on t (cost=47.35..3305.25 rows=2459 width=332) (actual time=2.400..12.539 rows=2401 loops=1) Recheck Cond: (date_part('day'::text, d) = '1'::double precision) Heap Blocks: exact=2401 -> Bitmap Index Scan on idx_t_expr (cost=0.00..46.73 rows=2459 width=0) (actual time=1.243..1.243 rows=2401 loops=1) Index Cond: (date_part('day'::text, d) = '1'::double precision) Planning time: 0.374 ms Execution time: 17.136 ms (7 rows)
Ainsi, bien que cela ne nous donne pas la même accélération de 40x que l'index du premier exemple, c'est un peu attendu car cette requête renvoie beaucoup plus de tuples (2401 contre 32). De plus, ceux-ci sont répartis dans toute la table et non aussi localisés que dans le premier exemple. C'est donc une belle accélération 2x, et dans de nombreux cas réels, vous verrez des améliorations beaucoup plus importantes.
Mais la possibilité d'utiliser des index pour des conditions avec des expressions complexes n'est pas l'information la plus intéressante ici - c'est un peu la raison pour laquelle les gens créent des index d'expression. Mais ce n'est pas le seul avantage.
Si vous regardez les deux plans d'exécution présentés ci-dessus (sans et avec l'index d'expression), vous remarquerez peut-être ceci :
QUERY PLAN ------------------------------------------------------------------------ Seq Scan on t (cost=0.00..4416.75 rows=365 width=332) (actual time=0.045..40.601 rows=2401 loops=1) ...
QUERY PLAN ------------------------------------------------------------------------ Bitmap Heap Scan on t (cost=47.35..3305.25 rows=2459 width=332) (actual time=2.400..12.539 rows=2401 loops=1) ...
À droite - la création de l'indice d'expression a considérablement amélioré les estimations. Sans l'index, nous n'avons que des statistiques (MCV + histogramme) pour les colonnes de table brutes, donc la base de données ne sait pas comment estimer l'expression
EXTRACT(day FROM d) = 1
Ainsi, il applique à la place une estimation par défaut pour les conditions d'égalité, qui est de 0,5 % de toutes les lignes - comme la table a 73050 lignes, nous nous retrouvons avec une estimation de seulement 365 lignes. Il est courant de voir des erreurs d'estimation bien pires dans les applications du monde réel.
Avec l'index, cependant, la base de données a également collecté des statistiques sur les colonnes de l'index, et dans ce cas, la colonne contient les résultats de l'expression. Et lors de la planification, l'optimiseur le remarque et produit une bien meilleure estimation.
Il s'agit d'un avantage considérable et peut aider à résoudre certains cas de plans de requête médiocres causés par des estimations inexactes. Pourtant, la plupart des gens ne connaissent pas cet outil pratique.
Et l'utilité de cet outil n'a fait qu'augmenter avec l'introduction du type de données JSONB dans la version 9.4, car c'est à peu près le seul moyen de collecter des statistiques sur le contenu des documents JSONB.
Lors de l'indexation de documents JSONB, deux stratégies d'indexation de base existent. Vous pouvez soit créer un index GIN/GiST sur l'ensemble du document, par ex. comme ça
CREATE INDEX ON t USING GIN (jsonb_column);
qui vous permet d'interroger des chemins arbitraires dans la colonne JSONB, d'utiliser l'opérateur de confinement pour faire correspondre les sous-documents, etc. C'est très bien, mais vous n'avez toujours que les statistiques de base par colonne, qui ne sont
pas très utiles car les documents sont traitées comme des valeurs scalaires (et personne ne correspond à des documents entiers ou n'utilise une plage de documents).
Les index d'expression, par exemple créés comme ceci :
CREATE INDEX ON t ((jsonb_column->'id'));
ne sera utile que pour l'expression particulière, c'est-à-dire que cet index nouvellement créé sera utile pour
SELECT * FROM t WHERE jsonb_column ->> 'id' = 123;
mais pas pour les requêtes accédant à d'autres clés JSON, comme "valeur" par exemple
SELECT * FROM t WHERE jsonb_column ->> 'value' = 'xxxx';
Cela ne veut pas dire que les index GIN/GiST sur l'ensemble du document sont inutiles, mais il faut choisir. Soit vous créez un index d'expression focalisé, utile lors de l'interrogation d'une clé particulière et avec l'avantage supplémentaire des statistiques sur l'expression. Soit vous créez un index GIN/GiST sur l'ensemble du document, capable de gérer des requêtes sur des clés arbitraires, mais sans les statistiques.
Cependant, vous pouvez avoir un gâteau et le manger aussi, dans ce cas, car vous pouvez créer les deux index en même temps, et la base de données choisira lequel d'entre eux utiliser pour les requêtes individuelles. Et vous aurez des statistiques précises, grâce aux index d'expressions.
Malheureusement, vous ne pouvez pas manger tout le gâteau, car les index d'expression et les index GIN/GiST utilisent des conditions différentes
-- expression (btree) SELECT * FROM t WHERE jsonb_column ->> 'id' = 123; -- GIN/GiST SELECT * FROM t WHERE jsonb_column @> '{"id" : 123}';
afin que le planificateur ne puisse pas les utiliser en même temps - les index d'expression pour l'estimation et GIN/GiST pour l'exécution.