La corretta applicazione degli indici può velocizzare le query.
Gli indici utilizzano i puntatori per accedere rapidamente alle pagine di dati.
Sono state apportate importanti modifiche agli indici in PostgreSQL 11, sono state rilasciate molte patch tanto attese.
Diamo un'occhiata ad alcune delle fantastiche funzionalità di questa versione.
Build di indici B-TREE paralleli
PostgreSQL 11 ha introdotto una patch per l'infrastruttura per consentire la creazione di indici paralleli.
Può essere utilizzato solo con l'indice B-Tree per ora.
Costruire un indice B-Tree parallelo è da due a tre volte più veloce che fare la stessa cosa senza lavorare in parallelo (o build seriale).
In PostgreSQL 11 la creazione dell'indice parallelo è attiva per impostazione predefinita.
Ci sono due parametri importanti:
- max_parallel_workers - Imposta il numero massimo di lavoratori che il sistema può supportare per le query parallele.
- max_parallel_maintenance_workers - Controlla il numero massimo di processi di lavoro che possono essere utilizzati per CREARE INDICE.
Verifichiamolo con un esempio:
severalnines=# CREATE TABLE test_btree AS SELECT generate_series(1,100000000) AS id;
SELECT 100000000
severalnines=# SET maintenance_work_mem = '1GB';
severalnines=# \timing
severalnines=# CREATE INDEX q ON test_btree (id);
TIME: 25294.185 ms (00:25.294)
Proviamo con il lavoro parallelo a 8 vie:
severalnines=# SET maintenance_work_mem = '2GB';
severalnines=# SET max_parallel_workers = 16;
severalnines=# SET max_parallel_maintenance_workers = 8;
severalnines=# \timing
severalnines=# CREATE INDEX q1 ON test_btree (id);
TIME: 11001.240 ms (00:11.001)
Possiamo vedere la differenza di prestazioni con il lavoratore parallelo, con prestazioni superiori al 60% con un piccolo cambiamento. La Maintenance_work_mem può anche essere aumentata per ottenere maggiori prestazioni.
La tabella ALTER aiuta anche ad aumentare i lavoratori paralleli. La sintassi di seguito può essere utilizzata per aumentare i lavoratori paralleli insieme a max_parallel_maintenance_workers. Questo bypassa completamente il modello di costo.
ALTER TABLE test_btree SET (parallel_workers = 24);
Suggerimento:RESET al valore predefinito una volta completata la compilazione dell'indice per evitare un piano di query negativo.
CREATE INDEX con l'opzione CONCURRENTLY supporta build parallele senza restrizioni speciali, solo la prima scansione della tabella viene effettivamente eseguita in parallelo.
Test più approfonditi delle prestazioni possono essere trovati qui.
Aggiungi il blocco predicato per gli indici Hash, Gist e Gin
PostgreSQL 11 viene fornito con il supporto del blocco predicato per indici hash, indici gin e indici Gist. Questi renderanno l'isolamento delle transazioni SERIALIZABLE molto più efficiente con quegli indici.
Vantaggio:il blocco del predicato può fornire prestazioni migliori a livello di isolamento serializzabile riducendo il numero di falsi positivi che porta a un errore di serializzazione non necessario.
In PostgreSQL 10, l'intervallo di blocco è la relazione, ma in PostgreSQL 11 il blocco è solo di pagina.
Proviamolo.
severalnines=# CREATE TABLE sv_predicate_lock1(c1 INT, c2 VARCHAR(10)) ;
CREATE TABLE
severalnines=# CREATE INDEX idx1_sv_predicate_lock1 ON sv_predicate_lock1 USING 'hash(c1) ;
CREATE INDEX
severalnines=# INSERT INTO sv_predicate_lock1 VALUES (generate_series(1, 100000), 'puja') ;
INSERT 0 100000
severalnines=# BEGIN ISOLATION LEVEL SERIALIZABLE ;
BEGIN
severalnines=# SELECT * FROM sv_predicate_lock1 WHERE c1=10000 FOR UPDATE ;
c1 | c2
-------+-------
10000 | puja
(1 row)
Come possiamo vedere di seguito, il blocco è a livello di pagina anziché di relazione. In PostgreSQL 10 era a livello di relazione, quindi è una GRANDE VITTORIA per le transazioni simultanee in PostgreSQL 11.
severalnines=# SELECT locktype, relation::regclass, mode FROM pg_locks ;
locktype | relation | mode
---------------+-------------------------+-----------------
relation | pg_locks | AccessShareLock
relation | idx1_sv_predicate_lock1 | AccessShareLock
relation | sv_predicate_lock1 | RowShareLock
virtualxid | | ExclusiveLock
transactionid | | ExclusiveLock
page | idx1_sv_predicate_lock1 | SIReadLock
tuple | sv_predicate_lock1 | SIReadLock
(7 rows)
Suggerimento:una scansione sequenziale avrà sempre bisogno di un blocco del predicato a livello di relazione. Ciò può comportare un aumento del tasso di errori di serializzazione. Può essere utile incoraggiare l'uso delle scansioni degli indici riducendo random_page_cost e/o aumentando cpu_tuple_cost.
Consenti aggiornamenti HOT per alcuni indici di espressioni
La funzione Heap Only Tuple (HOT) elimina le voci di indice ridondanti e consente il riutilizzo dello spazio occupato dalle tuple DELETEd o UPDATE obsolete senza eseguire un vuoto a livello di tabella. Riduce la dimensione dell'indice evitando la creazione di voci di indice con chiave identica.
Se il valore di un'espressione di indice è invariato dopo UPDATE, consenti aggiornamenti HOT laddove in precedenza PostgreSQL non li consentiva, aumentando in questi casi le prestazioni in modo significativo.
Ciò è particolarmente utile per indici come JSON->>campo in cui il valore JSON cambia ma il valore indicizzato no.
Questa funzione è stata ripristinata nella 11.1 a causa del degrado delle prestazioni (solo AT Free BSD come da Simon), maggiori dettagli/benchmark possono essere trovati qui. Questo dovrebbe essere risolto nelle versioni future.
Consenti la scansione di intere pagine indice hash
Indice hash:il pianificatore di query prenderà in considerazione l'utilizzo di un indice hash ogni volta che una colonna indicizzata è coinvolta in un confronto utilizzando l'operatore =. Inoltre non era sicuro per gli arresti anomali (non è stato registrato in WAL), quindi deve essere ricostruito dopo gli arresti anomali del DB e le modifiche all'hash non sono state scritte tramite la replica in streaming.
In PostgreSQL 10, l'indice hash è stato registrato WAL, ciò significa che è sicuro da CRASH e può essere replicato. Gli indici hash utilizzano molto meno spazio rispetto a B-Tree in modo che possano adattarsi meglio alla memoria.
In PostgreSQL 11, gli indici Btree hanno un'ottimizzazione chiamata "vuoto di pagina singola", che rimuove opportunisticamente i puntatori di indice morti dalle pagine di indice, prevenendo un'enorme quantità di rigonfiamento dell'indice, che altrimenti si verificherebbe. La stessa logica è stata trasferita negli indici Hash. Accelera il riciclaggio dello spazio, riducendo il rigonfiamento.
STATISTICHE dell'Indice di Funzione
È ora possibile specificare un valore STATISTICS per una colonna dell'indice di funzione. È estremamente prezioso per l'efficienza di un'applicazione specializzata. Ora possiamo raccogliere statistiche sulle colonne delle espressioni, che aiuteranno il pianificatore a prendere una decisione più accurata.
severalnines=# CREATE INDEX idx1_stats ON stat ((s1 + s2)) ;
CREATE INDEX
severalnines=# ALTER INDEX idx1_stats ALTER COLUMN 1 SET STATISTICS 1000 ;
ALTER INDEX
severalnines=# \d+ idx1_stats
Index "public.idx1_stats"
Column | Type | Definition | Storage | Stats target
--------+---------+------------+---------+--------------
expr | numeric | (c1 + c2) | main | 1000
btree, for table "public.stat1"
controllo
È stato aggiunto un nuovo modulo Contrib amcheck. È possibile controllare solo gli indici B-Tree.
Proviamolo!
severalnines=# CREATE EXTENSION amcheck ;
CREATE EXTENSION
severalnines=# SELECT bt_index_check('idx1_stats') ;
ERROR: invalid page in block 0 of relation base/16385/16580
severalnines=#CREATE INDEX idx1_hash_data1 ON data1 USING hash (c1) ;
CREATE INDEX
severalnines=# SELECT bt_index_check('idx1_hash_data1') ;
ERROR: only B-Tree indexes are supported as targets for verification
DETAIL: Relation "idx1_hash_data1" is not a B-Tree index.
Possibile indice partizionato locale
Prima di PostgreSQL11, non era possibile creare un indice su una tabella figlio o su una tabella partizionata.
In PostgreSQL 11, quando CREATE INDEX viene eseguito su una tabella partizionata/tabella padre, crea voci di catalogo per un indice sulla tabella partizionata e crea indici effettivi sulle partizioni esistenti. Li creerà anche in partizioni future.
Proviamo a creare una tabella padre e una partizione:
severalnines=# create table test_part ( a int, list varchar(5) ) partition by list (list);
CREATE TABLE
severalnines=# create table part_1 partition of test_part for values in ('India');
CREATE TABLE
severalnines=# create table part_2 partition of test_part for values in ('USA');
CREATE TABLE
severalnines=#
severalnines=# \d+ test_part
Table "public.test_part"
Column | Type | Collation | Nullable | Default | Storage | Stats target | Description
--------+----------------------+-----------+----------+---------+----------+--------------+-------------
a | integer | | | | plain | |
list | character varying(5) | | | | extended | |
Partition key: LIST (list)
Partitions: part_1 FOR VALUES IN ('India'),
part_2 FOR VALUES IN ('USA')
Proviamo a creare un indice sulla tabella padre:
severalnines=# create index i_test on test_part (a);
CREATE INDEX
severalnines=# \d part_2
Table "public.part_2"
Column | Type | Collation | Nullable | Default
--------+----------------------+-----------+----------+---------
a | integer | | |
list | character varying(5) | | |
Partition of: test_part FOR VALUES IN ('USA')
Indexes:
"part_2_a_idx" btree (a)
severalnines=# \d part_1
Table "public.part_1"
Column | Type | Collation | Nullable | Default
--------+----------------------+-----------+----------+---------
a | integer | | |
list | character varying(5) | | |
Partition of: test_part FOR VALUES IN ('India')
Indexes:
"part_1_a_idx" btree (a)
L'indice è distribuito a cascata in tutte le partizioni in PostgreSQL 11, una funzionalità davvero interessante.
Indice di copertura (includere CLAUSOLA per gli indici)
È possibile specificare una clausola INCLUDE per aggiungere colonne all'indice. Ciò è efficace quando si aggiungono colonne non correlate a un vincolo univoco di un indice univoco. Le colonne INCLUDE esistono esclusivamente per consentire a più query di trarre vantaggio dalle scansioni solo indice. Solo gli indici B-tree supportano la clausola INCLUDE come per ora.
Controlliamo il comportamento senza INCLUDE. Non utilizzerà la scansione solo dell'indice se vengono visualizzate colonne aggiuntive in SELECT. Questo può essere ottenuto utilizzando la clausola INCLUDE.
severalnines=# CREATE TABLE no_include (a int, b int, c int);
CREATE TABLE
severalnines=# INSERT INTO no_include SELECT 3 * val, 3 * val + 1, 3 * val + 2 FROM generate_series(0, 1000000) as val;
INSERT 0 1000001
severalnines=# CREATE UNIQUE INDEX old_unique_idx ON no_include(a, b);
CREATE INDEX
severalnines=# VACUUM ANALYZE;
VACUUM
EXPLAIN ANALYZE SELECT a, b FROM no_include WHERE a < 1000; - It will do index only scan
EXPLAIN ANALYZE SELECT a, b, c FROM no_include WHERE a < 1000; - It will not do index only scan as we have extra column in select.
severalnines=# CREATE INDEX old_idx ON no_include (a, b, c);
CREATE INDEX
severalnines=# VACUUM ANALYZE;
VACUUM
severalnines=# EXPLAIN ANALYZE SELECT a, b, c FROM no_include WHERE a < 1000; - It did index only scan as index on all three columns.
QUERY PLAN
-------------------------------------------------
Index Only Scan using old_idx on no_include
(cost=0.42..14.92 rows=371 width=12)
(actual time=0.086..0.291 rows=334 loops=1)
Index Cond: (a < 1000)
Heap Fetches: 0
Planning Time: 2.108 ms
Execution Time: 0.396 ms
(5 rows)
Proviamo con la clausola include. Nell'esempio seguente il VINCENTE UNICO viene creato nelle colonne aeb, ma l'indice include una colonna c.
severalnines=# CREATE TABLE with_include (a int, b int, c int);
CREATE TABLE
severalnines=# INSERT INTO with_include SELECT 3 * val, 3 * val + 1, 3 * val + 2 FROM generate_series(0, 1000000) as val;
INSERT 0 1000001
severalnines=# CREATE UNIQUE INDEX new_unique_idx ON with_include(a, b) INCLUDE (c);
CREATE INDEX
severalnines=# VACUUM ANALYZE;
VACUUM
severalnines=# EXPLAIN ANALYZE SELECT a, b, c FROM with_include WHERE a < 10000;
QUERY PLAN
-----------------------------------------------------
Index Only Scan using new_unique_idx on with_include
(cost=0.42..116.06 rows=3408 width=12)
(actual time=0.085..2.348 rows=3334 loops=1)
Index Cond: (a < 10000)
Heap Fetches: 0
Planning Time: 1.851 ms
Execution Time: 2.840 ms
(5 rows)
Non può esserci alcuna sovrapposizione tra le colonne nell'elenco delle colonne principali e quelle dell'elenco di inclusione
severalnines=# CREATE UNIQUE INDEX new_unique_idx ON with_include(a, b) INCLUDE (a);
ERROR: 42P17: included columns must not intersect with key columns
LOCATION: DefineIndex, indexcmds.c:373
Una colonna utilizzata con un'espressione nell'elenco principale funziona:
severalnines=# CREATE UNIQUE INDEX new_unique_idx_2 ON with_include(round(a), b) INCLUDE (a);
CREATE INDEX
Le espressioni non possono essere utilizzate in un elenco di inclusione perché non possono essere utilizzate in una scansione solo indice:
severalnines=# CREATE UNIQUE INDEX new_unique_idx_2 ON with_include(a, b) INCLUDE (round(c));
ERROR: 0A000: expressions are not supported in included columns
LOCATION: ComputeIndexAttrs, indexcmds.c:1446
Conclusione
Le nuove funzionalità di PostgreSQL miglioreranno sicuramente la vita dei DBA, quindi sta per diventare una forte scelta alternativa nel DB open source. Capisco che alcune funzionalità degli indici sono attualmente limitate a B-Tree, è ancora un ottimo inizio dell'era dell'esecuzione parallela per PostgreSQL e si sta dirigendo verso un bel strumento per guardare da vicino. Grazie!