Quale indice utilizzare con molti valori duplicati?


14

Facciamo alcune ipotesi:

Ho un tavolo che assomiglia a questo:

 a | b
---+---
 a | -1
 a | 17
  ...
 a | 21
 c | 17
 c | -3
  ...
 c | 22

Fatti sul mio set:

  • Le dimensioni dell'intera tabella sono ~ 10 10 righe.

  • Ho ~ 100k righe con valore ain colonna a, simile per altri valori (ad es c.).

  • Ciò significa ~ 100k valori distinti nella colonna 'a'.

  • La maggior parte delle mie query leggerà tutti o la maggior parte dei valori per un dato valore in a, ad es select sum(b) from t where a = 'c'.

  • La tabella è scritta in modo tale che i valori consecutivi siano fisicamente vicini (o è scritto in ordine, o supponiamo che sia CLUSTERstato usato su quella tabella e colonna a).

  • La tabella è raramente se mai aggiornata, ci preoccupiamo solo della velocità di lettura.

  • La tabella è relativamente stretta (diciamo ~ 25 byte per tupla, + 23 byte in testa).

Ora la domanda è: che tipo di indice dovrei usare? La mia comprensione è:

  • BTree Il mio problema qui è che l'indice BTree sarà enorme poiché, per quanto ne so, memorizzerà valori duplicati (è necessario, poiché non può presumere che la tabella sia ordinata fisicamente). Se il BTree è enorme, finisco per leggere sia l'indice che le parti della tabella a cui punta l'indice. (Possiamo usare fillfactor = 100per ridurre un po 'le dimensioni dell'indice.)

  • BRIN La mia comprensione è che qui posso avere un piccolo indice a spese della lettura di pagine inutili. Usare un piccolo pages_per_rangesignifica che l'indice è più grande (il che è un problema con BRIN poiché ho bisogno di leggere l'intero indice), avendo un grande pages_per_rangemezzo che leggerò molte pagine inutili. Esiste una formula magica per trovare un buon valore pages_per_rangeche tenga conto di questi compromessi?

  • GIN / GiST Non sono sicuro che siano rilevanti qui poiché sono usati principalmente per la ricerca full-text, ma ho anche sentito che sono bravi a gestire chiavi duplicate. Sarebbe utile GINo GiSTindicizzare qui?

Un'altra domanda è: Postgres utilizzerà il fatto che una tabella è CLUSTERedita (supponendo che non ci siano aggiornamenti) nel pianificatore di query (ad es. Cercando binariamente le pagine iniziali / finali pertinenti)? In qualche modo correlato, posso semplicemente archiviare tutte le mie colonne in un BTree e eliminare del tutto la tabella (o ottenere qualcosa di equivalente, credo che siano indici cluster in SQL Server)? C'è qualche indice ibrido BTree / BRIN che potrebbe aiutare qui?

Preferirei evitare di usare gli array per memorizzare i miei valori poiché la mia query finirà per essere meno leggibile in questo modo (capisco che questo ridurrebbe il costo dei 23 byte per overup di tupla riducendo il numero di tuple).


"utilizzato principalmente per la ricerca full-text" GiST è ampiamente utilizzato da PostGIS.
jpmc26

Risposte:


15

BTree

Il mio problema qui è che l'indice BTree sarà enorme poiché afaict memorizzerà valori duplicati (ha anche, dal momento che non può presumere che la tabella sia ordinata fisicamente). Se il BTree è enorme finisco per dover leggere sia l'indice che le parti della tabella che anche l'indice indica ...

Non necessariamente - Avere un indice btree che 'copre' sarà il tempo di lettura più veloce, e se questo è tutto ciò che vuoi (cioè se puoi permetterti lo spazio extra), allora è la soluzione migliore.

BRIN

La mia comprensione è che posso avere un piccolo indice qui a spese della lettura di pagine inutili. Usare un piccolo pages_per_rangesignifica che l'indice è più grande (il che è un problema con BRIN poiché ho bisogno di leggere l'intero indice), avendo un grande pages_per_rangemezzo che leggerò molte pagine inutili.

Se non puoi permetterti l'overhead di archiviazione di un indice btree di copertura, BRIN è l'ideale per te, perché hai già un clustering (questo è fondamentale per BRIN per essere utile). Gli indici BRIN sono minuscoli , quindi è probabile che tutte le pagine siano in memoria se si sceglie un valore adatto di pages_per_range.

Esiste una formula magica per trovare un buon valore di pages_per_range che tenga conto di questi compromessi?

