web-dev-qa-db-fra.com

Fonctionnement des index dans PostgreSQL

J'ai quelques questions concernant le fonctionnement des index dans PostgreSQL. J'ai une table Friends avec l'index suivant:

   Friends ( user_id1 ,user_id2) 

user_id1 et user_id2 sont des clés étrangères à la table user

  1. Sont-ils équivalents? Sinon, pourquoi?

    Index(user_id1,user_id2) and Index(user_id2,user_id1)
    
  2. Si je crée la clé primaire (user_id1, user_id2), cela crée-t-il automatiquement des index pour elle et

    Si les index de la première question ne sont pas équivalents, alors quel index est créé sur la commande de clé primaire ci-dessus?

80
codecool

Voici les résultats de l'interrogation d'une table sur le deuxième colonne d'un index multicolonne.
Les effets sont faciles à reproduire pour tout le monde. Essayez-le à la maison.

J'ai testé avec PostgreSQL 9.0.5 sur Debian en utilisant un tableau de taille moyenne d'une base de données réelle avec 23322 lignes. Il implémente la relation n: m entre les tables adr (adresse) et att (attribut), mais ce n'est pas pertinent ici. Schéma simplifié:

CREATE TABLE adratt (
  adratt_id serial PRIMARY KEY
, adr_id    integer NOT NULL
, att_id    integer NOT NULL
, log_up    timestamp(0) NOT NULL DEFAULT (now())::timestamp(0)
, CONSTRAINT adratt_uni UNIQUE (adr_id, att_id)
);

La contrainte UNIQUE implémente efficacement un index unique. J'ai répété le test avec un indice simple pour être sûr et j'ai obtenu des résultats identiques comme prévu.

CREATE INDEX adratt_idx ON adratt(adr_id, att_id)

La table est regroupée sur le adratt_uni index et avant le test j'ai couru:

CLUSTER adratt;
ANALYZE adratt;

Analyses séquentielles pour les requêtes sur (adr_id, att_id) sont aussi rapides que possible. L'index multicolonne sera toujours utilisé pour une condition de requête sur la deuxième colonne d'index seule.

J'ai exécuté les requêtes plusieurs fois pour remplir le cache et j'ai choisi le meilleur des dix runs pour obtenir des résultats comparables.

1. Requête utilisant les deux colonnes

SELECT *
FROM   adratt
WHERE  att_id = 90
AND    adr_id = 10;

 adratt_id | adr_id | att_id |       log_up
-----------+--------+--------+---------------------
       123 |     10 |     90 | 2008-07-29 09:35:54
(1 row)

Sortie de EXPLAIN ANALYZE:

Index Scan using adratt_uni on adratt  (cost=0.00..3.48 rows=1 width=20) (actual time=0.022..0.025 rows=1 loops=1)
  Index Cond: ((adr_id = 10) AND (att_id = 90))
Total runtime: 0.067 ms

2. Requête en utilisant la première colonne

SELECT * FROM adratt WHERE adr_id = 10

 adratt_id | adr_id | att_id |       log_up
-----------+--------+--------+---------------------
       126 |     10 |     10 | 2008-07-29 09:35:54
       125 |     10 |     13 | 2008-07-29 09:35:54
      4711 |     10 |     21 | 2008-07-29 09:35:54
     29322 |     10 |     22 | 2011-06-06 15:50:38
     29321 |     10 |     30 | 2011-06-06 15:47:17
       124 |     10 |     62 | 2008-07-29 09:35:54
     21913 |     10 |     78 | 2008-07-29 09:35:54
       123 |     10 |     90 | 2008-07-29 09:35:54
     28352 |     10 |    106 | 2010-11-22 12:37:50
(9 rows)

Sortie de EXPLAIN ANALYZE:

Index Scan using adratt_uni on adratt  (cost=0.00..8.23 rows=9 width=20) (actual time=0.007..0.023 rows=9 loops=1)
  Index Cond: (adr_id = 10)
Total runtime: 0.058 ms

3. Requête à l'aide de la deuxième colonne

SELECT * FROM adratt WHERE att_id = 90

 adratt_id | adr_id | att_id |       log_up
-----------+--------+--------+---------------------
       123 |     10 |     90 | 2008-07-29 09:35:54
       180 |     39 |     90 | 2008-08-29 15:46:07
