PostgreSQL
 sql >> Base de données >  >> RDS >> PostgreSQL

Tirer le meilleur parti de vos index PostgreSQL

Dans le monde Postgres, les index sont essentiels pour naviguer efficacement dans le stockage des données de table (alias le « tas »). Postgres ne maintient pas de clustering pour le tas, et l'architecture MVCC conduit à plusieurs versions du même tuple autour. Créer et maintenir des index efficaces et efficients pour prendre en charge les applications est une compétence essentielle.

Lisez la suite pour découvrir quelques conseils sur l'optimisation et l'amélioration de l'utilisation des index dans votre déploiement.

Remarque :Les requêtes présentées ci-dessous sont exécutées sur un exemple de base de données pagila non modifié.

Utiliser les index de couverture

Envisagez une requête pour récupérer les e-mails de tous les clients inactifs. Le client la table a un actif colonne, et la requête est simple :

pagila=# EXPLAIN SELECT email FROM customer WHERE active=0;
                        QUERY PLAN
-----------------------------------------------------------
 Seq Scan on customer  (cost=0.00..16.49 rows=15 width=32)
   Filter: (active = 0)
(2 rows)

La requête appelle une analyse séquentielle complète de la table des clients. Créons un index sur la colonne active :

pagila=# CREATE INDEX idx_cust1 ON customer(active);
CREATE INDEX
pagila=# EXPLAIN SELECT email FROM customer WHERE active=0;
                                 QUERY PLAN
-----------------------------------------------------------------------------
 Index Scan using idx_cust1 on customer  (cost=0.28..12.29 rows=15 width=32)
   Index Cond: (active = 0)
(2 rows)

Cela aide, et le scan séquentiel est devenu un « scan d'index ». Cela signifie que Postgres analysera l'index "idx_cust1", puis recherchera davantage le tas de la table pour lire les autres valeurs de colonne (dans ce cas, le email colonne) dont la requête a besoin.

PostgreSQL 11 a introduit les index couvrants. Cette fonctionnalité vous permet d'inclure une ou plusieurs colonnes supplémentaires dans l'index lui-même, c'est-à-dire que les valeurs de ces colonnes supplémentaires sont stockées dans le stockage des données d'index.

Si nous devions utiliser cette fonctionnalité et inclure la valeur de l'e-mail dans l'index, Postgres n'aurait pas besoin d'examiner le tas de la table pour obtenir la valeur de email . Voyons si cela fonctionne :

pagila=# CREATE INDEX idx_cust2 ON customer(active) INCLUDE (email);
CREATE INDEX
pagila=# EXPLAIN SELECT email FROM customer WHERE active=0;
                                    QUERY PLAN
----------------------------------------------------------------------------------
 Index Only Scan using idx_cust2 on customer  (cost=0.28..12.29 rows=15 width=32)
   Index Cond: (active = 0)
(2 rows)

Le "Index Only Scan" nous indique que la requête est maintenant complètement satisfaite par l'index lui-même, évitant ainsi potentiellement toutes les E/S disque pour lire le tas de la table.

Les index de couverture ne sont disponibles que pour les index B-Tree à partir de maintenant. De plus, le coût de maintien d'un indice de couverture est naturellement plus élevé qu'un indice régulier.

Utiliser des index partiels

Les index partiels indexent uniquement un sous-ensemble des lignes d'une table. Cela permet de réduire la taille des index et de les parcourir plus rapidement.

Supposons que nous ayons besoin d'obtenir la liste des e-mails des clients situés en Californie. La requête est :

SELECT c.email FROM customer c
JOIN address a ON c.address_id = a.address_id
WHERE a.district = 'California';

qui a un plan de requête qui implique l'analyse des deux tables jointes :

pagila=# EXPLAIN SELECT c.email FROM customer c
pagila-# JOIN address a ON c.address_id = a.address_id
pagila-# WHERE a.district = 'California';
                              QUERY PLAN
----------------------------------------------------------------------
 Hash Join  (cost=15.65..32.22 rows=9 width=32)
   Hash Cond: (c.address_id = a.address_id)
   ->  Seq Scan on customer c  (cost=0.00..14.99 rows=599 width=34)
   ->  Hash  (cost=15.54..15.54 rows=9 width=4)
         ->  Seq Scan on address a  (cost=0.00..15.54 rows=9 width=4)
               Filter: (district = 'California'::text)
(6 rows)

Voyons ce que nous apporte un index régulier :

pagila=# CREATE INDEX idx_address1 ON address(district);
CREATE INDEX
pagila=# EXPLAIN SELECT c.email FROM customer c
pagila-# JOIN address a ON c.address_id = a.address_id
pagila-# WHERE a.district = 'California';
                                      QUERY PLAN
