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

Ottieni il massimo dai tuoi indici PostgreSQL

Nel mondo Postgres, gli indici sono essenziali per navigare in modo efficiente nell'archivio tabledata (noto anche come "heap"). Postgres non mantiene un clustering per l'heap e l'architettura MVCC porta a più versioni dello stesso tuple in giro. Creare e mantenere indici efficaci ed efficienti per supportare le applicazioni è un'abilità essenziale.

Continua a leggere per scoprire alcuni suggerimenti sull'ottimizzazione e il miglioramento dell'uso degli indici nella tua distribuzione.

Nota:le query mostrate di seguito vengono eseguite su un database di esempio paginagila non modificato.

Utilizza gli indici di copertura

Considera una query per recuperare le email di tutti i clienti inattivi. Il cliente la tabella ha un attivo colonna e la query è semplice:

pagila=# EXPLAIN SELECT email FROM customer WHERE active=0;
                        QUERY PLAN
-----------------------------------------------------------
 Seq Scan on customer  (cost=0.00..16.49 rows=15 width=32)
   Filter: (active = 0)
(2 rows)

La query richiede una scansione sequenziale completa della tabella cliente. Creiamo un indice sulla colonna attiva:

pagila=# CREATE INDEX idx_cust1 ON customer(active);
CREATE INDEX
pagila=# EXPLAIN SELECT email FROM customer WHERE active=0;
                                 QUERY PLAN
-----------------------------------------------------------------------------
 Index Scan using idx_cust1 on customer  (cost=0.28..12.29 rows=15 width=32)
   Index Cond: (active = 0)
(2 rows)

Questo aiuta e la scansione sequenziale è diventata una "scansione dell'indice". Ciò significa che Postgres eseguirà la scansione dell'indice "idx_cust1", quindi cercherà ulteriormente lo sheap della tabella per leggere gli altri valori delle colonne (in questo caso, l'e-mail colonna) di cui la query ha bisogno.

PostgreSQL 11 introdotto la copertura degli indici. Questa funzione ti consente di includere una o più colonne aggiuntive nell'indice stesso, ovvero i valori di queste colonne aggiuntive vengono archiviati nell'archivio dati dell'indice.

Se dovessimo utilizzare questa funzione e includere il valore dell'e-mail all'interno dell'indice, Postgres non avrà bisogno di esaminare l'heap della tabella per ottenere il valore dell'e-mail . Vediamo se funziona:

pagila=# CREATE INDEX idx_cust2 ON customer(active) INCLUDE (email);
CREATE INDEX
pagila=# EXPLAIN SELECT email FROM customer WHERE active=0;
                                    QUERY PLAN
----------------------------------------------------------------------------------
 Index Only Scan using idx_cust2 on customer  (cost=0.28..12.29 rows=15 width=32)
   Index Cond: (active = 0)
(2 rows)

L'"Index Only Scan" ci dice che la query è ora completamente soddisfatta dall'indice stesso, evitando così potenzialmente tutto l'I/O del disco per leggere l'heap della tabella.

Gli indici di copertura sono disponibili solo per gli indici B-Tree a partire da ora. Inoltre, il costo del mantenimento di un indice di copertura è naturalmente superiore a quello normale.

Utilizza indici parziali

Gli indici parziali indicizzano solo un sottoinsieme delle righe in una tabella. Ciò mantiene gli indici di dimensioni inferiori e una scansione più veloce.

Supponiamo di dover ottenere l'elenco delle e-mail dei clienti con sede in California. La query è:

SELECT c.email FROM customer c
JOIN address a ON c.address_id = a.address_id
WHERE a.district = 'California';

che ha un piano di query che prevede la scansione di entrambe le tabelle unite:

pagila=# EXPLAIN SELECT c.email FROM customer c
pagila-# JOIN address a ON c.address_id = a.address_id
pagila-# WHERE a.district = 'California';
                              QUERY PLAN
----------------------------------------------------------------------
 Hash Join  (cost=15.65..32.22 rows=9 width=32)
   Hash Cond: (c.address_id = a.address_id)
   ->  Seq Scan on customer c  (cost=0.00..14.99 rows=599 width=34)
   ->  Hash  (cost=15.54..15.54 rows=9 width=4)
         ->  Seq Scan on address a  (cost=0.00..15.54 rows=9 width=4)
               Filter: (district = 'California'::text)
(6 rows)

Vediamo cosa ci offre un indice normale:

pagila=# CREATE INDEX idx_address1 ON address(district);
CREATE INDEX
pagila=# EXPLAIN SELECT c.email FROM customer c
pagila-# JOIN address a ON c.address_id = a.address_id
pagila-# WHERE a.district = 'California';
                                      QUERY PLAN
