Utilisation des index dans PostgreSQL


73

J'ai quelques questions concernant l'utilisation des index dans PostgreSQL. J'ai une Friendstable avec l'index suivant:

   Friends ( user_id1 ,user_id2) 

user_id1et user_id2sont des clés étrangères à la usertable

  1. Sont-ils équivalents? Si non alors pourquoi?

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

    Si les index de la première question ne sont pas équivalents, quel index est créé sur le raccourci clavier ci-dessus?

Réponses:


77

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

J'ai testé avec PostgreSQL 9.0.5 sur Debian en utilisant une table 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 UNIQUEcontrainte implémente effectivement un index unique. J'ai répété le test avec un index simple pour être sûr et j'ai obtenu les mêmes résultats que prévu.

CREATE INDEX adratt_idx ON adratt(adr_id, att_id)

La table est en cluster sur l' adratt_uniindex et avant le test, j'ai exécuté:

CLUSTER adratt;
ANALYZE adratt;

Les analyses séquentielles des requêtes (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 uniquement.

J'ai couru les requêtes plusieurs fois pour remplir la mémoire cache et les meilleurs tirages sur dix ont été sélectionnés 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 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 utilisant 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 & bitmapscan

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

Sortie de EXPLAIN ANALYSE:

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-colonne 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 trois fois plus rapide que sans l'index.
Après la désactivation des analyses d'index, le planificateur de requêtes choisit une analyse de tas bitmap, qui est presque aussi rapide. Ce n’est qu’après avoir également désactivé cette fonction qu’elle passe à une analyse séquentielle.


le regroupement fera une différence si le nombre de correspondances dans l'index est suffisamment élevé (voir ici pour preuve - notez les doubles exécutions pour obtenir les données mises en cache)
Jack Douglas

1
@ JackDouglas: J'ai réfléchi à cette question. Le regroupement peut aider d’une manière générale, car c’est effectivement aussi un a vacuum fullet un reindex. Autre que cela aidera le balayage d'index sur la première ou les deux colonnes menant beaucoup , mais mal des requêtes sur la deuxième colonne. Dans une table fraîchement en cluster, les lignes avec la même valeur dans la deuxième colonne sont dispersées, de sorte qu'un maximum de blocs doit être lu.
Erwin Brandstetter

28

re 1) Oui et non.

Pour une requête qui utilise les deux colonnes, par exemple, l' where (user_id1, user_id2) = (1,2)index créé n'a pas d'importance.

Pour une requête qui a une condition sur une seule des colonnes, par exemple, where user_id1 = 1cela a de l'importance, car seules les colonnes "principales" peuvent être utilisées pour une comparaison par l'optimiseur. Donc, where user_id1 = 1serait en mesure d'utiliser l'index (user_id1, user_id2) mais il ne serait pas en mesure de l'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 beaucoup 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 suivi. pour une condition WHERE.

Oracle 11 qui peut également (parfois) utiliser des colonnes qui ne se trouvent 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 pourrez trouver vous-même très facilement en créant simplement 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 fait un bon travail en expliquant l'ordre dans les colonnes d'index et d'autres sujets liés à l'index.


11

Ad 1)
Il existe des limitations dans PostgreSQL, comme décrit par @a_horse_with_no_name . Jusqu'à la version 8.0, les index multicolonnes ne pouvaient être utilisés que pour les requêtes sur la ou les colonnes principales. Cela a été amélioré dans la version 8.1. Le manuel actuel de Postgres 10 (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 il est plus efficace lorsqu'il existe 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 les éventuelles 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 situées à droite de ces colonnes sont cochées dans l'index. Elles enregistrent donc les visites dans la table proprement dite, mais ne réduisent pas la partie de l'index à analyser. Par exemple, avec un index activé (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 etb= 42 jusqu'à la dernière entrée avec a= 5. Les entrées d'index avec c> = 77 seraient ignorées, mais elles devraient tout de même être balayées. Cet index pourrait en principe être utilisé pour les requêtes soumises à des contraintes bet / ou csans contrainte a, mais tout l'index devrait être analysé. Par conséquent, dans la plupart des cas, le planificateur préférerait une analyse séquentielle à l'aide de l'index.

L'accent est à moi. Je peux le confirmer par expérience.
Voir aussi le cas de test ajouté ma réponse plus tard ici .


11

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

Il n'y avait pas d'index de couverture 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 d’ analyses à index uniquement . En fait, cette fonctionnalité a été publiée avec Postgres 9.2. Lisez le message de validation .
Depesz a écrit un article de blog très instructif .

Les vrais index de couverture (update) sont introduits dans la INCLUDEclause avec Postgres 11. Liés:

Ceci est un peu off, aussi:

il 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 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 tests, basés sur votre configuration, simplifiés à deux colonnes et avec 100 000 lignes. Sur mon installation PostgreSQL 9.0, les résultats étaient cohérents.

Si la table comporte des colonnes supplémentaires, l'accélération avec l'index devient plus substantielle, mais ce n'est certainement pas le seul facteur à prendre en compte .

Pour résumer les points principaux:

  • Les index multi-colonnes peuvent être utilisés avec des requêtes sur des colonnes non majuscules, mais l'accélération n'est que d'environ 3 fois le facteur 3 pour les critères de sélection (faible pourcentage de lignes dans le résultat). Plus haut pour les nuplets plus grands, plus bas pour les plus grandes parties du tableau dans l'ensemble 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 par toutes les transactions, vous pouvez obtenir une "analyse d'index uniquement" à la page 9.2 ou ultérieure.


7
  1. Sont-ils équivalents? Si non alors pourquoi?

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

Ce ne sont pas équivalents et généralement 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 effectivement accélérer une requête, mais cet effet est limité et n’est pas du même ordre que celui attendu d’un index pour améliorer une recherche. Il repose sur le fait qu’un «balayage complet» 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.

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

Les deux manières dont l'index peut aider sont si une analyse complète de l'indice est beaucoup moins chère qu'une analyse complète du tableau et soit: 1. les recherches dans le tableau sont peu coûteuses (car elles sont peu nombreuses ou sont regroupées), ou 2. l'index est couvrant donc il n'y a pas de consultation de table à tous les oops, 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, frappant 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 - frappe à nouveau 74 tampons ):

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 - ce qui en termes d'indexation est "légèrement plus rapide" !, et YMMV en fonction de la taille relative de la table et de l'index, ainsi que du nombre de lignes filtrées et des caractéristiques de clustering. 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, deux tampons sont activés :

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
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.