---------------------------------------------------------------------------------------
 Hash Join  (cost=12.98..29.55 rows=9 width=32)
   Hash Cond: (c.address_id = a.address_id)
   ->  Seq Scan on customer c  (cost=0.00..14.99 rows=599 width=34)
   ->  Hash  (cost=12.87..12.87 rows=9 width=4)
         ->  Bitmap Heap Scan on address a  (cost=4.34..12.87 rows=9 width=4)
               Recheck Cond: (district = 'California'::text)
               ->  Bitmap Index Scan on idx_address1  (cost=0.00..4.34 rows=9 width=0)
                     Index Cond: (district = 'California'::text)
(8 rows)

Le scan de adresse a été remplacé par une analyse d'index sur idx_address1 , et une analyse du tas de l'adresse.

En supposant qu'il s'agit d'une requête fréquente et qu'elle doit être optimisée, nous pouvons utiliser un index partiel qui n'indexe que les lignes d'adresse où le district est « Californie » :

pagila=# CREATE INDEX idx_address2 ON address(address_id) WHERE district='California';
CREATE INDEX
pagila=# EXPLAIN SELECT c.email FROM customer c
pagila-# JOIN address a ON c.address_id = a.address_id
pagila-# WHERE a.district = 'California';
                                           QUERY PLAN
------------------------------------------------------------------------------------------------
 Hash Join  (cost=12.38..28.96 rows=9 width=32)
   Hash Cond: (c.address_id = a.address_id)
   ->  Seq Scan on customer c  (cost=0.00..14.99 rows=599 width=34)
   ->  Hash  (cost=12.27..12.27 rows=9 width=4)
         ->  Index Only Scan using idx_address2 on address a  (cost=0.14..12.27 rows=9 width=4)
(5 rows)

La requête lit désormais uniquement l'index idx_address2 et ne touche pas la tableadresse .

Utiliser des index multi-valeurs

Certaines colonnes nécessitant une indexation peuvent ne pas avoir de type de données scalaire. Types de colonnecomme jsonb , tableaux et tsvector ont des valeurs composées ou multiples. Si vous avez besoin d'indexer de telles colonnes, vous devez généralement effectuer une recherche dans les valeurs individuelles de ces colonnes également.

Essayons de trouver tous les titres de films qui incluent des extraits des coulisses. Lefilm la table a une colonne de tableau de texte appelée special_features , qui inclut l'élément de tableau de texte Behind The Scenes si un film a cette fonctionnalité. Pour trouver tous ces films, nous devons sélectionner toutes les lignes qui ont "Behind The Scenes" dansany des valeurs du tableau special_features :

SELECT title FROM film WHERE special_features @> '{"Behind The Scenes"}';

L'opérateur de confinement @> vérifie si le côté gauche est un sur-ensemble du côté droit.

Voici le plan de requête :

pagila=# EXPLAIN SELECT title FROM film
pagila-# WHERE special_features @> '{"Behind The Scenes"}';
                           QUERY PLAN
-----------------------------------------------------------------
 Seq Scan on film  (cost=0.00..67.50 rows=5 width=15)
   Filter: (special_features @> '{"Behind The Scenes"}'::text[])
(2 rows)

qui nécessite une analyse complète du tas, au coût de 67.

Voyons si un index B-Tree régulier aide :

pagila=# CREATE INDEX idx_film1 ON film(special_features);
CREATE INDEX
pagila=# EXPLAIN SELECT title FROM film
pagila-# WHERE special_features @> '{"Behind The Scenes"}';
                           QUERY PLAN
-----------------------------------------------------------------
 Seq Scan on film  (cost=0.00..67.50 rows=5 width=15)
   Filter: (special_features @> '{"Behind The Scenes"}'::text[])
(2 rows)

L'indice n'est même pas pris en compte. L'index B-Tree n'a aucune idée qu'il y a des éléments individuels dans la valeur qu'il indexe.

Ce qu'il nous faut, c'est un index GIN.

pagila=# CREATE INDEX idx_film2 ON film USING GIN(special_features);
CREATE INDEX
pagila=# EXPLAIN SELECT title FROM film
pagila-# WHERE special_features @> '{"Behind The Scenes"}';
                                QUERY PLAN
---------------------------------------------------------------------------
 Bitmap Heap Scan on film  (cost=8.04..23.58 rows=5 width=15)
   Recheck Cond: (special_features @> '{"Behind The Scenes"}'::text[])
   ->  Bitmap Index Scan on idx_film2  (cost=0.00..8.04 rows=5 width=0)
         Index Cond: (special_features @> '{"Behind The Scenes"}'::text[])
(4 rows)

L'index GIN est capable de prendre en charge la correspondance de la valeur individuelle avec la valeur composite indexée, résultant en un plan de requête avec moins de la moitié du coût de l'original.

Éliminer les index en double

Au fil du temps, les index s'accumulent, et parfois on en ajoute un qui a exactement la même définition qu'un autre. Vous pouvez utiliser la vue catalogue pg_indexes pour obtenir les définitions SQL lisibles par l'homme des index. Vous pouvez également détecter facilement les définitions identiques :

  SELECT array_agg(indexname) AS indexes, replace(indexdef, indexname, '') AS defn
    FROM pg_indexes