---------------------------------------------------------------------------------------
 Hash Join  (cost=12.98..29.55 rows=9 width=32)
   Hash Cond: (c.address_id = a.address_id)
   ->  Seq Scan on customer c  (cost=0.00..14.99 rows=599 width=34)
   ->  Hash  (cost=12.87..12.87 rows=9 width=4)
         ->  Bitmap Heap Scan on address a  (cost=4.34..12.87 rows=9 width=4)
               Recheck Cond: (district = 'California'::text)
               ->  Bitmap Index Scan on idx_address1  (cost=0.00..4.34 rows=9 width=0)
                     Index Cond: (district = 'California'::text)
(8 rows)

La scansione dell'indirizzo è stato sostituito con una scansione dell'indice su idx_address1 e una scansione dell'heap degli indirizzi.

Supponendo che si tratti di una query frequente e che debba essere ottimizzata, possiamo utilizzare l'indice parziale che indicizza solo le righe di indirizzi in cui il distretto è "California":

pagila=# CREATE INDEX idx_address2 ON address(address_id) WHERE district='California';
CREATE INDEX
pagila=# EXPLAIN SELECT c.email FROM customer c
pagila-# JOIN address a ON c.address_id = a.address_id
pagila-# WHERE a.district = 'California';
                                           QUERY PLAN
------------------------------------------------------------------------------------------------
 Hash Join  (cost=12.38..28.96 rows=9 width=32)
   Hash Cond: (c.address_id = a.address_id)
   ->  Seq Scan on customer c  (cost=0.00..14.99 rows=599 width=34)
   ->  Hash  (cost=12.27..12.27 rows=9 width=4)
         ->  Index Only Scan using idx_address2 on address a  (cost=0.14..12.27 rows=9 width=4)
(5 rows)

La query ora legge solo l'indice idx_address2 e non tocca l'indirizzo della tabella .

Utilizza indici multivalore

Alcune colonne che richiedono l'indicizzazione potrebbero non avere un tipo di dati scalare. Tipi di colonna come jsonb , array e tsvector hanno valori composti o multipli. Se è necessario indicizzare tali colonne, di solito è necessario cercare anche i singoli valori in quelle colonne.

Proviamo a trovare tutti i titoli dei film che includono outtake dietro le quinte. Il film table ha una colonna di array di testo chiamata special_features , che include l'elemento dell'array di testo Dietro le quinte se un film ha quella caratteristica. Per trovare tutti questi film, dobbiamo selezionare tutte le righe che hanno "Dietro le quinte" inqualsiasi dei valori dell'array special_features :

SELECT title FROM film WHERE special_features @> '{"Behind The Scenes"}';

L'operatore di contenimento @> controlla se il lato sinistro è un superset del lato destro.

Ecco il piano di query:

pagila=# EXPLAIN SELECT title FROM film
pagila-# WHERE special_features @> '{"Behind The Scenes"}';
                           QUERY PLAN
-----------------------------------------------------------------
 Seq Scan on film  (cost=0.00..67.50 rows=5 width=15)
   Filter: (special_features @> '{"Behind The Scenes"}'::text[])
(2 rows)

che richiede una scansione completa dell'heap, al costo di 67.

Vediamo se un normale indice B-Tree aiuta:

pagila=# CREATE INDEX idx_film1 ON film(special_features);
CREATE INDEX
pagila=# EXPLAIN SELECT title FROM film
pagila-# WHERE special_features @> '{"Behind The Scenes"}';
                           QUERY PLAN
-----------------------------------------------------------------
 Seq Scan on film  (cost=0.00..67.50 rows=5 width=15)
   Filter: (special_features @> '{"Behind The Scenes"}'::text[])
(2 rows)

L'indice non è nemmeno considerato. L'indice B-Tree non ha idea della presenza di singoli elementi nel valore che ha indicizzato.

Quello di cui abbiamo bisogno è un indice GIN.

pagila=# CREATE INDEX idx_film2 ON film USING GIN(special_features);
CREATE INDEX
pagila=# EXPLAIN SELECT title FROM film
pagila-# WHERE special_features @> '{"Behind The Scenes"}';
                                QUERY PLAN
---------------------------------------------------------------------------
 Bitmap Heap Scan on film  (cost=8.04..23.58 rows=5 width=15)
   Recheck Cond: (special_features @> '{"Behind The Scenes"}'::text[])
   ->  Bitmap Index Scan on idx_film2  (cost=0.00..8.04 rows=5 width=0)
         Index Cond: (special_features @> '{"Behind The Scenes"}'::text[])
(4 rows)

L'indice GIN è in grado di supportare la corrispondenza del valore individuale con il valore composito indicizzato, risultando in un piano di query con meno della metà del costo dell'originale.

Elimina gli indici duplicati

Nel tempo gli indici si accumulano e talvolta ne viene aggiunto uno che ha la stessa definizione di un altro. Puoi utilizzare la vista catalogo pg_indexes per ottenere le definizioni SQL leggibili dagli indici. Puoi anche rilevare facilmente definizioni identiche:

  SELECT array_agg(indexname) AS indexes, replace(indexdef, indexname, '') AS defn
    FROM pg_indexes