Nessuna formula magica, ma inizia con pages_per_range un po 'meno della dimensione media (in pagine) occupata dal avalore medio . Probabilmente stai tentando di ridurre a icona: (numero di pagine BRIN scansionate) + (numero di pagine heap scansionate) per una query tipica. Cercare Heap Blocks: lossy=nnel piano di esecuzione pages_per_range=1e confrontare con altri valori per pages_per_range, ad esempio vedere quanti blocchi heap non necessari vengono sottoposti a scansione.

GIN / GiST

Non sono sicuro che siano rilevanti qui poiché sono utilizzati principalmente per la ricerca full-text, ma ho anche sentito che sono bravi a gestire chiavi duplicate. Uno GIN/ GiSTindice potrebbe aiutare qui?

Vale la pena prendere in considerazione GIN, ma probabilmente non GiST, tuttavia se il clustering naturale è davvero buono, allora BRIN sarà probabilmente una scommessa migliore.

Ecco un confronto di esempio tra i diversi tipi di indice per dati fittizi un po 'come il tuo:

tabella e indici:

create table foo(a,b,c) as
select *, lpad('',20)
from (select chr(g) a from generate_series(97,122) g) a
     cross join (select generate_series(1,100000) b) b
order by a;
create index foo_btree_covering on foo(a,b);
create index foo_btree on foo(a);
create index foo_gin on foo using gin(a);
create index foo_brin_2 on foo using brin(a) with (pages_per_range=2);
create index foo_brin_4 on foo using brin(a) with (pages_per_range=4);
vacuum analyze;

dimensioni della relazione:

select relname "name", pg_size_pretty(siz) "size", siz/8192 pages, (select count(*) from foo)*8192/siz "rows/page"
from( select relname, pg_relation_size(C.oid) siz
      from pg_class c join pg_namespace n on n.oid = c.relnamespace
      where nspname = current_schema ) z;
nome | dimensione | pagine | righe / pagina
: ----------------- | : ------ | ----: | --------:
pippo | 149 MB | 19118 | 135
foo_btree_covering | 56 MB | 7132 | 364
foo_btree | 56 MB | 7132 | 364
foo_gin | 2928 kB | 366 | 7103
foo_brin_2 | 264 kB | 33 | 78.787
foo_brin_4 | 136 kB | 17 | 152941

che copre btree:

explain analyze select sum(b) from foo where a='a';
| PIANO DI QUERY |
| : ------------------------------------------------- -------------------------------------------------- ------------------------------------------- |
| Aggregato (costo = 3282.57..3282.58 righe = 1 larghezza = 8) (tempo effettivo = 45.942..45.942 righe = 1 anelli = 1) |
| -> Solo indice Scansione utilizzando foo_btree_covering su foo (costo = 0.43..3017.80 righe = 105907 larghezza = 4) (tempo effettivo = 0.038..27.286 righe = 100000 loop = 1) |
| Indice cond: (a = 'a' :: testo) |
| Heap Fetches: 0 |
| Tempo di pianificazione: 0,099 ms |
| Tempo di esecuzione: 45.968 ms |

semplice btree:

drop index foo_btree_covering;
explain analyze select sum(b) from foo where a='a';
| PIANO DI QUERY |
| : ------------------------------------------------- -------------------------------------------------- ----------------------------- |
| Aggregato (costo = 4064.57..4064.58 righe = 1 larghezza = 8) (tempo effettivo = 54.242..54.242 righe = 1 anelli = 1) |
| -> Scansione indice usando foo_btree su foo (costo = 0.43..3799.80 righe = 105907 larghezza = 4) (tempo effettivo = 0.037..33.084 righe = 100000 loop = 1) |
| Indice cond: (a = 'a' :: testo) |
| Tempo di pianificazione: 0,135 ms |
| Tempo di esecuzione: 54.280 ms |

BRIN pages_per_range = 4:

drop index foo_btree;
explain analyze select sum(b) from foo where a='a';
| PIANO DI QUERY |
| : ------------------------------------------------- -------------------------------------------------- ----------------------------- |
| Aggregato (costo = 21595.38..21595.39 righe = 1 larghezza = 8) (tempo effettivo = 52.455..52.455 righe = 1 anelli = 1) |
| -> Scansione heap bitmap su foo (costo = 888.78..21330.61 righe = 105907 larghezza = 4) (tempo effettivo = 2.738..31.967 righe = 100000 loop = 1) |
| Ricontrolla Cond: (a = 'a' :: testo) |
| Righe rimosse dall'indice Ricontrolla: 96 |
| Heap Blocks: lossy = 736 |
| -> Scansione indice bitmap su foo_brin_4 (costo = 0,00..862,30 righe = 105907 larghezza = 0) (tempo effettivo = 2.720..2.720 righe = 7360 loop = 1) |
| Indice cond: (a = 'a' :: testo) |
| Tempo di pianificazione: 0.101 ms |
| Tempo di esecuzione: 52.501 ms |