GROUP BY defn
  HAVING count(*) > 1;

Et voici le résultat lorsqu'il est exécuté sur la base de données stock pagila :

pagila=#   SELECT array_agg(indexname) AS indexes, replace(indexdef, indexname, '') AS defn
pagila-#     FROM pg_indexes
pagila-# GROUP BY defn
pagila-#   HAVING count(*) > 1;
                                indexes                                 |                                defn
------------------------------------------------------------------------+------------------------------------------------------------------
 {payment_p2017_01_customer_id_idx,idx_fk_payment_p2017_01_customer_id} | CREATE INDEX  ON public.payment_p2017_01 USING btree (customer_id
 {payment_p2017_02_customer_id_idx,idx_fk_payment_p2017_02_customer_id} | CREATE INDEX  ON public.payment_p2017_02 USING btree (customer_id
 {payment_p2017_03_customer_id_idx,idx_fk_payment_p2017_03_customer_id} | CREATE INDEX  ON public.payment_p2017_03 USING btree (customer_id
 {idx_fk_payment_p2017_04_customer_id,payment_p2017_04_customer_id_idx} | CREATE INDEX  ON public.payment_p2017_04 USING btree (customer_id
 {payment_p2017_05_customer_id_idx,idx_fk_payment_p2017_05_customer_id} | CREATE INDEX  ON public.payment_p2017_05 USING btree (customer_id
 {idx_fk_payment_p2017_06_customer_id,payment_p2017_06_customer_id_idx} | CREATE INDEX  ON public.payment_p2017_06 USING btree (customer_id
(6 rows)

Index de surensemble

Il est également possible que vous vous retrouviez avec plusieurs index où l'un indexe un sur-ensemble de colonnes que l'autre fait. Cela peut être souhaitable ou non - le surensemble peut entraîner des analyses d'index uniquement, ce qui est une bonne chose, mais peut prendre trop de place, ou peut-être que la requête qu'il était initialement destiné à optimiser n'est plus utilisée.

Si vous souhaitez automatiser la détection de tels index, la table pg_catalog pg_index est un bon point de départ.

Index inutilisés

Au fur et à mesure que les applications qui utilisent la base de données évoluent, les requêtes qu'elles utilisent évoluent également. Les index qui ont été ajoutés précédemment ne peuvent plus être utilisés par aucune requête. Chaque fois qu'un index est scanné, il est noté par le gestionnaire de statistiques et le nombre cumulé est disponible dans la vue du catalogue système pg_stat_user_indexes comme valeur idx_scan . La surveillance de cette valeur sur une période de temps (par exemple, un mois) donne une bonne idée des index qui sont inutilisés et qui peuvent être supprimés.

Voici la requête pour obtenir le nombre d'analyses actuelles pour tous les index du schéma "public" :

SELECT relname, indexrelname, idx_scan
FROM   pg_catalog.pg_stat_user_indexes
WHERE  schemaname = 'public';

avec une sortie comme celle-ci :

pagila=# SELECT relname, indexrelname, idx_scan
pagila-# FROM   pg_catalog.pg_stat_user_indexes
pagila-# WHERE  schemaname = 'public'
pagila-# LIMIT  10;
    relname    |    indexrelname    | idx_scan
---------------+--------------------+----------
 customer      | customer_pkey      |    32093
 actor         | actor_pkey         |     5462
 address       | address_pkey       |      660
 category      | category_pkey      |     1000
 city          | city_pkey          |      609
 country       | country_pkey       |      604
 film_actor    | film_actor_pkey    |        0
 film_category | film_category_pkey |        0
 film          | film_pkey          |    11043
 inventory     | inventory_pkey     |    16048
(10 rows)

Reconstruire les index avec moins de verrouillage

Il n'est pas rare que les index doivent être recréés. Les index peuvent également devenir gonflés, et la recréation de l'index peut résoudre ce problème, ce qui le rend plus rapide à analyser. Les index peuvent également être corrompus. La modification des paramètres d'index peut également nécessiter la recréation de l'index.

Activer la création d'index parallèles

Dans PostgreSQL 11, la création d'index B-Tree est simultanée. Il peut utiliser plusieurs travailleurs parallèles pour accélérer la création de l'index. Cependant, vous devez vous assurer que ces entrées de configuration sont correctement définies :

SET max_parallel_workers = 32;
SET max_parallel_maintenance_workers = 16;

Les valeurs par défaut sont déraisonnablement petites. Idéalement, ces chiffres devraient augmenter avec le nombre de cœurs de processeur. Consultez la documentation pour plus d'informations.

Créer des index en arrière-plan

Vous pouvez également créer un index en arrière-plan, en utilisant CONCURRENTLY paramètre de CREATE INDEX commande :

pagila=# CREATE INDEX CONCURRENTLY idx_address1 ON address(district);
CREATE INDEX

Ceci est différent de la création d'un index normal en ce sens qu'il ne nécessite pas de verrou sur la table et ne verrouille donc pas les écritures. En revanche, cela prend plus de temps et de ressources.