Indice sulla chiave primaria non utilizzato nel join semplice


16

Ho le seguenti definizioni di tabella e indice:

CREATE TABLE munkalap (
    munkalap_id serial PRIMARY KEY,
    ...
);

CREATE TABLE munkalap_lepes (
    munkalap_lepes_id serial PRIMARY KEY,
    munkalap_id integer REFERENCES munkalap (munkalap_id),
    ...
);

CREATE INDEX idx_munkalap_lepes_munkalap_id ON munkalap_lepes (munkalap_id);

Perché nessuno degli indici su munkalap_id viene utilizzato nella seguente query?

EXPLAIN ANALYZE SELECT ml.* FROM munkalap m JOIN munkalap_lepes ml USING (munkalap_id);

QUERY PLAN
Hash Join  (cost=119.17..2050.88 rows=38046 width=214) (actual time=0.824..18.011 rows=38046 loops=1)
  Hash Cond: (ml.munkalap_id = m.munkalap_id)
  ->  Seq Scan on munkalap_lepes ml  (cost=0.00..1313.46 rows=38046 width=214) (actual time=0.005..4.574 rows=38046 loops=1)
  ->  Hash  (cost=78.52..78.52 rows=3252 width=4) (actual time=0.810..0.810 rows=3253 loops=1)
        Buckets: 1024  Batches: 1  Memory Usage: 115kB
        ->  Seq Scan on munkalap m  (cost=0.00..78.52 rows=3252 width=4) (actual time=0.003..0.398 rows=3253 loops=1)
Total runtime: 19.786 ms

È lo stesso anche se aggiungo un filtro:

EXPLAIN ANALYZE SELECT ml.* FROM munkalap m JOIN munkalap_lepes ml USING (munkalap_id) WHERE NOT lezarva;

QUERY PLAN
Hash Join  (cost=79.60..1545.79 rows=1006 width=214) (actual time=0.616..10.824 rows=964 loops=1)
  Hash Cond: (ml.munkalap_id = m.munkalap_id)
  ->  Seq Scan on munkalap_lepes ml  (cost=0.00..1313.46 rows=38046 width=214) (actual time=0.007..5.061 rows=38046 loops=1)
  ->  Hash  (cost=78.52..78.52 rows=86 width=4) (actual time=0.587..0.587 rows=87 loops=1)
        Buckets: 1024  Batches: 1  Memory Usage: 4kB
        ->  Seq Scan on munkalap m  (cost=0.00..78.52 rows=86 width=4) (actual time=0.014..0.560 rows=87 loops=1)
              Filter: (NOT lezarva)
Total runtime: 10.911 ms

Risposte:


22

Molte persone hanno sentito che "le scansioni sequenziali sono cattive" e cercano di eliminarle dai loro piani, ma non è così semplice. Se una query coprirà ogni riga di una tabella, una scansione sequenziale è il modo più veloce per ottenere quelle righe. Questo è il motivo per cui la query di join originale ha utilizzato la scansione seq, poiché erano necessarie tutte le righe in entrambe le tabelle.

Quando pianifica una query, il pianificatore di Postgres stima i costi di varie operazioni (calcolo, I / O sequenziali e casuali) in base a diversi schemi possibili e sceglie il piano che ritiene avere il costo più basso. Quando si esegue l'IO dall'archiviazione in rotazione (dischi), l'IO casuale è di solito sostanzialmente più lento dell'IO sequenziale, la configurazione pg predefinita per random_page_cost e seq_page_cost stimano una differenza di 4: 1 nel costo.

Queste considerazioni entrano in gioco quando si considera un metodo di join o filtro che utilizza un indice contro uno che esegue la scansione sequenziale di una tabella. Quando si utilizza un indice, il piano può trovare rapidamente una riga tramite l'indice, quindi è necessario tenere conto di un blocco casuale letto per risolvere i dati della riga. Nel caso della tua seconda query che ha aggiunto un predicato di filtro WHERE NOT lezarva, puoi vedere in che modo ciò ha influito sulle stime di pianificazione nei risultati EXPLAIN ANALYZE. Il pianificatore stima 1006 righe risultanti dal join (che corrisponde abbastanza da vicino al set di risultati effettivo di 964). Dato che la tabella più grande munkalap_lepes contiene circa 38 KB di righe, il pianificatore vede che il join dovrà accedere a circa 1006/38046 o 1/38 delle righe della tabella. Sa anche che la larghezza della riga avg è di 214 byte e un blocco è 8K, quindi ci sono circa 38 righe / blocco.