BRIN pages_per_range = 2:

drop index foo_brin_4;
explain analyze select sum(b) from foo where a='a';
| PIANO DI QUERY |
| : ------------------------------------------------- -------------------------------------------------- ----------------------------- |
| Aggregato (costo = 21659.38..21659.39 righe = 1 larghezza = 8) (tempo effettivo = 53.971..53.971 righe = 1 anelli = 1) |
| -> Scansione heap bitmap su foo (costo = 952.78..21394.61 righe = 105907 larghezza = 4) (tempo effettivo = 5.286..33.492 righe = 100000 loop = 1) |
| Ricontrolla Cond: (a = 'a' :: testo) |
| Righe rimosse dall'indice Ricontrolla: 96 |
| Heap Blocks: lossy = 736 |
| -> Scansione indice bitmap su foo_brin_2 (costo = 0,00..926,30 righe = 105907 larghezza = 0) (tempo effettivo = 5,275..5,275 righe = 7360 loop = 1) |
| Indice cond: (a = 'a' :: testo) |
| Tempo di pianificazione: 0,095 ms |
| Tempo di esecuzione: 54.016 ms |

GIN:

drop index foo_brin_2;
explain analyze select sum(b) from foo where a='a';
| PIANO DI QUERY |
| : ------------------------------------------------- -------------------------------------------------- ------------------------------ |
| Aggregato (costo = 21687.38..21687.39 righe = 1 larghezza = 8) (tempo effettivo = 55.331..55.331 righe = 1 anelli = 1) |
| -> Scansione heap bitmap su foo (costo = 980.78..21422.61 righe = 105907 larghezza = 4) (tempo effettivo = 12.377..33.956 righe = 100000 loop = 1) |
| Ricontrolla Cond: (a = 'a' :: testo) |
| Blocchi heap: esatto = 736 |
| -> Scansione indice bitmap su foo_gin (costo = 0,00..954,30 righe = 105907 larghezza = 0) (tempo effettivo = 12.271..12.271 righe = 100000 loop = 1) |
| Indice cond: (a = 'a' :: testo) |
| Tempo di pianificazione: 0,118 ms |
| Tempo di esecuzione: 55.366 ms |

dbfiddle qui


Quindi un indice di copertura salterebbe a leggere del tutto la tabella a spese dello spazio su disco? Sembra un buon compromesso. Penso che intendiamo la stessa cosa per l'indice BRIN con "leggi l'intero indice" (correggimi se sbaglio), intendevo scansionare l'intero indice BRIN che penso sia ciò che sta accadendo in dbfiddle.uk/… , no?
Foo

@foo about "(lo è anche perché non può assumere che la tabella sia ordinata fisicamente)." L'ordine fisico (cluster o no) della tabella è irrilevante. L'indice ha i valori nell'ordine giusto. Ma gli indici B-tree di Postgres devono memorizzare tutti i valori (e sì, più volte). Ecco come sono progettati. Memorizzare ogni valore distinto una sola volta sarebbe una bella caratteristica / miglioramento. Potresti suggerirlo agli sviluppatori di Postgres (e persino aiutare a implementarlo.) Jack dovrebbe commentare, penso che l'implementazione di Oracle di b-tree lo faccia.
ypercubeᵀᴹ

1
@foo - hai completamente ragione, una scansione di un indice BRIN esegue sempre la scansione dell'intero indice ( pgcon.org/2016/schedule/attachments/… , 2a ultima diapositiva) - sebbene ciò non sia mostrato sul piano esplicativo nel violino , è?
Jack dice di provare topanswers.xyz il

2
@ ypercubeᵀᴹ puoi usare COMPRESS su Oracle che memorizza ogni prefisso distinto una volta per blocco.
Jack dice di provare topanswers.xyz il

@JackDouglas Ho letto Bitmap Index Scanche significa "leggi tutto l'indice di brin", ma forse è una lettura sbagliata. Oracle COMPRESSsembra qualcosa che sarebbe utile qui dal momento che ridurrebbe le dimensioni dell'albero B, ma sono bloccato con pg!
fo