...
(83 rows)

Sortie de EXPLAIN ANALYZE:

Index Scan using adratt_uni on adratt  (cost=0.00..818.51 rows=83 width=20) (actual time=0.014..0.694 rows=83 loops=1)
  Index Cond: (att_id = 90)
Total runtime: 0.849 ms

4. Désactiver indexscan et bitmapscan

SET enable_indexscan = off;
SELECT * FROM adratt WHERE att_id = 90

Sortie de EXPLAIN ANALYZE:

Bitmap Heap Scan on adratt  (cost=779.94..854.74 rows=83 width=20) (actual time=0.558..0.743 rows=83 loops=1)
  Recheck Cond: (att_id = 90)
  ->  Bitmap Index Scan on adratt_uni  (cost=0.00..779.86 rows=83 width=0) (actual time=0.544..0.544 rows=83 loops=1)
        Index Cond: (att_id = 90)
Total runtime: 0.894 ms
SET enable_bitmapscan = off
SELECT * FROM adratt WHERE att_id = 90

Sortie de EXPLAIN ANALYZE:

Seq Scan on adratt  (cost=0.00..1323.10 rows=83 width=20) (actual time=0.009..2.429 rows=83 loops=1)
  Filter: (att_id = 90)
Total runtime: 2.680 ms

Conclusion

Comme prévu, l'index multi-colonnes est utilisé pour une requête sur la deuxième colonne uniquement.
Comme prévu, il est moins efficace, mais la requête est toujours fois plus rapide que sans l'index.
Après avoir désactivé les analyses d'index, le planificateur de requêtes choisit une analyse de tas bitmap, qui fonctionne presque aussi rapidement. Ce n'est qu'après avoir désactivé cela que cela revient à un balayage séquentiel.

83

re 1) Oui et non.

Pour une requête qui utilise les deux colonnes, par exemple where (user_id1, user_id2) = (1,2) peu importe quel index est créé.

Pour une requête qui a une condition sur une seule des colonnes, par exemple where user_id1 = 1 Cela a de l'importance car généralement seules les colonnes "en tête" peuvent être utilisées pour une comparaison par l'optimiseur. Ainsi, where user_id1 = 1 Pourrait utiliser l'index (user_id1, user_id2) mais il ne pourrait pas utiliser d'index (user_id2, user_id1) pour tous les cas.

Après avoir joué avec cela (après qu'Erwin nous ait si gentiment montré une configuration où cela fonctionne), il semble que cela dépende fortement de la distribution des données de la deuxième colonne bien que je n'aie pas encore découvert quelle situation permet à l'optimiseur d'utiliser des colonnes de fin pour une condition OERE.

Oracle 11 qui peut également (parfois) utiliser des colonnes qui ne sont pas au début de la définition de l'index.

re 2) Oui, cela créera un index

Citation du manuel

L'ajout d'une clé primaire créera automatiquement un index btree unique sur la colonne ou le groupe de colonnes utilisé dans la clé primaire.

re 2a) Primary Key (user_id1,user_id2) créera un index sur (user_id1, user_id2) (que vous pouvez découvrir par vous-même très facilement simplement création d'une telle clé primaire)

Je vous recommande fortement de lire le chapitre sur les index dans le manuel , il répond essentiellement à toutes les questions ci-dessus.

De plus, Quel index créer? by depesz explique bien l'ordre des colonnes d'index et d'autres sujets liés à l'index.

30

Annonce 1)
Il y a des limitations dans PostgreSQL comme @a_horse_with_no_name le décrit . Jusqu'à version 8. les index multicolonnes ne pouvaient être utilisés que pour les requêtes sur les colonnes de tête. Cela a été amélioré dans la version 8.1. Le manuel actuel pour Postgres 1 (mis à jour) explique:

