I Postgres-världen är index väsentliga för att effektivt navigera i tabelldatalagringen (aka "högen"). Postgres upprätthåller inte en klustring för heapen, och MVCC-arkitekturen leder till att flera versioner av samma tuplar runt. Att skapa och underhålla effektiva och effektiva index för att stödja applikationer är en viktig färdighet.
Läs vidare för att kolla in några tips om hur du optimerar och förbättrar användningen av index i din implementering.
Obs:Frågor som visas nedan körs på en omodifierad pagila-exempeldatabas.
Använd täckande index
Överväg en fråga för att hämta e-postmeddelanden från alla inaktiva kunder. kunden tabellen har en aktiv kolumn, och frågan är enkel:
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)
Frågan kräver en fullständig sekventiell genomsökning av kundtabellen. Låt oss skapa ett index på den aktiva kolumnen:
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)
Detta hjälper, och den sekventiella skanningen har blivit en "indexskanning". Det betyder att Postgres kommer att skanna indexet "idx_cust1" och sedan slå upp tabellens hög ytterligare för att läsa de andra kolumnvärdena (i det här fallet, e-post kolumn) som frågan behöver.
PostgreSQL 11 introducerade täckande index. Den här funktionen låter dig inkludera en eller flera ytterligare kolumner i själva indexet – det vill säga värdena för dessa extra kolumner lagras i indexdatalagringen.
Om vi skulle använda den här funktionen och inkludera värdet av e-post i indexet, behöver Postgres inte titta in i tabellens hög för att få värdet ave-post . Låt oss se om detta fungerar:
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)
"Endast indexsökning" berättar för oss att frågan nu är helt tillfredsställd av själva indexet, vilket potentiellt undviker all disk I/O för att läsa tabellens hög.
Täckande index är endast tillgängliga för B-Tree-index från och med nu. Dessutom är kostnaden för att upprätthålla ett täckande index naturligtvis högre än ett vanligt.
Använd partiella index
Partiella index indexerar bara en delmängd av raderna i en tabell. Detta håller indexen mindre i storlek och snabbare att skanna igenom.
Anta att vi behöver få en lista över e-postmeddelanden från kunder i Kalifornien. Frågan är:
SELECT c.email FROM customer c
JOIN address a ON c.address_id = a.address_id
WHERE a.district = 'California';
som har en frågeplan som innebär att man skannar båda tabellerna som är sammanfogade:
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)
Låt oss se vad ett vanligt index ger oss:
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)
Genomsökningen av adress har ersatts med en indexsökning över idx_address1 ,och en skanning av adressens hög.
Förutsatt att detta är en frekvent fråga och behöver optimeras, kan vi använda separata index som bara indexerar de adressrader där distriktet är 'Kalifornien':
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)
Frågan läser nu bara indexet idx_address2 och rör inte tabellensadress .
Använd flervärdesindex
Vissa kolumner som behöver indexeras kanske inte har en skalär datatyp. Kolumntyper som jsonb , matriser och tsector har sammansatta eller flera värden. Om du behöver indexera sådana kolumner är det vanligtvis så att du behöver söka igenom de individuella värdena i dessa kolumner också.
Låt oss försöka hitta alla filmtitlar som inkluderar bilder bakom kulisserna. filmen tabellen har en textmatriskolumn som heter special_features , som inkluderar textarrayelementet Behind The Scenes om en film har den funktionen. För att hitta alla sådana filmer måste vi välja alla rader som har "Behind The Scenes" ialla av värdena för arrayen special_features :
SELECT title FROM film WHERE special_features @> '{"Behind The Scenes"}';
Inneslutningsoperatören @> kontrollerar om den vänstra sidan är en superset av den högra sidan.
Här är frågeplanen:
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)
vilket kräver en fullständig genomsökning av högen, till en kostnad av 67.
Låt oss se om ett vanligt B-Tree-index 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)
Indexet beaktas inte ens. B-Tree-indexet har ingen aning om att det finns individuella element i värdet det indexerade.
Vad vi behöver är ett 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)
GIN-indexet kan stödja matchning av det individuella värdet mot det indexerade sammansatta värdet, vilket resulterar i en frågeplan med mindre än halva kostnaden för originalet.
Eliminera dubbletter av index
Med tiden ackumuleras index, och ibland läggs en till som har exakt samma definition som en annan. Du kan använda katalogvyn pg_indexes
för att få de mänskliga läsbara SQL-definitionerna av index. Du kan också enkelt upptäcka identiska definitioner:
SELECT array_agg(indexname) AS indexes, replace(indexdef, indexname, '') AS defn
FROM pg_indexes
GROUP BY defn
HAVING count(*) > 1;
Och här är resultatet när det körs 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-index
Det är också möjligt att du slutar med flera index där den ena indexerar över en uppsättning av kolumner som den andra gör. Detta kan vara önskvärt eller inte – superuppsättningen kan resultera i enbart indexsökningar, vilket är bra, men det kan ta för mycket utrymme, eller så kanske frågan som den ursprungligen var tänkt att optimera inte används längre.
Om du vill automatisera upptäckten av sådana index, är pg_catalog tablepg_index en god utgångspunkt.
Oanvända index
I takt med att applikationerna som använder databasen utvecklas, så utvecklas också frågorna som de använder. Index som lades till tidigare får inte längre användas av någon fråga. Varje gång ett index skannas, noteras det av statistikhanteraren och det ackumulerade antalet är tillgängligt i systemkatalogvyn pg_stat_user_indexes
som värdet idx_scan
. Att övervaka detta värde över en tidsperiod (säg en månad) ger en god uppfattning om vilka index som är oanvända och kan tas bort.
Här är frågan för att få de aktuella skanningsantalerna för alla index i det "offentliga" schemat:
SELECT relname, indexrelname, idx_scan
FROM pg_catalog.pg_stat_user_indexes
WHERE schemaname = 'public';
med utdata så här:
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)
Återskapa index med mindre låsning
Det är inte ovanligt att index behöver återskapas. Index kan också bli uppblåsta, och att återskapa indexet kan fixa det, vilket gör att det går snabbare att skanna. Index kan också bli korrupta. Ändring av indexparametrar kan också behöva återskapa indexet.
Aktivera skapande av parallellt index
I PostgreSQL 11 skapas B-Tree-index samtidigt. Den kan använda sig av flera parallella arbetare för att påskynda skapandet av indexet. Du måste dock se till att dessa konfigurationsposter är inställda på lämpligt sätt:
SET max_parallel_workers = 32;
SET max_parallel_maintenance_workers = 16;
Standardvärdena är orimligt små. Helst bör dessa siffror öka med antalet CPU-kärnor. Se dokumenten för mer information.
Skapa index i bakgrunden
Du kan också skapa ett index i bakgrunden med hjälp av SAMTIDIGT parametern för CREATE INDEX kommando:
pagila=# CREATE INDEX CONCURRENTLY idx_address1 ON address(district);
CREATE INDEX
Detta skiljer sig från att göra ett vanligt skapande index genom att det inte kräver ett lås över bordet och därför inte låser ut skrivningar. På nackdelen tar det mer tid och resurser att slutföra.