In de Postgres-wereld zijn indexen essentieel om efficiënt door de tabelgegevensopslag te navigeren (ook bekend als de "heap"). Postgres onderhoudt geen clustering voor de heap, en de MVCC-architectuur leidt tot meerdere versies van dezelfde tuple die rondslingeren. Het creëren en onderhouden van effectieve en efficiënte indexen om applicaties te ondersteunen is een essentiële vaardigheid.
Lees verder voor enkele tips voor het optimaliseren en verbeteren van het gebruik van indexen in uw implementatie.
Opmerking:onderstaande zoekopdrachten worden uitgevoerd op een ongewijzigde pagila-voorbeelddatabase.
Dekkingsindexen gebruiken
Overweeg een zoekopdracht om de e-mails van alle inactieve klanten op te halen. De klant tafel heeft een actieve kolom, en de vraag is eenvoudig:
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)
De query vereist een volledige sequentiële scan van de klantentabel. Laten we een index maken op de actieve kolom:
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)
Dit helpt, en de sequentiële scan is een "indexscan" geworden. Dit betekent dat Postgres de index "idx_cust1" zal scannen en vervolgens verder zal zoeken in de tabel om de andere kolomwaarden te lezen (in dit geval de e-mail kolom) die de query nodig heeft.
PostgreSQL 11 introduceerde dekkende indexen. Met deze functie kunt u een of meer extra kolommen in de index zelf opnemen - dat wil zeggen dat de waarden van deze extra kolommen worden opgeslagen in de indexgegevensopslag.
Als we deze functie zouden gebruiken en de waarde van e-mail in de index zouden opnemen, hoeft Postgres niet in de heap van de tabel te kijken om de waarde van e-mail te krijgen. . Eens kijken of dit werkt:
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)
De "Index Only Scan" vertelt ons dat de query nu volledig wordt voldaan door de index zelf, waardoor mogelijk alle schijf-I/O wordt vermeden voor het lezen van de heap van de tabel.
Dekkingsindexen zijn vanaf nu alleen beschikbaar voor B-Tree-indexen. Ook zijn de kosten voor het onderhouden van een dekkingsindex natuurlijk hoger dan voor een reguliere.
Gedeeltelijke indexen gebruiken
Gedeeltelijke indexen indexeren alleen een subset van de rijen in een tabel. Hierdoor blijven de indexen kleiner en sneller om door te bladeren.
Stel dat we de lijst met e-mails van klanten in Californië nodig hebben. De vraag is:
SELECT c.email FROM customer c
JOIN address a ON c.address_id = a.address_id
WHERE a.district = 'California';
die een queryplan heeft waarbij beide tabellen worden gescand die zijn samengevoegd:
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)
Laten we eens kijken wat een reguliere index ons oplevert:
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)
De scan van adres is vervangen door een indexscan over idx_address1 ,en een scan van de adreshoop.
Ervan uitgaande dat dit een veelvoorkomende zoekopdracht is en moet worden geoptimaliseerd, kunnen we een aparte index gebruiken die alleen die rijen met adressen indexeert waar het district 'Californië' is:
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)
De query leest nu alleen de index idx_address2 en raakt het tafeladres niet aan .
Gebruik indexen met meerdere waarden
Sommige kolommen die moeten worden geïndexeerd, hebben mogelijk geen scalair gegevenstype. Kolomtypen zoals jsonb , arrays en tsvector samengestelde of meerdere waarden hebben. Als u dergelijke kolommen moet indexeren, moet u meestal ook de afzonderlijke waarden in die kolommen doorzoeken.
Laten we proberen alle filmtitels te vinden die outtakes achter de schermen bevatten. Defilm tabel heeft een tekstarraykolom genaamd special_features , dat het tekstarray-element Behind The Scenes . bevat als een film die functie heeft. Om al dergelijke films te vinden, moeten we alle rijen selecteren die "Behind The Scenes" hebben inelke van de waarden van de array special_features :
SELECT title FROM film WHERE special_features @> '{"Behind The Scenes"}';
De insluitingsoperator @> controleert of de linkerkant een superset is van de rechterkant.
Hier is het zoekplan:
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)
waarvoor een volledige scan van de hoop nodig is, tegen een kostprijs van 67.
Eens kijken of een gewone B-Tree-index helpt:
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)
Er wordt zelfs geen rekening gehouden met de index. De B-Tree-index heeft geen idee dat er individuele elementen in de geïndexeerde waarde zitten.
Wat we nodig hebben is een GIN-index.
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)
De GIN-index ondersteunt het matchen van de individuele waarde met de geïndexeerde samengestelde waarde, wat resulteert in een queryplan met minder dan de helft van de kosten van het origineel.
Dubbele indexen elimineren
In de loop van de tijd stapelen indexen zich op en soms wordt er een toegevoegd die exact dezelfde definitie heeft als een andere. U kunt de catalogusweergave pg_indexes
. gebruiken om de voor mensen leesbare SQL-definities van indexen te krijgen. U kunt ook gemakkelijk identieke definities detecteren:
SELECT array_agg(indexname) AS indexes, replace(indexdef, indexname, '') AS defn
FROM pg_indexes
GROUP BY defn
HAVING count(*) > 1;
En hier is het resultaat wanneer het wordt uitgevoerd in de pagila-database met aandelen:
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)
Superset-indexen
Het is ook mogelijk dat u meerdere indexen krijgt waarbij de ene een superset van kolommen indexeert die de andere doet. Dit kan al dan niet wenselijk zijn - de superset kan resulteren in alleen-index-scans, wat een goede zaak is, maar kan te veel ruimte in beslag nemen, of misschien wordt de zoekopdracht die oorspronkelijk was bedoeld om te optimaliseren niet meer gebruikt.
Als u de detectie van dergelijke indexen wilt automatiseren, is de pg_catalog tablepg_index een goed startpunt.
Ongebruikte indexen
Naarmate de toepassingen die de database gebruiken, evolueren, veranderen ook de query's die ze gebruiken. Indexen die eerder zijn toegevoegd, mogen door geen enkele zoekopdracht meer worden gebruikt. Elke keer dat een index wordt gescand, wordt dit genoteerd door de statistiekenbeheerder en is een cumulatieve telling beschikbaar in de systeemcatalogusweergave pg_stat_user_indexes
als de waarde idx_scan
. Door deze waarde gedurende een bepaalde periode (bijvoorbeeld een maand) te bewaken, krijgt u een goed idee van welke indexen niet worden gebruikt en kunnen worden verwijderd.
Hier is de vraag om de huidige scantellingen te krijgen voor alle indexen in het 'openbare' schema:
SELECT relname, indexrelname, idx_scan
FROM pg_catalog.pg_stat_user_indexes
WHERE schemaname = 'public';
met uitvoer als volgt:
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)
Indexen opnieuw opbouwen met minder vergrendeling
Het is niet ongebruikelijk dat indexen opnieuw moeten worden gemaakt. Indexen kunnen ook opgeblazen raken, en het opnieuw maken van de index kan dit verhelpen, waardoor het sneller wordt om te scannen. Indexen kunnen ook corrupt worden. Het wijzigen van indexparameters kan ook nodig zijn om de index opnieuw te maken.
Schakel het maken van een parallelle index in
In PostgreSQL 11 is het maken van B-Tree-indexen gelijktijdig. Het kan gebruik maken van meerdere parallelle werkers om het maken van de index te versnellen. U moet er echter voor zorgen dat deze configuratie-items correct zijn ingesteld:
SET max_parallel_workers = 32;
SET max_parallel_maintenance_workers = 16;
De standaardwaarden zijn onredelijk klein. Idealiter zouden deze aantallen moeten toenemen met het aantal CPU-kernen. Zie de documenten voor meer informatie.
Maak indexen op de achtergrond
U kunt ook een index op de achtergrond maken met behulp van de CONCURRENTLY parameter van de CREATE INDEX commando:
pagila=# CREATE INDEX CONCURRENTLY idx_address1 ON address(district);
CREATE INDEX
Dit verschilt van het doen van een normale index voor het maken van een index, omdat het geen vergrendeling over de tabel vereist en daarom schrijfbewerkingen niet uitsluit. Het nadeel is dat het meer tijd en middelen kost om te voltooien.