Ve světě Postgres jsou indexy nezbytné pro efektivní navigaci v úložišti tabulkových dat (neboli „hromadě“). Postgres neudržuje klastrování pro heap a architektura MVCC vede k více verzím stejného tuplelyingu. Vytváření a udržování účinných a efektivních indexů pro podporu aplikací je základní dovedností.
Čtěte dále a podívejte se na několik tipů pro optimalizaci a zlepšení používání indexů ve vašem nasazení.
Poznámka:Dotazy zobrazené níže se spouštějí v nemodifikované vzorové databázi.
Použití krycích indexů
Zvažte dotaz k načtení e-mailů všech neaktivních zákazníků. Zákazník tabulka má aktivní a dotaz je přímočarý:
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)
Dotaz vyžaduje úplné sekvenční prohledání tabulky zákazníků. Pojďme vytvořit index na aktivním sloupci:
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)
To pomáhá a sekvenční skenování se stalo „indexovým skenováním“. To znamená, že Postgres naskenuje index „idx_cust1“ a poté dále vyhledá v hromadě tabulky, aby načetl hodnoty ostatních sloupců (v tomto případě e-mail sloupec), který dotaz potřebuje.
PostgreSQL 11 zavedl krycí indexy. Tato funkce vám umožňuje zahrnout jeden nebo více dalších sloupců do samotného indexu – to znamená, že hodnoty těchto dalších sloupců jsou uloženy v úložišti dat indexu.
Pokud bychom použili tuto funkci a zahrnuli hodnotu e-mailu do indexu, Postgres se nebude muset dívat do hromady tabulky, aby získal hodnotuemail . Uvidíme, jestli to funguje:
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)
„Prohledávání pouze indexu“ nám říká, že dotaz je nyní zcela uspokojen samotným indexem, čímž se potenciálně vyhneme všem I/O disku pro čtení haldy tabulky.
Krycí indexy jsou zatím dostupné pouze pro indexy B-Stromu. Také náklady na udržování indexu krytí jsou přirozeně vyšší než u běžného indexu.
Použít částečné indexy
Částečné indexy indexují pouze podmnožinu řádků v tabulce. Díky tomu jsou indexy menší a jejich procházení je rychlejší.
Předpokládejme, že potřebujeme získat seznam e-mailů zákazníků v Kalifornii. Dotaz je:
SELECT c.email FROM customer c
JOIN address a ON c.address_id = a.address_id
WHERE a.district = 'California';
který má plán dotazů, který zahrnuje skenování obou tabulek, které jsou spojeny:
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)
Podívejme se, co nám přinese běžný index:
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)
Skenování adresy byla nahrazena indexovým skenováním přes idx_address1 a skenování hromady adres.
Za předpokladu, že se jedná o častý dotaz a je třeba jej optimalizovat, můžeme použít samostatný index, který indexuje pouze ty řádky adres, kde je okres ‚Kalifornie‘:
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)
Dotaz nyní čte pouze index idx_address2 a nedotýká seadresy stolu .
Použití indexů s více hodnotami
Některé sloupce, které vyžadují indexování, nemusí mít skalární datový typ. Typy sloupců jako jsonb , pole a tsvector mají složené nebo vícenásobné hodnoty. Potřebujete-li takové sloupce indexovat, obvykle se stává, že potřebujete prohledat i jednotlivé hodnoty v těchto sloupcích.
Pokusme se najít všechny filmové tituly, které obsahují zákulisní výstupy. film tabulka má sloupec textového pole s názvem special_features , který obsahuje prvek textového pole Behind The Scenes pokud film má tuto vlastnost. Abychom našli všechny takové filmy, musíme vybrat všechny řádky, které vjakémkoli obsahují „Behind The Scenes“ z hodnot pole special_features :
SELECT title FROM film WHERE special_features @> '{"Behind The Scenes"}';
Operátor kontejnmentu @> zkontroluje, zda je levá strana nadmnožinou pravé strany.
Zde je plán dotazů:
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)
což vyžaduje úplné skenování haldy za cenu 67.
Podívejme se, zda pomáhá pravidelný index B-stromu:
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)
S indexem se ani nepočítá. Index B-Tree netuší, že v hodnotě, kterou indexoval, jsou jednotlivé prvky.
Co potřebujeme, je index GIN.
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)
Index GIN je schopen podporovat shodu individuální hodnoty s indexovanou složenou hodnotou, což vede k plánu dotazů s méně než polovičními náklady oproti originálu.
Odstranění duplicitních indexů
Postupem času se indexy hromadí a někdy se přidá jeden, který má přesně stejnou definici jako jiný. Můžete použít katalogové zobrazení pg_indexes
získat pro člověka čitelné SQL definice indexů. Můžete také snadno zjistit identické definice:
SELECT array_agg(indexname) AS indexes, replace(indexdef, indexname, '') AS defn
FROM pg_indexes
GROUP BY defn
HAVING count(*) > 1;
A zde je výsledek při spuštění v databázi pagila:
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)
Indexy supermnožiny
Je také možné, že skončíte s více indexy, kde jeden indexuje jako superset sloupců, který dělá druhý. To může, ale nemusí být žádoucí – supermnožina může mít za následek skenování pouze na základě indexu, což je dobrá věc, ale může zabírat příliš mnoho místa nebo možná dotaz, který byl původně určen k optimalizaci, již není používán.
Pokud chcete automatizovat detekci takových indexů, pg_catalog tablepg_index je dobrým výchozím bodem.
Nepoužité indexy
Jak se vyvíjejí aplikace využívající databázi, vyvíjejí se i dotazy, které používají. Indexy, které byly přidány dříve, již nelze používat v žádném dotazu. Pokaždé, když je index naskenován, je zaznamenán správcem statistik a v zobrazení systémového katalogu pg_stat_user_indexes
je k dispozici kumulativní počet jako hodnotu idx_scan
. Sledování této hodnoty po určitou dobu (řekněme měsíc) poskytuje dobrou představu o tom, které indexy jsou nepoužívané a lze je odstranit.
Zde je dotaz na získání aktuálních počtů skenování pro všechny indexy ve schématu ‚public‘:
SELECT relname, indexrelname, idx_scan
FROM pg_catalog.pg_stat_user_indexes
WHERE schemaname = 'public';
s výstupem takto:
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)
Znovu sestavení indexů s menším zamykáním
Není neobvyklé, že je nutné indexy znovu vytvořit. Indexy se také mohou nafouknout a opětovné vytvoření indexu to může opravit, což způsobí, že bude rychlejší skenování. Indexy se také mohou poškodit. Změna parametrů indexu může také vyžadovat opětovné vytvoření indexu.
Povolit vytváření paralelního indexu
V PostgreSQL 11 je vytváření indexu B-Stromu souběžné. Pro urychlení vytváření indexu může využívat více paralelních pracovníků. Musíte se však ujistit, že tyto položky konfigurace jsou správně nastaveny:
SET max_parallel_workers = 32;
SET max_parallel_maintenance_workers = 16;
Výchozí hodnoty jsou nepřiměřeně malé. V ideálním případě by se tato čísla měla zvyšovat s počtem jader CPU. Další informace naleznete v dokumentaci.
Vytváření indexů na pozadí
Můžete také vytvořit index na pozadí pomocí SOUČASNĚ parametr CREATE INDEX příkaz:
pagila=# CREATE INDEX CONCURRENTLY idx_address1 ON address(district);
CREATE INDEX
To se liší od běžného vytváření indexu v tom, že nevyžaduje zámek nad tabulkou, a proto nezamyká zápisy. Nevýhodou je, že dokončení vyžaduje více času a zdrojů.