Un index B-tree multicolonne peut être utilisé avec des conditions de requête qui impliquent n'importe quel sous-ensemble des colonnes de l'index, mais l'index est plus efficace lorsqu'il y a des contraintes sur les colonnes de tête (les plus à gauche). La règle exacte est que les contraintes d'égalité sur les colonnes de tête, ainsi que toutes les contraintes d'inégalité sur la première colonne qui n'ont pas de contrainte d'égalité, seront utilisées pour limiter la partie de l'index analysée. Les contraintes sur les colonnes à droite de ces colonnes sont vérifiées dans l'index, donc elles économisent les visites à la table proprement dite, mais elles ne réduisent pas la partie de l'index qui doit être analysée. Par exemple, étant donné un index sur (a, b, c) et une condition de requête WHERE a = 5 AND b >= 42 AND c < 77, l'index devrait être analysé à partir de la première entrée avec a = 5 et b = 42 jusqu'à la dernière entrée avec a = 5. Entrées d'index avec c> = 77 serait ignoré, mais ils devraient encore être analysés. Cet index pourrait en principe être utilisé pour les requêtes qui ont des contraintes sur b et/ou c sans aucune contrainte sur a - mais l'index entier devrait être analysé, donc dans la plupart des cas, le planificateur préférerait un balayage séquentiel de table plutôt que l'utilisation de l'index.

Je souligne. Je peux le confirmer par expérience.
Voir également le cas de test ajouté ma réponse ultérieure ici .

12

C'est en réponse à la réponse de Jack , un commentaire ne ferait pas.

Il n'y avait aucun index couvrant dans PostgreSQL avant la version 9.2. En raison du modèle MVCC, chaque tuple du jeu de résultats doit être visité pour vérifier la visibilité. Vous pensez peut-être à Oracle.

Les développeurs de PostgreSQL parlent de "scans indexés uniquement" . En fait, la fonctionnalité a été publiée avec Postgres 9.2. Lire le message de validation .
Depesz a écrit un article de blog très instructif .

Les vrais indices de couverture (mise à jour) sont introduits avec la clause INCLUDE avec Postgres 11. Liés:

C'est aussi un peu décalé:

elle repose sur le fait qu'une "analyse complète" d'un index est souvent plus rapide qu'une "analyse complète" de la table indexée en raison des colonnes supplémentaires de la table qui n'apparaissent pas dans l'index.

Comme indiqué dans les commentaires sur mon autre réponse, j'ai également exécuté des tests avec une table de deux nombres entiers et rien d'autre. L'index contient les mêmes colonnes que la table. La taille d'un index btree est d'environ 2/3 de celle de la table. Pas assez pour expliquer une accélération du facteur 3. J'ai exécuté plus de test, basé sur votre configuration, simplifié en deux colonnes et avec 100 000 lignes. Sur mon installation PostgreSQL 9.0, les résultats étaient cohérents.

Si le tableau a des colonnes supplémentaires, l'accélération avec index devient plus substantielle, mais ce n'est certainement pas le seul facteur ici .

Pour résumer les points principaux:

  • Les index multi-colonnes peuvent être utilisés avec des requêtes sur des colonnes non en tête, mais l'accélération est seulement autour du facteur 3 pour les critères sélectifs (petit pourcentage de lignes dans le résultat). Plus haut pour les tuples plus gros, plus bas pour les plus grandes portions du tableau dans le jeu de résultats.

  • Créez un index supplémentaire sur ces colonnes si les performances sont importantes.

  • Si toutes les colonnes impliquées sont incluses dans un index (index couvrant) et que toutes les lignes impliquées (par bloc) sont visibles pour toutes les transactions, vous pouvez obtenir un "scan d'index uniquement" en pg 9.2 ou version ultérieure.

12
  1. Sont-ils équivalents? Sinon, pourquoi?

    Index (user_id1, user_id2) et Index (user_id2, user_id1)

Ceux-ci ne sont pas équivalents et de manière générale, l'index (bar, baz) ne sera pas efficace pour les requêtes de la forme select * from foo where baz=?

Erwin a démontré que de tels index peuvent en effet accélérer une requête mais cet effet est limité et pas du même ordre que vous attendez généralement d'un index pour améliorer une recherche - il repose sur le fait qu'un 'full l'analyse 'd'un index est souvent plus rapide qu'une' analyse complète 'de la table indexée en raison des colonnes supplémentaires dans la table qui n'apparaissent pas dans l'index.

Résumé: les index peuvent aider les requêtes, même sur les colonnes non principales, mais de deux manières secondaires et relativement mineures et pas de la manière spectaculaire que vous attendez normalement d'un index en raison de sa structure btree

