Funzionamento degli indici in PostgreSQL


73

Ho un paio di domande sul funzionamento degli indici in PostgreSQL. Ho una Friendstabella con il seguente indice:

   Friends ( user_id1 ,user_id2) 

user_id1e user_id2sono chiavi esterne alla usertabella

  1. Sono equivalenti? Se no allora perché?

    Index(user_id1,user_id2) and Index(user_id2,user_id1)
  2. Se creo la chiave primaria (user_id1, user_id2), crea automaticamente gli indici per essa e

    Se gli indici nella prima domanda non sono equivalenti, quale indice viene creato sul comando chiave primaria sopra?

Risposte:


77

Ecco i risultati dell'interrogazione di una tabella sulla seconda colonna di un indice a più colonne .
Gli effetti sono facili da riprodurre per chiunque. Provalo a casa.

Ho provato con PostgreSQL 9.0.5 su Debian usando una tabella di medie dimensioni di un database reale con 23322 righe. Implementa la relazione n: m tra le tabelle adr(indirizzo) e att(attributo), ma non è rilevante qui. Schema semplificato:

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)
);

Il UNIQUEvincolo implementa efficacemente un indice univoco. Ho ripetuto il test con un semplice indice per essere sicuro e ho ottenuto risultati identici come previsto.

CREATE INDEX adratt_idx ON adratt(adr_id, att_id)

La tabella è raggruppata adratt_unisull'indice e prima del test ho eseguito:

CLUSTER adratt;
ANALYZE adratt;

Le scansioni sequenziali per le query (adr_id, att_id)sono più veloci che possono essere. L'indice a più colonne verrà comunque utilizzato per una condizione di query solo sulla seconda colonna dell'indice.

Ho eseguito le query un paio di volte per popolare la cache e ho scelto la migliore delle dieci esecuzioni per ottenere risultati comparabili.

1. Interrogazione utilizzando entrambe le colonne

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)

Uscita di 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. Interrogazione utilizzando la prima colonna

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)

Uscita di 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. Interrogazione utilizzando la seconda colonna

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)

Uscita di 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. Disabilita indexscan e bitmapscan

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

Uscita di 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

Uscita di 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

Conclusione

Come previsto, l'indice a più colonne viene utilizzato per una query sulla sola seconda colonna.
Come previsto, è meno efficace, ma la query è ancora 3 volte più veloce che senza l'indice.
Dopo aver disabilitato le scansioni dell'indice, il pianificatore di query sceglie una scansione heap bitmap, che si comporta quasi altrettanto velocemente. Solo dopo aver disabilitato anche quello, torna a una scansione sequenziale.


il clustering farà la differenza se il numero di corrispondenze nell'indice è abbastanza alto (vedi qui per la prova - nota le doppie esecuzioni per ottenere i dati memorizzati nella cache)
Jack Douglas

1
@JackDouglas: ci ho pensato un po 'di più. Il clustering può essere di aiuto in generale, perché è effettivamente anche a vacuum fulle a reindex. Oltre a ciò, aiuterà molto a indicizzare le scansioni sulla prima o su entrambe le colonne iniziali , ma danneggerà le query sulla seconda colonna. In una tabella appena raggruppata, le righe con lo stesso valore nella seconda colonna vengono distribuite, in modo che sia necessario leggere un massimo di blocchi.
Erwin Brandstetter,

28

re 1) Sì e no.

Per una query che utilizza entrambe le colonne, ad esempio where (user_id1, user_id2) = (1,2), non importa quale indice viene creato.

Per una query che ha una condizione su una sola colonna, ad esempio, where user_id1 = 1è importante perché in genere solo l'ottimizzatore può utilizzare solo le colonne "principali". Quindi where user_id1 = 1sarebbe in grado di utilizzare l'indice (user_id1, user_id2) ma non sarebbe in grado di utilizzare un indice (user_id2, user_id1) per tutti i casi.

Dopo aver giocato con questo (dopo che Erwin ci ha gentilmente mostrato una configurazione in cui funziona), sembra che questo dipenda fortemente dalla distribuzione dei dati della seconda colonna, anche se non ho ancora scoperto quale situazione consente all'ottimizzatore di utilizzare le colonne finali per una condizione WHERE.

Oracle 11 che può anche (a volte) utilizzare colonne che non sono all'inizio della definizione dell'indice.

re 2) Sì, creerà un indice

Citazione dal manuale

L'aggiunta di una chiave primaria creerà automaticamente un indice btree univoco sulla colonna o sul gruppo di colonne utilizzate nella chiave primaria.

re 2a) Primary Key (user_id1,user_id2)creerà un indice su (user_id1, user_id2) (che puoi scoprire da solo molto facilmente creando semplicemente una chiave primaria)

Consiglio vivamente di leggere il capitolo sugli indici nel manuale , in sostanza risponde a tutte le domande sopra.

Inoltre, quale indice creare? di depesz fa un buon lavoro spiegando l'ordine su colonne di indice e altri argomenti relativi all'indice.


11

Annuncio 1)
In PostgreSQL sono presenti limitazioni come descritto da @a_horse_with_no_name . Fino alla versione 8.0, gli indici a più colonne potevano essere utilizzati solo per le query sulle colonne principali. Questo è stato migliorato nella versione 8.1. Il manuale attuale di Postgres 10 (aggiornato) spiega:

Un indice B-tree a più colonne può essere utilizzato con condizioni di query che coinvolgono qualsiasi sottoinsieme delle colonne dell'indice, ma l'indice è più efficiente in presenza di vincoli sulle colonne principali (più a sinistra). La regola esatta è che i vincoli di uguaglianza sulle colonne iniziali, più eventuali vincoli di disuguaglianza sulla prima colonna che non hanno un vincolo di uguaglianza, saranno utilizzati per limitare la parte dell'indice che viene scansionata. I vincoli sulle colonne a destra di queste colonne sono controllati nell'indice, quindi salvano le visite nella tabella corretta, ma non riducono la parte dell'indice che deve essere scansionata. Ad esempio, dato un indice attivo (a, b, c)e una condizione della query WHERE a = 5 AND b >= 42 AND c < 77, l'indice dovrebbe essere scansionato dalla prima voce con a= 5 eb= 42 fino all'ultima voce con a= 5. Le voci dell'indice con c> = 77 verrebbero ignorate, ma dovrebbero comunque essere scansionate. In linea di principio questo indice potrebbe essere utilizzato per le query che hanno vincoli su be / o csenza vincolo su a- ma l'intero indice dovrebbe essere scansionato, quindi nella maggior parte dei casi il planner preferirebbe una scansione sequenziale della tabella rispetto all'indice.

Enfasi mia. Posso confermarlo per esperienza.
Vedi anche il caso di prova aggiunto la mia risposta successiva qui .


11

Questo è in risposta alla risposta di Jack , un commento non lo farebbe.

In PostgreSQL non c'erano indici di copertura prima della versione 9.2. A causa del modello MVCC, ogni tupla nel set di risultati deve essere visitata per verificare la visibilità. Forse stai pensando a Oracle.

Gli sviluppatori PostgreSQL parlano di "scansioni solo indice" . In effetti, la funzione è stata rilasciata con Postgres 9.2. Leggi il messaggio di commit .
Depesz ha scritto un post sul blog molto istruttivo .

Gli indici di copertura reali (aggiornamento) vengono introdotti con la INCLUDEclausola Postgres 11. Correlati:

Anche questo è un po 'fuori:

si basa sul fatto che una "scansione completa" di un indice è spesso più rapida di una "scansione completa" della tabella indicizzata a causa delle colonne extra nella tabella che non compaiono nell'indice.

Come riportato nei commenti sull'altra mia risposta, ho anche eseguito dei test con una tabella di due numeri interi e nient'altro. L'indice contiene le stesse colonne della tabella. La dimensione di un indice btree è di circa 2/3 della tabella. Non abbastanza per spiegare un'accelerazione del fattore 3. Ho eseguito più test, in base alla tua configurazione, semplificato su due colonne e con 100000 righe. Nella mia installazione di PostgreSQL 9.0 i risultati sono stati coerenti.

Se la tabella ha colonne aggiuntive, l'accelerazione con indice diventa più sostanziale, ma questo non è certamente l'unico fattore qui .

Per riassumere i punti principali:

  • Gli indici a più colonne possono essere utilizzati con le query su colonne non iniziali, ma l'accelerazione si basa solo sul fattore 3 per criteri selettivi (piccola percentuale di righe nel risultato). Più in alto per le tuple più grandi, più in basso per le parti più grandi della tabella nel set di risultati.

  • Crea un indice aggiuntivo su quelle colonne se le prestazioni sono importanti.

  • Se tutte le colonne interessate sono incluse in un indice (indice di copertura) e tutte le righe interessate (per blocco) sono visibili a tutte le transazioni, è possibile ottenere una "scansione solo indice" a pag 9.2 o successive.


7
  1. Sono equivalenti? Se no allora perché?

    Index (user_id1, user_id2) e Index (user_id2, user_id1)

Questi non sono equivalenti e l'indice in generale (bar, baz) non sarà efficace per le query del modulo select * from foo where baz=?

Erwin ha dimostrato che tali indici possono effettivamente accelerare una query, ma questo effetto è limitato e non dello stesso ordine in cui ci si aspetta che un indice migliori una ricerca - si basa sul fatto che una "scansione completa" di un indice è spesso più veloce di una "scansione completa" della tabella indicizzata a causa delle colonne extra nella tabella che non compaiono nell'indice.

Riepilogo: gli indici possono aiutare le query anche su colonne non iniziali, ma in uno dei due modi secondari e relativamente minori e non nel modo drammatico che normalmente ti aspetti che un indice aiuti a causa della sua struttura btree

nb i due modi in cui l'indice può aiutare sono se una scansione completa dell'indice è significativamente più economica di una scansione completa della tabella e sia: 1. le ricerche della tabella sono economiche (perché ce ne sono poche o sono raggruppate), oppure 2. l'indice copre quindi non ci sono ricerche di tabella a tutti gli spiacenti, vedere i commenti di Erwins qui

banco di prova:

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;

query 1 (nessun indice, colpendo 74 buffer ):

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

query 2 (con indice - l'ottimizzatore ignora l'indice - colpendo nuovamente 74 buffer ):

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

query 2 (con indice - e inganniamo l'ottimizzatore per usarlo):

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

Quindi l'accesso tramite l'indice è due volte più veloce in questo caso colpendo 30 buffer - che in termini di indicizzazione è 'leggermente più veloce'! E YMMV a seconda della dimensione relativa di tabella e indice, insieme al numero di righe filtrate e caratteristiche di clustering dei dati nella tabella

Al contrario, le query sulla colonna principale fanno uso della struttura btree dell'indice, in questo caso colpendo 2 buffer :

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
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.