Enorme discrepanza tra dimensione dell'indice segnalata e numero di buffer nel piano di esecuzione


10

Il problema

Abbiamo una domanda come

SELECT COUNT(1) 
  FROM article
  JOIN reservation ON a_id = r_article_id 
 WHERE r_last_modified < now() - '8 weeks'::interval 
   AND r_group_id = 1 
   AND r_status = 'OPEN';

Dato che si verifica un timeout (dopo 10 minuti) il più delle volte, ho deciso di indagare sul problema.

L' EXPLAIN (ANALYZE, BUFFERS)output è simile al seguente:

 Aggregate  (cost=264775.48..264775.49 rows=1 width=0) (actual time=238960.290..238960.291 rows=1 loops=1)
   Buffers: shared hit=200483 read=64361 dirtied=666 written=8, temp read=3631 written=3617
   I/O Timings: read=169806.955 write=0.154
   ->  Hash Join  (cost=52413.67..264647.65 rows=51130 width=0) (actual time=1845.483..238957.588 rows=21644 loops=1)
         Hash Cond: (reservation.r_article_id = article.a_id)
         Buffers: shared hit=200483 read=64361 dirtied=666 written=8, temp read=3631 written=3617
         I/O Timings: read=169806.955 write=0.154
         ->  Index Scan using reservation_r_article_id_idx1 on reservation  (cost=0.42..205458.72 rows=51130 width=4) (actual time=34.035..237000.197 rows=21644 loops=1)
               Filter: ((r_group_id = 1) AND (r_status = 'OPEN') AND (r_last_modified < (now() - '56 days'::interval)))
               Rows Removed by Filter: 151549
               Buffers: shared hit=200193 read=48853 dirtied=450 written=8
               I/O Timings: read=168614.105 write=0.154
         ->  Hash  (cost=29662.22..29662.22 rows=1386722 width=4) (actual time=1749.392..1749.392 rows=1386814 loops=1)
               Buckets: 32768  Batches: 8  Memory Usage: 6109kB
               Buffers: shared hit=287 read=15508 dirtied=216, temp written=3551
               I/O Timings: read=1192.850
               ->  Seq Scan on article  (cost=0.00..29662.22 rows=1386722 width=4) (actual time=23.822..1439.310 rows=1386814 loops=1)
                     Buffers: shared hit=287 read=15508 dirtied=216
                     I/O Timings: read=1192.850
 Total runtime: 238961.812 ms

Il nodo del collo di bottiglia è ovviamente la scansione dell'indice. Quindi vediamo la definizione dell'indice:

CREATE INDEX reservation_r_article_id_idx1 
    ON reservation USING btree (r_article_id)
 WHERE (r_status <> ALL (ARRAY['FULFILLED', 'CLOSED', 'CANCELED']));

Dimensioni e numeri di riga

La sua dimensione (riportata da \di+o visitando il file fisico) è di 36 MB. Dato che le prenotazioni di solito trascorrono solo un tempo relativamente breve in tutti gli stati non elencati sopra, si verificano molti aggiornamenti, quindi l'indice è piuttosto gonfio (qui vengono sprecati circa 24 MB) - tuttavia, la dimensione è relativamente piccola.

La reservationtabella ha una dimensione di circa 3,8 GB, contenente circa 40 milioni di righe. Il numero di prenotazioni non ancora chiuse è di circa 170.000 (il numero esatto è riportato nel nodo di scansione indice sopra).

Ora la sorpresa: la scansione dell'indice riporta il recupero di enormi quantità di buffer (ovvero pagine da 8 kb):

Buffers: shared hit=200193 read=48853 dirtied=450 written=8

I numeri letti dalla cache e dal disco (o dalla cache del sistema operativo) si sommano a 1,9 GB!

Nella peggiore delle ipotesi

D'altra parte, lo scenario peggiore, quando ogni tupla si trova su una pagina diversa della tabella, rappresenterebbe la visita (21644 + 151549) + 4608 pagine (righe totali recuperate dalla tabella più il numero di pagina dell'indice dal fisico taglia). Questo è ancora solo sotto i 180.000 - molto al di sotto dei quasi 250.000 osservati.

Interessante (e forse importante) è che la velocità di lettura del disco è di circa 2,2 MB / s, il che è abbastanza normale, immagino.

E allora?

Qualcuno ha idea di da dove possa venire questa discrepanza?

Nota: per essere chiari, abbiamo idee su cosa migliorare / cambiare qui, ma mi piacerebbe davvero capire i numeri che ho ottenuto: ecco di cosa si tratta.

Aggiornamento: controllo dell'effetto della memorizzazione nella cache o del microvacuuming