nb les deux façons dont l'index peut aider sont si une analyse complète de l'index est nettement moins chère qu'une analyse complète de la table et soit: 1. les recherches de table sont bon marché (car il y en a peu ou elles sont groupées), ou 2. l'index est couvrant donc il n'y a aucune recherche de table du tout oups, voir les commentaires d'Erwins ici

banc d'essai:

create table foo(bar integer not null, baz integer not null, qux text not null);

insert into foo(bar, baz, qux)
select random()*100, random()*100, 'some random text '||g from generate_series(1,10000) g;

requête 1 (pas d'index, frapper 74 tampons ):

explain (buffers, analyze, verbose) select max(qux) from foo where baz=0;
                                                  QUERY PLAN
--------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=181.41..181.42 rows=1 width=32) (actual time=3.301..3.302 rows=1 loops=1)
   Output: max(qux)
   Buffers: shared hit=74
   ->  Seq Scan on stack.foo  (cost=0.00..181.30 rows=43 width=32) (actual time=0.043..3.228 rows=52 loops=1)
         Output: bar, baz, qux
         Filter: (foo.baz = 0)
         Buffers: shared hit=74
 Total runtime: 3.335 ms

requête 2 (avec index - l'optimiseur ignore l'index - frapper 74 tampons à nouveau):

create index bar_baz on foo(bar, baz);

explain (buffers, analyze, verbose) select max(qux) from foo where baz=0;
                                                  QUERY PLAN
--------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=199.12..199.13 rows=1 width=32) (actual time=3.277..3.277 rows=1 loops=1)
   Output: max(qux)
   Buffers: shared hit=74
   ->  Seq Scan on stack.foo  (cost=0.00..199.00 rows=50 width=32) (actual time=0.043..3.210 rows=52 loops=1)
         Output: bar, baz, qux
         Filter: (foo.baz = 0)
         Buffers: shared hit=74
 Total runtime: 3.311 ms

requête 2 (avec index - et nous trompons l'optimiseur pour l'utiliser):

explain (buffers, analyze, verbose) select max(qux) from foo where bar>-1000 and baz=0;
                                                       QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=115.56..115.57 rows=1 width=32) (actual time=1.495..1.495 rows=1 loops=1)
   Output: max(qux)
   Buffers: shared hit=36 read=30
   ->  Bitmap Heap Scan on stack.foo  (cost=73.59..115.52 rows=17 width=32) (actual time=1.370..1.428 rows=52 loops=1)
         Output: bar, baz, qux
         Recheck Cond: ((foo.bar > (-1000)) AND (foo.baz = 0))
         Buffers: shared hit=36 read=30
         ->  Bitmap Index Scan on bar_baz  (cost=0.00..73.58 rows=17 width=0) (actual time=1.356..1.356 rows=52 loops=1)
               Index Cond: ((foo.bar > (-1000)) AND (foo.baz = 0))
               Buffers: shared read=30
 Total runtime: 1.535 ms

Ainsi, l'accès via l'index est deux fois plus rapide dans ce cas, atteignant 30 tampons - qui en termes d'indexation est `` légèrement plus rapide '' !, et YMMV selon la taille relative de la table et de l'index, ainsi que le nombre de lignes filtrées et les caractéristiques de regroupement des données dans la table

En revanche, les requêtes sur la colonne de tête utilisent la structure btree de l'index - dans ce cas, frappant 2 tampons :

explain (buffers, analyze, verbose) select max(qux) from foo where bar=0;
                                                       QUERY PLAN
------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=75.70..75.71 rows=1 width=32) (actual time=0.172..0.173 rows=1 loops=1)
   Output: max(qux)
   Buffers: shared hit=38
   ->  Bitmap Heap Scan on stack.foo  (cost=4.64..75.57 rows=50 width=32) (actual time=0.036..0.097 rows=59 loops=1)
         Output: bar, baz, qux
         Recheck Cond: (foo.bar = 0)
         Buffers: shared hit=38
         ->  Bitmap Index Scan on bar_baz  (cost=0.00..4.63 rows=50 width=0) (actual time=0.024..0.024 rows=59 loops=1)
               Index Cond: (foo.bar = 0)
               Buffers: shared hit=2
 Total runtime: 0.209 ms