L'indicizzazione del database è l'uso di strutture dati speciali che mirano a migliorare le prestazioni, ottenendo l'accesso diretto alle pagine di dati. Un indice di un database funziona come la sezione dell'indice di un libro cartaceo:guardando nella sezione dell'indice, è molto più veloce identificare le pagine che contengono il termine che ci interessa. Possiamo facilmente individuare le pagine e accedervi direttamente . Questo invece di scansionare le pagine del libro in sequenza, finché non troviamo il termine che stiamo cercando.
Gli indici sono uno strumento essenziale nelle mani di un DBA. L'uso degli indici può fornire notevoli miglioramenti delle prestazioni per una vasta gamma di domini di dati. PostgreSQL è noto per la sua grande estensibilità e la ricca raccolta di componenti aggiuntivi sia di base che di terze parti e l'indicizzazione non fa eccezione a questa regola. Gli indici PostgreSQL coprono un'ampia gamma di casi, dai più semplici indici b-tree su tipi scalari agli indici GiST geospaziali agli indici fts o json o array GIN.
Gli indici, tuttavia, per quanto meravigliosi possano sembrare (e in realtà sono!) non sono gratuiti. C'è una certa penalità che va con le scritture su una tabella indicizzata. Quindi il DBA, prima di esaminare le sue opzioni per creare un indice specifico, dovrebbe prima assicurarsi che detto indice abbia senso in primo luogo, il che significa che i guadagni dalla sua creazione supereranno la perdita di prestazioni sulle scritture.
Terminologia di base dell'indice PostgreSQL
Prima di descrivere i tipi di indici in PostgreSQL e il loro utilizzo, diamo un'occhiata alla terminologia che qualsiasi DBA incontrerà prima o poi durante la lettura dei documenti.
- Metodo di accesso all'indice (chiamato anche Metodo di accesso ):il tipo di indice (B-tree, GiST, GIN, ecc.)
- Digita: il tipo di dati della colonna indicizzata
- Operatore: una funzione tra due tipi di dati
- Famiglia di operatori: operatore di tipi di dati incrociati, raggruppando operatori di tipi con comportamento simile
- Classe operatore (indicato anche come strategia dell'indice ):definisce gli operatori che devono essere utilizzati dall'indice per una colonna
Nel catalogo di sistema di PostgreSQL, i metodi di accesso sono memorizzati in pg_am, le classi di operatori in pg_opclass, le famiglie di operatori in pg_opfamily. Le dipendenze di quanto sopra sono mostrate nel diagramma seguente:
Tipi di indici in PostgreSQL
PostgreSQL fornisce i seguenti tipi di indice:
- B-albero: l'indice predefinito, applicabile ai tipi che possono essere ordinati
- Hash: gestisce solo l'uguaglianza
- GiST: adatto per tipi di dati non scalari (es. forme geometriche, fts, array)
- SP-GiST: GIST a partizione spaziale, un'evoluzione di GiST per la gestione di strutture non bilanciate (quadtrees, k-d tree, radix tree)
- GIN: adatto per tipi complessi (es. jsonb, fts, arrays )
- BRIN: un tipo di indice relativamente nuovo che supporta dati che possono essere ordinati memorizzando valori min/max in ogni blocco
In basso cercheremo di sporcarci le mani con alcuni esempi del mondo reale. Tutti gli esempi forniti sono realizzati con PostgreSQL 10.0 (con client 10 e 9 psql) su FreeBSD 11.1.
Indici ad albero B
Supponiamo di avere la seguente tabella:
create table part (
id serial primary key,
partno varchar(20) NOT NULL UNIQUE,
partname varchar(80) NOT NULL,
partdescr text,
machine_id int NOT NULL
);
testdb=# \d part
Table "public.part"
Column | Type | Modifiers
------------+-----------------------+---------------------------------------------------
id | integer | not null default nextval('part_id_seq'::regclass)
partno | character varying(20)| not null
partname | character varying(80)| not null
partdescr | text |
machine_id | integer | not null
Indexes:
"part_pkey" PRIMARY KEY, btree (id)
"part_partno_key" UNIQUE CONSTRAINT, btree (partno)
Quando definiamo questa tabella piuttosto comune, PostgreSQL crea due indici B-tree unici dietro le quinte:part_pkey e part_partno_key. Quindi ogni vincolo univoco in PostgreSQL è implementato con un INDEX univoco. Popoliamo la nostra tabella con un milione di righe di dati:
testdb=# with populate_qry as (select gs from generate_series(1,1000000) as gs )
insert into part (partno, partname,machine_id) SELECT 'PNo:'||gs, 'Part '||gs,0 from populate_qry;
INSERT 0 1000000
Ora proviamo a fare alcune domande sulla nostra tabella. Per prima cosa diciamo al client psql di riportare i tempi di query digitando \timing:
testdb=# select * from part where id=100000;
id | partno | partname | partdescr | machine_id
--------+------------+-------------+-----------+------------
100000 | PNo:100000 | Part 100000 | | 0
(1 row)
Time: 0,284 ms
testdb=# select * from part where partno='PNo:100000';
id | partno | partname | partdescr | machine_id
--------+------------+-------------+-----------+------------
100000 | PNo:100000 | Part 100000 | | 0
(1 row)
Time: 0,319 ms
Osserviamo che bastano solo frazioni di millisecondo per ottenere i nostri risultati. Ci aspettavamo questo poiché per entrambe le colonne utilizzate nelle query precedenti abbiamo già definito gli indici appropriati. Ora proviamo a interrogare sulla colonna partname, per la quale non esiste alcun indice.
testdb=# select * from part where partname='Part 100000';
id | partno | partname | partdescr | machine_id
--------+------------+-------------+-----------+------------
100000 | PNo:100000 | Part 100000 | | 0
(1 row)
Time: 89,173 ms
Qui vediamo chiaramente che per la colonna non indicizzata le prestazioni calano significativamente. Ora creiamo un indice su quella colonna e ripetiamo la query:
testdb=# create index part_partname_idx ON part(partname);
CREATE INDEX
Time: 15734,829 ms (00:15,735)
testdb=# select * from part where partname='Part 100000';
id | partno | partname | partdescr | machine_id
--------+------------+-------------+-----------+------------
100000 | PNo:100000 | Part 100000 | | 0
(1 row)
Time: 0,525 ms
Il nostro nuovo indice part_partname_idx è anche un indice B-tree (l'impostazione predefinita). Innanzitutto notiamo che la creazione dell'indice sulla tabella milioni di righe ha richiesto una notevole quantità di tempo, circa 16 secondi. Quindi osserviamo che la nostra velocità di query è stata aumentata da 89 ms fino a 0,525 ms. Gli indici B-tree, oltre a controllare l'uguaglianza, possono anche aiutare con le query che coinvolgono altri operatori su tipi ordinati, come <,<=,>=,>. Proviamo con <=e>=
testdb=# select count(*) from part where partname>='Part 9999900';
count
-------
9
(1 row)
Time: 0,359 ms
testdb=# select count(*) from part where partname<='Part 9999900';
count
--------
999991
(1 row)
Time: 355,618 ms
La prima query è molto più veloce della seconda, utilizzando le parole chiave EXPLAIN (o EXPLAIN ANALYZE) possiamo vedere se l'indice effettivo viene utilizzato o meno:
testdb=# explain select count(*) from part where partname>='Part 9999900';
QUERY PLAN
-----------------------------------------------------------------------------------------
Aggregate (cost=8.45..8.46 rows=1 width=8)
-> Index Only Scan using part_partname_idx on part (cost=0.42..8.44 rows=1 width=0)
Index Cond: (partname >= 'Part 9999900'::text)
(3 rows)
Time: 0,671 ms
testdb=# explain select count(*) from part where partname<='Part 9999900';
QUERY PLAN
----------------------------------------------------------------------------------------
Finalize Aggregate (cost=14603.22..14603.23 rows=1 width=8)
-> Gather (cost=14603.00..14603.21 rows=2 width=8)
Workers Planned: 2
-> Partial Aggregate (cost=13603.00..13603.01 rows=1 width=8)
-> Parallel Seq Scan on part (cost=0.00..12561.33 rows=416667 width=0)
Filter: ((partname)::text <= 'Part 9999900'::text)
(6 rows)
Time: 0,461 ms
Nel primo caso, il pianificatore di query sceglie di utilizzare l'indice part_partname_idx. Osserviamo inoltre che ciò comporterà una scansione solo indice, il che significa nessun accesso alle tabelle di dati. Nel secondo caso, il pianificatore determina che non ha senso utilizzare l'indice poiché i risultati restituiti sono una grande porzione della tabella, nel qual caso si ritiene che una scansione sequenziale sia più veloce.
Indici hash
L'uso di indici hash fino a PgSQL 9.6 incluso è stato sconsigliato per motivi legati alla mancanza di scrittura WAL. A partire da PgSQL 10.0 questi problemi sono stati risolti, ma gli indici hash non avevano ancora senso da usare. Ci sono sforzi in PgSQL 11 per rendere gli indici hash un metodo di indice di prima classe insieme ai suoi fratelli maggiori (B-tree, GiST, GIN). Quindi, con questo in mente, proviamo effettivamente un indice hash in azione.
Arricchiremo la nostra tabella delle parti con una nuova colonna parttype e la popoleremo con valori di uguale distribuzione, quindi eseguiremo una query che verifica il tipo di parte uguale a "Sterzo":
testdb=# alter table part add parttype varchar(100) CHECK (parttype in ('Engine','Suspension','Driveline','Brakes','Steering','General')) NOT NULL DEFAULT 'General';
ALTER TABLE
Time: 42690,557 ms (00:42,691)
testdb=# with catqry as (select id,(random()*6)::int % 6 as cat from part)
update part SET parttype = CASE WHEN cat=1 THEN 'Engine' WHEN cat=2 THEN 'Suspension' WHEN cat=3 THEN 'Driveline' WHEN cat=4 THEN 'Brakes' WHEN cat=5 THEN 'Steering' ELSE 'General' END FROM catqry WHERE part.id=catqry.id;
UPDATE 1000000
Time: 46345,386 ms (00:46,345)
testdb=# select count(*) from part where id % 500 = 0 AND parttype = 'Steering';
count
-------
322
(1 row)
Time: 93,361 ms
Ora creiamo un indice hash per questa nuova colonna e riproviamo la query precedente:
testdb=# create index part_parttype_idx ON part USING hash(parttype);
CREATE INDEX
Time: 95525,395 ms (01:35,525)
testdb=# analyze ;
ANALYZE
Time: 1986,642 ms (00:01,987)
testdb=# select count(*) from part where id % 500 = 0 AND parttype = 'Steering';
count
-------
322
(1 row)
Time: 63,634 ms
Notiamo il miglioramento dopo aver utilizzato l'indice hash. Ora confronteremo le prestazioni di un indice hash su numeri interi con l'equivalente indice b-tree.
testdb=# update part set machine_id = id;
UPDATE 1000000
Time: 392548,917 ms (06:32,549)
testdb=# select * from part where id=500000;
id | partno | partname | partdescr | machine_id | parttype
--------+------------+-------------+-----------+------------+------------
500000 | PNo:500000 | Part 500000 | | 500000 | Suspension
(1 row)
Time: 0,316 ms
testdb=# select * from part where machine_id=500000;
id | partno | partname | partdescr | machine_id | parttype
--------+------------+-------------+-----------+------------+------------
500000 | PNo:500000 | Part 500000 | | 500000 | Suspension
(1 row)
Time: 97,037 ms
testdb=# create index part_machine_id_idx ON part USING hash(machine_id);
CREATE INDEX
Time: 4756,249 ms (00:04,756)
testdb=#
testdb=# select * from part where machine_id=500000;
id | partno | partname | partdescr | machine_id | parttype
--------+------------+-------------+-----------+------------+------------
500000 | PNo:500000 | Part 500000 | | 500000 | Suspension
(1 row)
Time: 0,297 ms
Come si vede, con l'uso degli indici hash, la velocità delle query che controllano l'uguaglianza è molto vicina alla velocità degli indici B-tree. Si dice che gli indici hash siano leggermente più veloci per l'uguaglianza rispetto ai B-tree, infatti abbiamo dovuto provare ogni query due o tre volte fino a quando l'indice hash non ha dato un risultato migliore rispetto all'equivalente b-tree.
Scarica il whitepaper oggi Gestione e automazione di PostgreSQL con ClusterControlScopri ciò che devi sapere per distribuire, monitorare, gestire e ridimensionare PostgreSQLScarica il whitepaperIndici GiST
GiST (Generalized Search Tree) è più di un singolo tipo di indice, ma piuttosto un'infrastruttura per costruire molte strategie di indicizzazione. La distribuzione PostgreSQL predefinita fornisce supporto per i tipi di dati geometrici, tsquery e tsvector. In contrib ci sono implementazioni di molte altre classi di operatori. Leggendo i documenti e la dir contrib, il lettore osserverà che c'è una sovrapposizione piuttosto grande tra i casi d'uso GiST e GIN:array int, ricerca full-text per nominare i casi principali. In questi casi GIN è più veloce e la documentazione ufficiale lo afferma esplicitamente. Tuttavia, GiST fornisce un ampio supporto per i tipi di dati geometrici. Inoltre, come al momento in cui scrivo, GiST (e SP-GiST) è l'unico metodo significativo che può essere utilizzato con vincoli di esclusione. Vedremo un esempio su questo. Supponiamo (rimanendo nel campo dell'ingegneria meccanica) di avere l'esigenza di definire variazioni di tipo macchine per un particolare tipo di macchina, che siano valide per un certo periodo di tempo; e che per una particolare variazione non possono esistere altre variazioni per lo stesso tipo di macchina il cui periodo di tempo si sovrappone (è in conflitto) con il particolare periodo di variazione.
create table machine_type (
id SERIAL PRIMARY KEY,
mtname varchar(50) not null,
mtvar varchar(20) not null,
start_date date not null,
end_date date,
CONSTRAINT machine_type_uk UNIQUE (mtname,mtvar)
);
Sopra diciamo a PostgreSQL che per ogni nome del tipo di macchina (mtname) può esserci solo una variazione (mtvar). Start_date indica la data di inizio del periodo in cui questa variazione del tipo di macchina è valida e end_date indica la data di fine di questo periodo. Null end_date significa che la variazione del tipo di macchina è attualmente valida. Ora vogliamo esprimere il requisito di non sovrapposizione con un vincolo. Il modo per farlo è con un vincolo di esclusione:
testdb=# alter table machine_type ADD CONSTRAINT machine_type_per EXCLUDE USING GIST (mtname WITH =,daterange(start_date,end_date) WITH &&);
La sintassi EXCLUDE PostgreSQL ci consente di specificare molte colonne di tipi diversi e con un operatore diverso per ognuna. &&è l'operatore di sovrapposizione per gli intervalli di date e =è l'operatore di uguaglianza comune per varchar. Ma finché premiamo invio PostgreSQL si lamenta con un messaggio:
ERROR: data type character varying has no default operator class for access method "gist"
HINT: You must specify an operator class for the index or define a default operator class for the data type.
Quello che manca qui è il supporto GiST opclass per varchar. A condizione di aver creato e installato correttamente l'estensione btree_gist, possiamo procedere con la creazione dell'estensione:
testdb=# create extension btree_gist ;
CREATE EXTENSION
E quindi riprovare a creare il vincolo e testarlo:
testdb=# alter table machine_type ADD CONSTRAINT machine_type_per EXCLUDE USING GIST (mtname WITH =,daterange(start_date,end_date) WITH &&);
ALTER TABLE
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SH','2008-01-01','2013-01-01');
INSERT 0 1
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SG','2002-01-01','2009-01-01');
ERROR: conflicting key value violates exclusion constraint "machine_type_per"
DETAIL: Key (mtname, daterange(start_date, end_date))=(Subaru EJ20, [2002-01-01,2009-01-01)) conflicts with existing key (mtname, daterange(start_date, end_date))=(Subaru EJ20, [2008-01-01,2013-01-01)).
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SG','2002-01-01','2008-01-01');
INSERT 0 1
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SJ','2013-01-01',null);
INSERT 0 1
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SJ2','2018-01-01',null);
ERROR: conflicting key value violates exclusion constraint "machine_type_per"
DETAIL: Key (mtname, daterange(start_date, end_date))=(Subaru EJ20, [2018-01-01,)) conflicts with existing key (mtname, daterange(start_date, end_date))=(Subaru EJ20, [2013-01-01,)).
Indici SP-GiST
SP-GiST, acronimo di GiST con partizioni spaziali, come GiST, è un'infrastruttura che consente lo sviluppo di molte strategie diverse nel dominio delle strutture di dati basate su disco non bilanciate. La distribuzione PgSQL predefinita offre supporto per punti bidimensionali, intervalli (qualsiasi tipo), testo e tipi inet. Come GiST, SP-GiST può essere utilizzato nei vincoli di esclusione, in modo simile all'esempio mostrato nel capitolo precedente.
Indici GIN
GIN (Generalized Inverted Index) come GiST e SP-GiST possono fornire molte strategie di indicizzazione. GIN è adatto quando vogliamo indicizzare colonne di tipi compositi. La distribuzione PostgreSQL predefinita fornisce supporto per qualsiasi tipo di array, jsonb e ricerca full-text (tsvector). In contrib ci sono implementazioni di molte altre classi di operatori. Jsonb, una funzionalità molto apprezzata di PostgreSQL (e uno sviluppo relativamente recente (9.4+)) si basa su GIN per il supporto dell'indice. Un altro uso comune di GIN è l'indicizzazione per la ricerca full-text. La ricerca di testo completo in PgSQL merita un articolo a sé stante, quindi tratteremo qui solo la parte dell'indicizzazione. Per prima cosa facciamo un po' di preparazione alla nostra tabella, dando valori non nulli alla colonna partdescr e aggiornando una singola riga con un valore significativo:
testdb=# update part set partdescr ='';
UPDATE 1000000
Time: 383407,114 ms (06:23,407)
testdb=# update part set partdescr = 'thermostat for the cooling system' where id=500000;
UPDATE 1
Time: 2,405 ms
Quindi eseguiamo una ricerca testuale sulla colonna appena aggiornata:
testdb=# select * from part where partdescr @@ 'thermostat';
id | partno | partname | partdescr | machine_id | parttype
--------+------------+-------------+-----------------------------------+------------+------------
500000 | PNo:500000 | Part 500000 | thermostat for the cooling system | 500000 | Suspension
(1 row)
Time: 2015,690 ms (00:02,016)
Questo è abbastanza lento, quasi 2 secondi per portare il nostro risultato. Ora proviamo a creare un indice GIN sul tipo tsvector e ripetiamo la query, usando una sintassi adatta agli indici:
testdb=# CREATE INDEX part_partdescr_idx ON part USING gin(to_tsvector('english',partdescr));
CREATE INDEX
Time: 1431,550 ms (00:01,432)
testdb=# select * from part where to_tsvector('english',partdescr) @@ to_tsquery('thermostat');
id | partno | partname | partdescr | machine_id | parttype
--------+------------+-------------+-----------------------------------+------------+------------
500000 | PNo:500000 | Part 500000 | thermostat for the cooling system | 500000 | Suspension
(1 row)
Time: 0,952 ms
E otteniamo un'accelerazione di 2000 volte. Inoltre, possiamo notare il tempo relativamente breve impiegato per creare l'indice. Puoi sperimentare l'utilizzo di GiST invece di GIN nell'esempio precedente e misurare le prestazioni di letture, scritture e creazione di indici per entrambi i metodi di accesso.
Indici BRIN
BRIN (Block Range Index) è l'ultima aggiunta al set di tipi di indice di PostgreSQL, da quando è stato introdotto in PostgreSQL 9.5, avendo solo pochi anni come funzionalità di base standard. BRIN funziona su tabelle molto grandi memorizzando informazioni di riepilogo per un insieme di pagine chiamato "Intervallo di blocchi". Gli indici BRIN sono con perdite (come GiST) e ciò richiede sia una logica aggiuntiva nell'esecutore di query di PostgreSQL, sia anche la necessità di ulteriore manutenzione. Vediamo BRIN in azione:
testdb=# select count(*) from part where machine_id BETWEEN 5000 AND 10000;
count
-------
5001
(1 row)
Time: 100,376 ms
testdb=# create index part_machine_id_idx_brin ON part USING BRIN(machine_id);
CREATE INDEX
Time: 569,318 ms
testdb=# select count(*) from part where machine_id BETWEEN 5000 AND 10000;
count
-------
5001
(1 row)
Time: 5,461 ms
Qui vediamo in media un'accelerazione di circa 18 volte grazie all'uso dell'indice BRIN. Tuttavia, la vera casa di BRIN è nel dominio dei big data, quindi speriamo di testare questa tecnologia relativamente nuova in scenari del mondo reale in futuro.