6

Oltre btree e Brin , che sembrano le più opzioni ragionevoli, alcune altre opzioni esotiche, che potrebbe essere la pena indagare - potrebbero utile o meno nel tuo caso:

  • INCLUDEindici . Saranno - si spera - nella prossima versione principale (10) di Postgres, da qualche parte intorno a settembre 2017. Un indice attivo (a) INCLUDE (b)ha la stessa struttura di un indice attivo (a)ma include nelle pagine foglia tutti i valori di b(ma non ordinati). Il che significa che non puoi usarlo per esempio per SELECT * FROM t WHERE a = 'a' AND b = 2 ;. L'indice potrebbe essere usato ma mentre un (a,b)indice troverà le righe corrispondenti con una sola ricerca, l'indice di inclusione dovrà passare attraverso i valori (possibilmente 100K come nel tuo caso) che corrispondono a = 'a'e controllano i bvalori.
    D'altro canto, l'indice è leggermente meno ampio (a,b)dell'indice e non è necessario che l'ordine sia bcalcolato per la query SUM(b). Potresti anche avere ad esempio(a) INCLUDE (b,c,d) che può essere utilizzato per query simili alle tue che si aggregano su tutte e 3 le colonne.

  • Indici (parziali) filtrati . Un suggerimento che potrebbe sembrare un po 'folle * all'inizio:

    CREATE INDEX flt_a  ON t (b) WHERE (a = 'a') ;
    ---
    CREATE INDEX flt_xy ON t (b) WHERE (a = 'xy') ;

    Un indice per ogni avalore. Nel tuo caso circa 100.000 indici. Anche se questo suona molto, considera che ogni indice sarà molto piccolo, sia in termini di dimensioni (numero di righe) che di larghezza (poiché memorizzerà solo i bvalori). In tutti gli altri aspetti, tuttavia (gli indici da 100K insieme) fungerà da indice b-tree (a,b)mentre utilizza lo spazio di un (b)indice.
    Lo svantaggio è che dovrai crearli e mantenerli tu stesso, ogni volta che un nuovo valore di aviene aggiunto nella tabella. Poiché la tua tabella è piuttosto stabile, senza molti (o nessun) inserimenti / aggiornamenti, questo non sembra un problema.

  • Tabelle riassuntive. Poiché la tabella è piuttosto stabile, è sempre possibile creare e popolare una tabella di riepilogo con gli aggregati più comuni necessari ( sum(b), sum(c), sum(d), avg(b), count(distinct b), ecc.). Sarà piccolo (solo 100.000 righe) e dovrà essere popolato una sola volta e aggiornato solo quando le righe vengono inserite / aggiornate / eliminate nella tabella principale.

*: idea copiata da questa azienda che gestisce 10 milioni di indici nel loro sistema di produzione: The Heap: esecuzione di 10 milioni di indici Postgresql in produzione (e conteggio) .


1 è interessante ma, come fai notare, la pagina 10 non è ancora disponibile. 2 fa rumore folle (o almeno contro 'senso comune'), avrò una lettura in quanto come fai notare che potrebbe lavorare con il mio quasi nessuno scrive il flusso di lavoro. 3. non avrebbe funzionato per me, che ho usato SUMcome esempio, ma in pratica le mie domande non possono essere precalcolate (sono più simili select ... from t where a = '?' and ??wjere ??sarebbe qualche altra condizione definita dall'utente.
pippo

1
Bene, non possiamo aiutarti se non sappiamo cosa ??sia;)
ypercubeᵀᴹ

Lei menziona gli indici filtrati. Che dire del partizionamento della tabella?
jpmc26

@ jpmc26 divertente, stavo pensando di aggiungere nella risposta che il suggerimento di indici filtrati è in un certo senso una forma di partizionamento. Anche il partizionamento potrebbe essere utile qui, ma non ne sono sicuro. Ciò comporterebbe molti piccoli indici / tabelle.
ypercubeᵀᴹ

2
Mi aspetto che la copertura parziale degli indici btree sia il re delle prestazioni qui, poiché i dati non vengono quasi mai aggiornati. Anche se ciò significa 100.000 indici. La dimensione totale dell'indice è la più piccola (ad eccezione di un indice BRIN, ma in questo caso Postgres deve leggere e filtrare ulteriormente le pagine dell'heap). La generazione di indici può essere automatizzata con SQL dinamico. Dichiarazione di esempio DOin questa risposta correlata .
Erwin Brandstetter,
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.