Sulla base della risposta di jjanes , ho verificato cosa succede quando eseguo di nuovo esattamente la stessa query. Il numero di buffer interessati non cambia davvero. (Per fare questo, ho semplificato la query al minimo indispensabile che mostra ancora il problema.) Questo è quello che vedo dalla prima esecuzione:

 Aggregate  (cost=240541.52..240541.53 rows=1 width=0) (actual time=97703.589..97703.590 rows=1 loops=1)
   Buffers: shared hit=413981 read=46977 dirtied=56
   I/O Timings: read=96807.444
   ->  Index Scan using reservation_r_article_id_idx1 on reservation  (cost=0.42..240380.54 rows=64392 width=0) (actual time=13.757..97698.461 rows=19236 loops=1)
         Filter: ((r_group_id = 1) AND (r_status = 'OPEN') AND (r_last_modified < (now() - '56 days'::interval)))
         Rows Removed by Filter: 232481
         Buffers: shared hit=413981 read=46977 dirtied=56
         I/O Timings: read=96807.444
 Total runtime: 97703.694 ms

e dopo il secondo:

 Aggregate  (cost=240543.26..240543.27 rows=1 width=0) (actual time=388.123..388.124 rows=1 loops=1)
   Buffers: shared hit=460990
   ->  Index Scan using reservation_r_article_id_idx1 on reservation  (cost=0.42..240382.28 rows=64392 width=0) (actual time=0.032..385.900 rows=19236 loops=1)
         Filter: ((r_group_id = 1) AND (r_status = 'OPEN') AND (r_last_modified < (now() - '56 days'::interval)))
         Rows Removed by Filter: 232584
         Buffers: shared hit=460990
 Total runtime: 388.187 ms

1
Probabilmente irrilevante ma hai bisogno di unirti a article? Sembra che tutte le colonne coinvolte siano dalla reservationtabella e (supponendo) che ci sia un FK, il risultato dovrebbe essere lo stesso.
ypercubeᵀᴹ

Questa è un'ottima domanda. E hai ragione, non è necessario: questa è una query utilizzata nel monitoraggio da un altro team. Tuttavia, almeno guardando il piano di query, tutto il resto è solo una decorazione per quella brutta scansione dell'indice :)
dezso

1
Consentitemi di aggiungere che la rimozione del join non fa una grande differenza: la scansione dell'indice esagerata rimane lì.
dezso,

Toast accesso alla tabella? Anche se dubito che una qualsiasi delle colonne che mostri sia tostata. Se si dispone di un clone inattivo del database a scopo di test, è possibile eseguirlo pg_stat_reset(), quindi eseguire la query e quindi pg_statio_user_tablescontrollare dove attribuisce i blocchi.
jjanes,

Risposte:


4

Penso che la chiave qui sia il sacco di aggiornamenti e il gonfio sull'indice.

L'indice contiene puntatori a righe nella tabella che non sono più "attive". Queste sono le vecchie versioni delle righe aggiornate. Le vecchie versioni di riga vengono conservate per un po ', per soddisfare le query con una vecchia istantanea, e quindi conservate per un po' di più perché nessuno vuole fare il lavoro di rimuoverle più spesso del necessario.

Durante la scansione dell'indice, deve andare a visitare queste righe e quindi nota che non sono più visibili, quindi le ignora. La explain (analyze,buffers)dichiarazione non riporta esplicitamente su questa attività, tranne attraverso il conteggio dei buffer letti / colpiti nel processo di ispezione di queste righe.

Esiste un codice "microvacuum" per btrees, in modo tale che quando la scansione torna di nuovo all'indice, ricorda che il puntatore che ha inseguito non era più attivo e lo contrassegna come morto nell'indice. In questo modo la successiva query simile che viene eseguita non ha bisogno di inseguirla di nuovo. Quindi, se esegui di nuovo la stessa identica query, probabilmente vedrai che gli accessi al buffer si avvicinano a ciò che hai previsto.

Puoi anche VACUUMla tabella più spesso, che pulirà le tuple morte fuori dalla tabella stessa, non solo dall'indice parziale. In generale, è probabile che le tabelle con un indice parziale a rotazione elevata beneficino di un vuoto più aggressivo rispetto al livello predefinito.


Per favore, vedi la mia modifica - a me sembra una cache, non un microvacuuming.
dezso,

I tuoi nuovi numeri sono molto diversi da quelli precedenti (circa il doppio), quindi è difficile interpretare ciò che significano senza vedere anche i nuovi numeri per le righe effettive e le righe filtrate per la scansione dell'indice.
jjanes,

Aggiunti i piani completi come sembrano oggi. Il numero di buffer interessato è cresciuto molto da venerdì, così come il conteggio delle righe.
dezso

Hai transazioni di lunga durata in giro? In tal caso, è possibile che la scansione dell'indice continui a rintracciare le righe che non sono visibili ad esso (che causa gli accessi al buffer extra), ma non può microvaccarli via perché potrebbero essere visibili a qualcun altro con un vecchio snapshot.
jjanes,

Non ne ho nessuna: la transazione tipica richiede meno di un secondo. Occasionalmente qualche secondo, ma non più a lungo.
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.