I Postgres-verdenen er indekser afgørende for effektivt at navigere i tabeldatalageret (også kaldet "dyngen"). Postgres opretholder ikke en clustering for heapen, og MVCC-arkitekturen fører til, at flere versioner af den samme tuples rundt. At skabe og vedligeholde effektive og effektive indekser for at understøtte applikationer er en væsentlig færdighed.
Læs videre for at se et par tips om optimering og forbedring af brugen af indekser i din implementering.
Bemærk:Forespørgsler vist nedenfor køres på en umodificeret pagila-eksempeldatabase.
Brug dækkende indekser
Overvej en forespørgsel for at hente e-mails fra alle inaktive kunder. kunden tabel har en aktiv kolonne, og forespørgslen er ligetil:
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)
Forespørgslen kræver en fuld sekventiel scanning af kundetabellen. Lad os oprette et indeks på den aktive kolonne:
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)
Dette hjælper, og den sekventielle scanning er blevet en "indeksscanning". Det betyder, at Postgres vil scanne indekset "idx_cust1" og derefter slå tabellens bunke yderligere op for at læse de andre kolonneværdier (i dette tilfælde e-mail kolonne), som forespørgslen har brug for.
PostgreSQL 11 introducerede dækkende indekser. Denne funktion giver dig mulighed for at inkludere en eller flere ekstra kolonner i selve indekset – det vil sige, at værdierne af disse ekstra kolonner er gemt i indeksdatalageret.
Hvis vi skulle bruge denne funktion og inkludere værdien af e-mail i indekset, behøver Postgres ikke at kigge ind i tabellens bunke for at få værdien afe-mail . Lad os se, om dette virker:
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)
"Kun indeksscanning" fortæller os, at forespørgslen nu er fuldstændig tilfredsstillet af selve indekset, hvilket potentielt undgår al disk I/O til at læse tabellens heap.
Dækkende indekser er kun tilgængelige for B-Tree indekser fra nu af. Også omkostningerne ved at opretholde et dækkende indeks er naturligvis højere end et almindeligt.
Brug delvise indekser
Delvise indekser indekserer kun en delmængde af rækkerne i en tabel. Dette holder indekserne mindre i størrelse og hurtigere at scanne igennem.
Antag, at vi skal have listen over e-mails fra kunder i Californien. Spørgsmålet er:
SELECT c.email FROM customer c
JOIN address a ON c.address_id = a.address_id
WHERE a.district = 'California';
som har en forespørgselsplan, der involverer scanning af begge de tabeller, der er forbundet:
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)
Lad os se, hvad et almindeligt indeks giver os:
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)
Scanningen af adresse er blevet erstattet med en indeksscanning over idx_address1 ,og en scanning af adressebunken.
Forudsat at dette er en hyppig forespørgsel og skal optimeres, kan vi bruge et separat indeks, som kun indekserer de adresserækker, hvor distriktet er 'Californien':
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)
Forespørgslen læser nu kun indekset idx_address2 og rører ikke tabellensadresse .
Brug Multi-Value Indexes
Nogle kolonner, der skal indekseres, har muligvis ikke en skalær datatype. Kolonnetyper som jsonb , arrays og tsector har sammensatte eller flere værdier. Hvis du har brug for at indeksere sådanne kolonner, er det normalt også tilfældet, at du skal gennemsøge de individuelle værdier i disse kolonner.
Lad os prøve at finde alle filmtitler, der inkluderer billeder bag kulisserne. filmen tabellen har en tekstarray-kolonne kaldet special_features , som inkluderer tekstarray-elementet Behind The Scenes hvis en film har den funktion. For at finde alle sådanne film skal vi vælge alle rækker, der har "Behind The Scenes" ienhver af værdierne for arrayet special_features :
SELECT title FROM film WHERE special_features @> '{"Behind The Scenes"}';
Indeslutningsoperatøren @> kontrollerer, om venstre side er et supersæt af højre side.
Her er forespørgselsplanen:
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)
hvilket kræver en fuld scanning af bunken til en pris af 67.
Lad os se, om et almindeligt B-Tree-indeks hjælper:
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)
Indekset er ikke engang taget i betragtning. B-Tree-indekset aner ikke, at der er individuelle elementer i den værdi, det indekserede.
Det, vi har brug for, er et GIN-indeks.
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)
GIN-indekset er i stand til at understøtte matchning af den individuelle værdi mod den indekserede sammensatte værdi, hvilket resulterer i en forespørgselsplan med mindre end halvdelen af prisen for originalen.
Eliminér duplikerede indekser
Over tid akkumuleres indekser, og nogle gange bliver der tilføjet en, som har nøjagtig samme definition som en anden. Du kan bruge katalogvisningen pg_indexes
sammen med de menneskeligt læsbare SQL-definitioner af indekser. Du kan også nemt finde identiske definitioner:
SELECT array_agg(indexname) AS indexes, replace(indexdef, indexname, '') AS defn
FROM pg_indexes
GROUP BY defn
HAVING count(*) > 1;
Og her er resultatet, når det køres på stock pagila-databasen:
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-indekser
Det er også muligt, at du ender med flere indekser, hvor den ene indekserer en række kolonner, som den anden gør. Dette er måske eller måske ikke ønskværdigt – supersættet kan resultere i kun indeksscanninger, hvilket er en god ting, men det kan optage for meget plads, eller måske bruges den forespørgsel, det oprindeligt var beregnet til at optimere, ikke længere.
Hvis du ønsker at automatisere registreringen af sådanne indekser, er pg_catalog tablepg_index et godt udgangspunkt.
Ubrugte indekser
Efterhånden som de applikationer, der bruger databasen, udvikler sig, så udvikler de forespørgsler, de bruger. Indekser, der blev tilføjet tidligere, kan ikke længere bruges af nogen forespørgsel. Hver gang et indeks scannes, noteres det af statistikadministratoren, og det akkumulerede antal er tilgængeligt i systemkatalogvisningen pg_stat_user_indexes
som værdien idx_scan
. Overvågning af denne værdi over en periode (f.eks. en måned) giver en god idé om, hvilke indekser der er ubrugte og kan fjernes.
Her er forespørgslen for at få det aktuelle scanningsantal for alle indekser i det 'offentlige' skema:
SELECT relname, indexrelname, idx_scan
FROM pg_catalog.pg_stat_user_indexes
WHERE schemaname = 'public';
med output som dette:
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)
Genopbyg indekser med mindre låsning
Det er ikke ualmindeligt, at indekser skal genskabes. Indekser kan også blive oppustede, og genskabelse af indekset kan løse det, hvilket får det til at blive hurtigere at scanne. Indekser kan også blive korrupte. Ændring af indeksparametre kan også kræve genskabelse af indekset.
Aktiver oprettelse af parallel indeks
I PostgreSQL 11 er oprettelse af B-Tree-indeks samtidig. Det kan gøre brug af flere parallelle arbejdere for at fremskynde oprettelsen af indekset. Du skal dog sørge for, at disse konfigurationsindgange er indstillet korrekt:
SET max_parallel_workers = 32;
SET max_parallel_maintenance_workers = 16;
Standardværdierne er urimeligt små. Ideelt set bør disse tal stige med antallet af CPU-kerner. Se dokumenterne for mere information.
Opret indekser i baggrunden
Du kan også oprette et indeks i baggrunden ved at bruge SAMTIDIGT parameter for CREATE INDEX kommando:
pagila=# CREATE INDEX CONCURRENTLY idx_address1 ON address(district);
CREATE INDEX
Dette adskiller sig fra at lave et almindeligt oprette indeks ved, at det ikke kræver en låsning over bordet og derfor ikke låser udskrivninger. På den negative side tager det mere tid og ressourcer at fuldføre.