Při výuce školení PostgreSQL, jak na základní, tak na pokročilá témata, často zjišťuji, že účastníci mají velmi malou představu o tom, jak silné mohou být výrazové indexy (pokud si jich vůbec uvědomují). Dovolte mi tedy uvést stručný přehled.
Řekněme tedy, že máme tabulku s řadou časových razítek (ano, máme funkci create_series, která umí generovat data):
CREATE TABLE t AS SELECT d, repeat(md5(d::text), 10) AS padding FROM generate_series(timestamp '1900-01-01', timestamp '2100-01-01', interval '1 day') s(d); VACUUM ANALYZE t;
Součástí stolu je i výplňový sloupek, aby byl o něco větší. Nyní udělejme jednoduchý dotaz na rozsah a vybereme pouze jeden měsíc z ~200 let zahrnutých v tabulce. Pokud to v dotazu vysvětlíte, uvidíte něco takového:
EXPLAIN SELECT * FROM t WHERE d BETWEEN '2001-01-01' AND '2001-02-01'; QUERY PLAN ------------------------------------------------------------------------ Seq Scan on t (cost=0.00..4416.75 rows=32 width=332) Filter: ((d >= '2001-01-01 00:00:00'::timestamp without time zone) AND (d <= '2001-02-01 00:00:00'::timestamp without time zone)) (2 rows)
a na mém notebooku to běží za ~20 ms. Není to špatné, vezmeme-li v úvahu, že to musí projít celou tabulkou s ~75 tisíc řádky.
Ale pojďme vytvořit index ve sloupci časového razítka (všechny indexy zde jsou výchozí typ, tj. btree, pokud není výslovně uvedeno):
CREATE INDEX idx_t_d ON t (d);
A nyní zkusme spustit dotaz znovu:
QUERY PLAN ------------------------------------------------------------------------ Index Scan using idx_t_d on t (cost=0.29..9.97 rows=34 width=332) Index Cond: ((d >= '2001-01-01 00:00:00'::timestamp without time zone) AND (d <= '2001-02-01 00:00:00'::timestamp without time zone)) (2 rows)
a to běží za 0,5 ms, takže zhruba 40x rychleji. Ale to byly samozřejmě jednoduché indexy, vytvořené přímo na sloupci, nikoli výraz index. Předpokládejme tedy, že místo toho potřebujeme vybrat data z každého 1. dne každého měsíce a provést dotaz, jako je tento
SELECT * FROM t WHERE EXTRACT(day FROM d) = 1;
který však nemůže použít index, protože potřebuje vyhodnotit výraz ve sloupci, zatímco index je postaven na samotném sloupci, jak je znázorněno na EXPLAIN ANALYZE:
QUERY PLAN ------------------------------------------------------------------------ Seq Scan on t (cost=0.00..4416.75 rows=365 width=332) (actual time=0.045..40.601 rows=2401 loops=1) Filter: (date_part('day'::text, d) = '1'::double precision) Rows Removed by Filter: 70649 Planning time: 0.209 ms Execution time: 43.018 ms (5 rows)
Takže to musí nejen provést sekvenční skenování, ale také provést vyhodnocení, čímž se doba trvání dotazu prodlouží na 43 ms.
Databáze nemůže použít index z několika důvodů. Indexy (alespoň indexy btree) se spoléhají na dotazování setříděných dat poskytovaných stromovou strukturou, a zatímco dotaz na rozsah z toho může těžit, druhý dotaz (s voláním `extract`) nikoli.
Poznámka:Dalším problémem je, že množina operátorů podporovaných indexy (tj. které lze hodnotit přímo na indexech) je velmi omezená. A funkce „extrahovat“ není podporována, takže dotaz nemůže vyřešit problém s objednáváním pomocí skenování indexu bitmapy.
Teoreticky by se databáze mohla pokusit transformovat podmínku na podmínky rozsahu, ale to je extrémně obtížné a specifické pro vyjádření. V tomto případě bychom museli generovat nekonečný počet takových „denních“ rozsahů, protože plánovač ve skutečnosti nezná minimální/maximální časová razítka v tabulce. Databáze se tedy ani nepokouší.
Ale zatímco databáze neví, jak podmínky transformovat, vývojáři často ano. Například s podmínkami jako
(column + 1) >= 1000
není těžké to takto přepsat
column >= (1000 - 1)
což funguje dobře s indexy.
Ale co když taková transformace není možná, jako například u příkladu dotazu
SELECT * FROM t WHERE EXTRACT(day FROM d) = 1;
V tomto případě by vývojář musel čelit stejnému problému s neznámým min/max pro sloupec d, a i tak by to generovalo mnoho rozsahů.
Tento blogový příspěvek je o výrazových indexech a zatím jsme používali pouze běžné indexy postavené přímo na sloupci. Vytvořme tedy první index výrazu:
CREATE INDEX idx_t_expr ON t ((extract(day FROM d))); ANALYZE t;
což nám dává tento plán vysvětlení
QUERY PLAN ------------------------------------------------------------------------ Bitmap Heap Scan on t (cost=47.35..3305.25 rows=2459 width=332) (actual time=2.400..12.539 rows=2401 loops=1) Recheck Cond: (date_part('day'::text, d) = '1'::double precision) Heap Blocks: exact=2401 -> Bitmap Index Scan on idx_t_expr (cost=0.00..46.73 rows=2459 width=0) (actual time=1.243..1.243 rows=2401 loops=1) Index Cond: (date_part('day'::text, d) = '1'::double precision) Planning time: 0.374 ms Execution time: 17.136 ms (7 rows)
Takže i když nám to nedává stejné 40násobné zrychlení jako index v prvním příkladu, je to tak trochu očekávané, protože tento dotaz vrací mnohem více n-tic (2401 vs. 32). Navíc jsou rozprostřeny po celé tabulce a nejsou tak lokalizované jako v prvním příkladu. Je to tedy pěkné 2x zrychlení a v mnoha skutečných případech uvidíte mnohem větší vylepšení.
Ale schopnost používat indexy pro podmínky se složitými výrazy zde není tou nejzajímavější informací – to je tak trochu důvod, proč lidé vytvářejí indexy výrazů. Ale to není jediná výhoda.
Pokud se podíváte na dva výše uvedené plány vysvětlení (bez a s indexem výrazů), můžete si všimnout tohoto:
QUERY PLAN ------------------------------------------------------------------------ Seq Scan on t (cost=0.00..4416.75 rows=365 width=332) (actual time=0.045..40.601 rows=2401 loops=1) ...
QUERY PLAN ------------------------------------------------------------------------ Bitmap Heap Scan on t (cost=47.35..3305.25 rows=2459 width=332) (actual time=2.400..12.539 rows=2401 loops=1) ...
Vpravo – vytvoření indexu výrazů výrazně zlepšilo odhady. Bez indexu máme pouze statistiky (MCV + histogram) pro nezpracované sloupce tabulky, takže databáze neví, jak výraz odhadnout
EXTRACT(day FROM d) = 1
Místo toho tedy použije výchozí odhad podmínek rovnosti, což je 0,5 % všech řádků – protože tabulka má 73050 řádků, skončíme s odhadem pouhých 365 řádků. V aplikacích v reálném světě je běžné vidět mnohem horší chyby v odhadech.
S indexem však databáze také shromažďovala statistiky o sloupcích indexu a v tomto případě sloupec obsahuje výsledky výrazu. A při plánování si toho optimalizátor všimne a vytvoří mnohem lepší odhad.
To je obrovská výhoda a může pomoci s opravou některých případů špatných plánů dotazů způsobených nepřesnými odhady. Přesto většina lidí o tomto praktickém nástroji neví.
A užitečnost tohoto nástroje vzrostla až se zavedením datového typu JSONB ve verzi 9.4, protože je to asi jediný způsob, jak shromažďovat statistiky o obsahu dokumentů JSONB.
Při indexování dokumentů JSONB existují dvě základní strategie indexování. Můžete buď vytvořit index GIN/GiST na celém dokumentu, např. takhle
CREATE INDEX ON t USING GIN (jsonb_column);
což vám umožňuje dotazovat se na libovolné cesty ve sloupci JSONB, používat operátor kontejnmentu k přiřazování dílčích dokumentů atd. To je skvělé, ale stále máte pouze základní statistiky pro jednotlivé sloupce, které
není příliš užitečné jako dokumenty jsou považovány za skalární hodnoty (a nikdo neodpovídá celým dokumentům ani nepoužívá rozsah dokumentů).
Indexy výrazů, vytvořené například takto:
CREATE INDEX ON t ((jsonb_column->'id'));
bude užitečný pouze pro konkrétní výraz, tj. tento nově vytvořený index bude užitečný pro
SELECT * FROM t WHERE jsonb_column ->> 'id' = 123;
ale ne pro dotazy s přístupem k jiným klíčům JSON, jako je například ‘value’
SELECT * FROM t WHERE jsonb_column ->> 'value' = 'xxxx';
Tím neříkám, že GIN/GiST indexy na celém dokumentu jsou zbytečné, ale musíte si vybrat. Buď vytvoříte index zaměřený na výraz, který je užitečný při dotazování na konkrétní klíč as přidanou výhodou statistiky výrazu. Nebo vytvoříte index GIN/GiST na celém dokumentu, který dokáže zpracovat dotazy na libovolné klíče, ale bez statistik.
Můžete si ale dát dort a sníst ho i v tomto případě, protože můžete vytvořit oba indexy současně a databáze si vybere, který z nich použije pro jednotlivé dotazy. A díky indexům výrazů budete mít přesné statistiky.
Bohužel nemůžete sníst celý koláč, protože výrazové indexy a indexy GIN/GiST používají různé podmínky
-- expression (btree) SELECT * FROM t WHERE jsonb_column ->> 'id' = 123; -- GIN/GiST SELECT * FROM t WHERE jsonb_column @> '{"id" : 123}';
takže je plánovač nemůže použít současně – výrazové indexy pro odhad a GIN/GiST pro provedení.