PostgreSQL
 sql >> Database >  >> RDS >> PostgreSQL

Sull'utilità degli indici di espressione

Quando insegno corsi di formazione su PostgreSQL, sia su argomenti di base che avanzati, spesso scopro che i partecipanti non hanno idea di quanto possano essere potenti gli indici di espressione (se ne sono a conoscenza). Lascia che ti dia una breve panoramica.

Quindi, supponiamo di avere una tabella, con un intervallo di timestamp (sì, abbiamo la funzione generate_series che può generare date):

CREATE TABLE t AS
SELECT d, repeat(md5(d::text), 10) AS padding
  FROM generate_series(timestamp '1900-01-01',
                       timestamp '2100-01-01',
                       interval '1 day') s(d);
VACUUM ANALYZE t;

La tabella include anche una colonna di riempimento, per renderla un po' più grande. Ora, eseguiamo una semplice query di intervallo, selezionando solo un mese dai circa 200 anni inclusi nella tabella. Se spieghi la query, vedrai qualcosa di simile a questo:

EXPLAIN SELECT * FROM t WHERE d BETWEEN '2001-01-01' AND '2001-02-01';

                               QUERY PLAN
------------------------------------------------------------------------
 Seq Scan on t  (cost=0.00..4416.75 rows=32 width=332)
   Filter: ((d >= '2001-01-01 00:00:00'::timestamp without time zone)
        AND (d <= '2001-02-01 00:00:00'::timestamp without time zone))
(2 rows)

e sul mio laptop, questo viene eseguito in ~ 20 ms. Non male, considerando che questo deve attraversare l'intera tabella con circa 75.000 righe.

Ma creiamo un indice sulla colonna timestamp (tutti gli indici qui sono il tipo predefinito, cioè btree, a meno che non sia menzionato esplicitamente):

CREATE INDEX idx_t_d ON t (d);

E ora proviamo a eseguire nuovamente la query:

                               QUERY PLAN
------------------------------------------------------------------------
 Index Scan using idx_t_d on t  (cost=0.29..9.97 rows=34 width=332)
   Index Cond: ((d >= '2001-01-01 00:00:00'::timestamp without time zone)
            AND (d <= '2001-02-01 00:00:00'::timestamp without time zone))
(2 rows)

e questo viene eseguito in 0,5 ms, quindi circa 40 volte più veloce. Ma quello era ovviamente un semplice indice, creato direttamente sulla colonna, non un indice di espressione. Supponiamo quindi di dover selezionare i dati da ogni 1° giorno di ogni mese, eseguendo una query come questa

SELECT * FROM t WHERE EXTRACT(day FROM d) = 1;

che però non può utilizzare l'indice, in quanto deve valutare un'espressione sulla colonna mentre l'indice è costruito sulla colonna stessa, come mostrato in EXPLAIN ANALYZE:

                               QUERY PLAN
------------------------------------------------------------------------
 Seq Scan on t  (cost=0.00..4416.75 rows=365 width=332)
                (actual time=0.045..40.601 rows=2401 loops=1)
   Filter: (date_part('day'::text, d) = '1'::double precision)
   Rows Removed by Filter: 70649
 Planning time: 0.209 ms
 Execution time: 43.018 ms
(5 rows)

Quindi non solo questo deve eseguire una scansione sequenziale, ma deve anche eseguire la valutazione, aumentando la durata della query a 43 ms.

Il database non è in grado di utilizzare l'indice per diversi motivi. Gli indici (almeno btree indexes) si basano sull'interrogazione di dati ordinati, forniti dalla struttura ad albero, e mentre la query di intervallo può trarne vantaggio, la seconda query (con la chiamata `extract`) non può.

Nota:un altro problema è che l'insieme di operatori supportati dagli indici (cioè che possono essere valutati direttamente sugli indici) è molto limitato. E la funzione "estrai" non è supportata, quindi la query non può aggirare il problema dell'ordine utilizzando una scansione dell'indice bitmap.

In teoria il database potrebbe tentare di trasformare la condizione in condizioni di intervallo, ma è estremamente difficile e specifico per l'espressione. In questo caso dovremmo generare un numero infinito di tali intervalli "al giorno", perché il pianificatore non conosce realmente i timestamp min/max nella tabella. Quindi il database non ci prova nemmeno.

Ma mentre il database non sa come trasformare le condizioni, gli sviluppatori spesso lo fanno. Ad esempio con condizioni come

(column + 1) >= 1000

non è difficile riscriverlo così

column >= (1000 - 1)

che funziona bene con gli indici.

Ma cosa succede se tale trasformazione non è possibile, come ad esempio per la query di esempio

SELECT * FROM t WHERE EXTRACT(day FROM d) = 1;

In questo caso lo sviluppatore dovrebbe affrontare lo stesso problema con min/max sconosciuto per la colonna d, e anche in questo caso genererebbe molti intervalli.

Bene, questo post sul blog riguarda gli indici delle espressioni e finora abbiamo utilizzato solo indici regolari, costruiti direttamente sulla colonna. Quindi, creiamo il primo indice di espressione:

CREATE INDEX idx_t_expr ON t ((extract(day FROM d)));
ANALYZE t;

