Когато преподавам обучения на PostgreSQL, както по основни, така и по напреднали теми, често откривам, че присъстващите нямат много малка представа колко мощни могат да бъдат индексите на изрази (ако изобщо са наясно с тях). Така че позволете ми да ви дам кратък преглед.
И така, да кажем, че имаме таблица с набор от времеви печати (да, имаме функция generate_series, която може да генерира дати):
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;
Таблицата включва и колона за допълване, за да я направи малко по-голяма. Сега, нека направим проста заявка за диапазон, като изберете само един месец от ~200 години, включени в таблицата. Ако обясните заявката, ще видите нещо подобно:
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)
и на моя лаптоп това работи за ~20ms. Не е лошо, като се има предвид, че това трябва да премине през цялата таблица с ~75k реда.
Но нека създадем индекс в колоната с времеви отпечатък (всички индекси тук са типът по подразбиране, т.е. btree, освен ако не е упоменато изрично):
CREATE INDEX idx_t_d ON t (d);
И сега нека се опитаме да изпълним заявката отново:
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)
и това се изпълнява за 0,5 мс, така че приблизително 40 пъти по-бързо. Но това, разбира се, бяха обикновени индекси, създадени директно върху колоната, а не индекс на израза. Така че нека приемем, че вместо това трябва да избираме данни от всеки 1-ви ден на всеки месец, като правим заявка като тази
SELECT * FROM t WHERE EXTRACT(day FROM d) = 1;
който обаче не може да използва индекса, тъй като трябва да оцени израз в колоната, докато индексът е изграден върху самата колона, както е показано в 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)
Така че не само това трябва да извърши последователно сканиране, но също така трябва да направи оценка, увеличавайки продължителността на заявката до 43 мс.
Базата данни не може да използва индекса по множество причини. Индексите (поне btree индекси) разчитат на заявка за сортирани данни, предоставени от дървоподобната структура, и докато заявката за диапазон може да се възползва от това, втората заявка (с извикване на „extract“) не може.
Забележка:Друг проблем е, че наборът от оператори, поддържани от индекси (т.е. които могат да бъдат оценени директно върху индекси), е много ограничен. Функцията „извличане“ не се поддържа, така че заявката не може да заобиколи проблема с поръчката чрез сканиране на растерни индекси.
На теория базата данни може да се опита да трансформира условието в условия за диапазон, но това е изключително трудно и специфично за изразяване. В този случай ще трябва да генерираме безкраен брой такива диапазони „на ден“, тъй като плановникът всъщност не знае мин./макс. времеви отпечатъци в таблицата. Така че базата данни дори не се опитва.
Но докато базата данни не знае как да трансформира условията, разработчиците често го правят. Например с условия като
(column + 1) >= 1000
не е трудно да го пренапишеш така
column >= (1000 - 1)
което работи добре с индексите.
Но какво ще стане, ако такава трансформация не е възможна, като например за примерната заявка
SELECT * FROM t WHERE EXTRACT(day FROM d) = 1;
В този случай разработчикът ще трябва да се сблъска със същия проблем с неизвестни мин./макс. за колоната d и дори тогава ще генерира много диапазони.
Е, тази публикация в блога е за индексите на изрази и досега използвахме само обикновени индекси, изградени директно върху колоната. И така, нека създадем първия индекс на израза:
CREATE INDEX idx_t_expr ON t ((extract(day FROM d))); ANALYZE t;
което след това ни дава този план за обяснение
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)
Така че макар това да не ни дава същата 40-кратна скорост като индекса в първия пример, това е малко очаквано, тъй като тази заявка връща много повече кортежи (2401 срещу 32). Освен това те са разпределени в цялата таблица и не са толкова локализирани, както в първия пример. Така че това е хубаво 2x ускорение и в много случаи в реалния свят ще видите много по-големи подобрения.
Но способността да се използват индекси за условия със сложни изрази не е най-интересната информация тук – това е причината хората да създават индекси на изрази. Но това не е единствената полза.
Ако погледнете двата плана за обяснение, представени по-горе (без и с индекса на израза), може да забележите това:
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) ...
Вдясно – създаването на индекса на изразите значително подобри оценките. Без индекса имаме само статистика (MCV + хистограма) за необработени колони на таблицата, така че базата данни не знае как да оцени израза
EXTRACT(day FROM d) = 1
Така че вместо това прилага оценка по подразбиране за условия на равенство, която е 0,5% от всички редове – тъй като таблицата има 73050 реда, в крайна сметка получаваме оценка от само 365 реда. Обичайно е да виждате много по-лоши грешки при оценката в приложенията в реалния свят.
С индекса обаче базата данни събира и статистически данни за колоните на индекса и в този случай колоната съдържа резултати от израза. И докато планира, оптимизаторът забелязва това и прави много по-добра оценка.
Това е огромно предимство и може да помогне за коригиране на някои случаи на лоши планове за заявки, причинени от неточни оценки. И все пак повечето хора не знаят за този удобен инструмент.
И полезността на този инструмент се увеличи само с въвеждането на JSONB тип данни в 9.4, тъй като това е единственият начин за събиране на статистически данни за съдържанието на JSONB документите.
Когато индексирате JSONB документи, съществуват две основни стратегии за индексиране. Можете да създадете GIN/GiST индекс за целия документ, напр. така
CREATE INDEX ON t USING GIN (jsonb_column);
което ви позволява да заявявате произволни пътища в колоната JSONB, да използвате оператор за ограничаване, за да съпоставите поддокументи и т.н. Това е чудесно, но все още имате само основната статистика за колона, която
не е много полезна като документите се третират като скаларни стойности (и никой не съпоставя цели документи или не използва диапазон от документи).
Индекси на изрази, например създадени така:
CREATE INDEX ON t ((jsonb_column->'id'));
ще бъде полезен само за конкретния израз, т.е. този новосъздадения индекс ще бъде полезен за
SELECT * FROM t WHERE jsonb_column ->> 'id' = 123;
но не и за заявки за достъп до други JSON ключове, като „стойност“ например
SELECT * FROM t WHERE jsonb_column ->> 'value' = 'xxxx';
Това не означава, че индексите на GIN/GiST в целия документ са безполезни, но трябва да изберете. Или създавате фокусиран индекс на изрази, полезен при заявка за конкретен ключ и с допълнителната полза от статистиката за израза. Или създавате GIN/GiST индекс за целия документ, който може да обработва заявки за произволни ключове, но без статистика.
В този случай обаче можете да хапнете торта и да я ядете, защото можете да създадете и двата индекса едновременно и базата данни ще избере кой от тях да използва за отделни заявки. И ще имате точни статистически данни, благодарение на индексите на изразите.
За съжаление не можете да изядете цялата торта, защото индексите на изрази и индексите GIN/GiST използват различни условия
-- expression (btree) SELECT * FROM t WHERE jsonb_column ->> 'id' = 123; -- GIN/GiST SELECT * FROM t WHERE jsonb_column @> '{"id" : 123}';
така че плановникът не може да ги използва едновременно – индекси на изрази за оценка и GIN/GiST за изпълнение.