GROUP BY defn
  HAVING count(*) > 1;

Ed ecco il risultato quando viene eseguito sul database di stock pagila:

pagila=#   SELECT array_agg(indexname) AS indexes, replace(indexdef, indexname, '') AS defn
pagila-#     FROM pg_indexes
pagila-# GROUP BY defn
pagila-#   HAVING count(*) > 1;
                                indexes                                 |                                defn
------------------------------------------------------------------------+------------------------------------------------------------------
 {payment_p2017_01_customer_id_idx,idx_fk_payment_p2017_01_customer_id} | CREATE INDEX  ON public.payment_p2017_01 USING btree (customer_id
 {payment_p2017_02_customer_id_idx,idx_fk_payment_p2017_02_customer_id} | CREATE INDEX  ON public.payment_p2017_02 USING btree (customer_id
 {payment_p2017_03_customer_id_idx,idx_fk_payment_p2017_03_customer_id} | CREATE INDEX  ON public.payment_p2017_03 USING btree (customer_id
 {idx_fk_payment_p2017_04_customer_id,payment_p2017_04_customer_id_idx} | CREATE INDEX  ON public.payment_p2017_04 USING btree (customer_id
 {payment_p2017_05_customer_id_idx,idx_fk_payment_p2017_05_customer_id} | CREATE INDEX  ON public.payment_p2017_05 USING btree (customer_id
 {idx_fk_payment_p2017_06_customer_id,payment_p2017_06_customer_id_idx} | CREATE INDEX  ON public.payment_p2017_06 USING btree (customer_id
(6 rows)

Indici superset

È anche possibile che ti ritrovi con più indici in cui uno indicizza un superset di colonne rispetto all'altro. Questo può essere desiderabile o meno:quello del superset può comportare scansioni solo dell'indice, il che è positivo, ma potrebbe occupare troppo spazio, o forse la query che originariamente si intendeva ottimizzare non viene più utilizzata.

Se desideri automatizzare il rilevamento di tali indici, il pg_catalog tablepg_index è un buon punto di partenza.

Indici non utilizzati

Man mano che le applicazioni che utilizzano il database si evolvono, aumentano anche le query che utilizzano. Gli indici aggiunti in precedenza non possono più essere utilizzati da nessuna query. Ogni volta che un indice viene scansionato, viene annotato dal gestore delle statistiche e il conteggio cumulativo è disponibile nella vista del catalogo di sistema pg_stat_user_indexes come valore idx_scan . Il monitoraggio di questo valore per un periodo di tempo (ad esempio un mese) dà una buona idea di quali indici sono inutilizzati e possono essere rimossi.

Ecco la query per ottenere i conteggi di scansione correnti per tutti gli indici nello schema "pubblico":

SELECT relname, indexrelname, idx_scan
FROM   pg_catalog.pg_stat_user_indexes
WHERE  schemaname = 'public';

con output come questo:

pagila=# SELECT relname, indexrelname, idx_scan
pagila-# FROM   pg_catalog.pg_stat_user_indexes
pagila-# WHERE  schemaname = 'public'
pagila-# LIMIT  10;
    relname    |    indexrelname    | idx_scan
---------------+--------------------+----------
 customer      | customer_pkey      |    32093
 actor         | actor_pkey         |     5462
 address       | address_pkey       |      660
 category      | category_pkey      |     1000
 city          | city_pkey          |      609
 country       | country_pkey       |      604
 film_actor    | film_actor_pkey    |        0
 film_category | film_category_pkey |        0
 film          | film_pkey          |    11043
 inventory     | inventory_pkey     |    16048
(10 rows)

Ricostruisci indici con meno locking

Non è raro che gli indici debbano essere ricreati. Gli indici possono anche gonfiarsi e ricreare l'indice può risolverlo, facendo sì che la scansione diventi più veloce. Gli indici possono anche essere danneggiati. Anche la modifica dei parametri dell'indice potrebbe richiedere la ricreazione dell'indice.

Abilita creazione indice parallelo

In PostgreSQL 11, la creazione dell'indice B-Tree è simultanea. Può utilizzare più lavoratori paralleli per velocizzare la creazione dell'indice. Tuttavia, devi assicurarti che queste voci di configurazione siano impostate in modo appropriato:

SET max_parallel_workers = 32;
SET max_parallel_maintenance_workers = 16;

I valori predefiniti sono irragionevolmente piccoli. Idealmente, questi numeri dovrebbero aumentare con il numero di core della CPU. Consulta i documenti per ulteriori informazioni.

Crea indici in background

Puoi anche creare un indice in background, usando CONCURRENTLY parametro di CREATE INDEX comando:

pagila=# CREATE INDEX CONCURRENTLY idx_address1 ON address(district);
CREATE INDEX

Ciò è diverso dall'esecuzione di un normale indice di creazione in quanto non richiede un blocco sulla tabella e quindi non blocca le scritture. Il lato negativo è che il completamento richiede più tempo e risorse.