che poi ci dà questo piano di spiegazione

                               QUERY PLAN
------------------------------------------------------------------------
 Bitmap Heap Scan on t  (cost=47.35..3305.25 rows=2459 width=332)
                        (actual time=2.400..12.539 rows=2401 loops=1)
   Recheck Cond: (date_part('day'::text, d) = '1'::double precision)
   Heap Blocks: exact=2401
   ->  Bitmap Index Scan on idx_t_expr  (cost=0.00..46.73 rows=2459 width=0)
                                (actual time=1.243..1.243 rows=2401 loops=1)
         Index Cond: (date_part('day'::text, d) = '1'::double precision)
 Planning time: 0.374 ms
 Execution time: 17.136 ms
(7 rows)

Quindi, sebbene questo non ci dia la stessa velocità di 40 volte dell'indice nel primo esempio, è previsto in quanto questa query restituisce molte più tuple (2401 contro 32). Inoltre quelli sono sparsi per l'intera tabella e non così localizzati come nel primo esempio. Quindi è un bel 2x accelerazione e in molti casi del mondo reale vedrai miglioramenti molto più grandi.

Ma la possibilità di utilizzare gli indici per le condizioni con espressioni complesse non è l'informazione più interessante qui – questo è il motivo per cui le persone creano indici di espressioni. Ma non è l'unico vantaggio.

Se guardi i due piani di spiegazione presentati sopra (senza e con l'indice di espressione), potresti notare questo:

                               QUERY PLAN
------------------------------------------------------------------------
 Seq Scan on t  (cost=0.00..4416.75 rows=365 width=332)
                (actual time=0.045..40.601 rows=2401 loops=1)
 ...
                               QUERY PLAN
------------------------------------------------------------------------
 Bitmap Heap Scan on t  (cost=47.35..3305.25 rows=2459 width=332)
                        (actual time=2.400..12.539 rows=2401 loops=1)
 ...

Destra:la creazione dell'indice di espressione ha migliorato significativamente le stime. Senza l'indice abbiamo solo le statistiche (MCV + istogramma) per le colonne della tabella grezza, quindi il database non sa come stimare l'espressione

EXTRACT(day FROM d) = 1

Quindi applica invece una stima predefinita per le condizioni di uguaglianza, che è lo 0,5% di tutte le righe:poiché la tabella ha 73050 righe, si ottiene una stima di sole 365 righe. È comune vedere errori di stima molto peggiori nelle applicazioni del mondo reale.

Con l'indice, invece, il database raccoglieva anche statistiche sulle colonne dell'indice, e in questo caso la colonna contiene i risultati dell'espressione. E durante la pianificazione, l'ottimizzatore lo nota e produce una stima molto migliore.

Questo è un enorme vantaggio e può aiutare a correggere alcuni casi di piani di query scadenti causati da stime imprecise. Eppure la maggior parte delle persone non è a conoscenza di questo pratico strumento.

E l'utilità di questo strumento è aumentata solo con l'introduzione del tipo di dati JSONB in ​​9.4, perché è l'unico modo per raccogliere statistiche sui contenuti dei documenti JSONB.

Quando si indicizzano i documenti JSONB, esistono due strategie di indicizzazione di base. Puoi creare un indice GIN/GiST sull'intero documento, ad es. così

CREATE INDEX ON t USING GIN (jsonb_column);

che ti consente di interrogare percorsi arbitrari nella colonna JSONB, utilizzare l'operatore di contenimento per abbinare i documenti secondari, ecc. È fantastico, ma hai ancora solo le statistiche di base per colonna, che
non sono molto utili come i documenti sono trattati come valori scalari (e nessuno trova interi documenti o utilizza un intervallo di documenti).

Indici di espressioni, ad esempio creati in questo modo:

CREATE INDEX ON t ((jsonb_column->'id'));

sarà utile solo per l'espressione particolare, cioè questo indice appena creato sarà utile per

SELECT * FROM t WHERE jsonb_column ->> 'id' = 123;

ma non per le query che accedono ad altre chiavi JSON, come ad esempio "value"

SELECT * FROM t WHERE jsonb_column ->> 'value' = 'xxxx';

Questo non vuol dire che gli indici GIN/GiST sull'intero documento siano inutili, ma devi scegliere. O crei un indice di espressione mirato, utile quando si interroga una chiave particolare e con l'ulteriore vantaggio delle statistiche sull'espressione. Oppure crei un indice GIN/GiST sull'intero documento, in grado di gestire query su chiavi arbitrarie, ma senza le statistiche.

Comunque puoi avere una torta e mangiarla anche in questo caso, perché puoi creare entrambi gli indici contemporaneamente e il database sceglierà quale di essi utilizzare per le singole query. E avrai statistiche accurate, grazie agli indici delle espressioni.

Purtroppo, non puoi mangiare l'intera torta, perché gli indici di espressione e gli indici GIN/GiST utilizzano condizioni diverse

-- expression (btree)
SELECT * FROM t WHERE jsonb_column ->> 'id' = 123;

-- GIN/GiST
SELECT * FROM t WHERE jsonb_column @> '{"id" : 123}';

quindi il pianificatore non può usarli contemporaneamente:indici di espressione per la stima e GIN/GiST per l'esecuzione.