Con queste statistiche, il pianificatore considera probabile che il join dovrà leggere tutti o la maggior parte dei blocchi di dati della tabella. Poiché le ricerche dell'indice non sono gratuite e il calcolo per scansionare un blocco che valuta una condizione di filtro è molto economico rispetto all'IO, il planner ha scelto di scansionare sequenzialmente la tabella ed evitare overhead dell'indice e letture casuali mentre calcola la scansione seq sarà più veloce.

Nel mondo reale, i dati sono spesso disponibili in memoria tramite la cache della pagina del sistema operativo e quindi non tutti i blocchi letti richiedono IO. Può essere piuttosto difficile prevedere l'efficacia di una cache per una determinata query, ma il planner Pg utilizza alcune semplici euristiche. Il valore di configurazione efficace_cache_size informa i pianificatori delle stime della probabilità di incorrere in costi di I / O effettivi. Un valore più elevato indurrà a stimare un costo inferiore per IO casuale e potrebbe quindi orientarlo verso un metodo guidato dall'indice su una scansione sequenziale.


Grazie, questa è finora la migliore (e più concisa) descrizione che ho letto. Chiariti alcuni punti chiave.
dezso

1
Spiegazione eccellente. Tuttavia, il calcolo della pagina di righe / dati è un po 'fuori. Devi fattorizzare l'intestazione della pagina (24 byte) + 4 byte per ogni puntatore elemento per riga + l'intestazione della riga HeapTupleHeader(23 byte per riga) + maschera di bit NULL + allineamento secondo MAXALIGN. Infine, una quantità sconosciuta di riempimento a causa dell'allineamento dei dati dipende dai tipi di dati delle colonne e dalla loro sequenza. Tutto sommato non ci sono più di 33 righe su una pagina da 8 kb in questo caso. (Non tenendo conto di TOAST.)
Erwin Brandstetter,

1
@ErwinBrandstetter Grazie per aver compilato calcoli delle dimensioni delle righe più esigenti. Ho sempre ipotizzato che l'output della stima della larghezza della riga spiegando includesse considerazioni per riga come l'intestazione e la maschera di bit NULL, ma non l'overhead a livello di pagina.
dbenhur,

1
@dbenhur: è possibile eseguire una rapida EXPLAIN ANALYZE SELECT foo from barcon una tabella fittizia di base per verificare. Inoltre, lo spazio su disco effettivo dipende dall'allineamento dei dati, che sarebbe difficile considerare quando vengono recuperate solo alcune righe. La larghezza della riga in EXPLAINrappresenta il requisito di spazio di base per il set di colonne recuperato.
Erwin Brandstetter,

5

Stai recuperando tutte le righe da entrambe le tabelle, quindi non c'è alcun vantaggio reale utilizzando una scansione dell'indice. Una scansione dell'indice ha senso solo se si selezionano solo poche righe da una tabella (in genere meno del 10% -15%)


Sì, hai ragione :) Ho provato a chiarire la situazione con un caso più specifico, vedi l'ultima domanda.
dezso,

@dezso: stessa cosa. Se hai un indice attivo (lezarva, munkalap_id)ed è abbastanza selettivo, allora può essere usato. NOTCiò lo rende meno probabile.
ypercubeᵀᴹ

Ho aggiunto un indice parziale basato sul tuo suggerimento e viene utilizzato, quindi metà del problema è risolto. Ma non mi aspetto che l'indice sulla chiave esterna sia inutile dato che voglio UNIRE contro solo 87 valori rispetto al 3252 originale.
dezso

1
@dezso Le righe hanno una larghezza di 214 byte, quindi avrai un po 'meno di 40 righe per blocco di dati 8K. Anche la selettività dell'indice è di circa 1/40 (1006/38046). Quindi, Pg calcola che leggere tutti i blocchi in sequenza è più economico della probabile lettura di circa lo stesso numero di blocchi in modo casuale quando si utilizza l'indice. Questi compromessi stimati possono essere influenzati dai valori di configurazione efficacia_cache_size e random_page_cost.
dbenhur,

@dbenhur: potresti rendere il tuo commento una risposta adeguata?
dezso,
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.