Выкарыстоўваны ўсе магчымасці азначнікаў у PostgreSQL

Выкарыстоўваны ўсе магчымасці азначнікаў у PostgreSQL
У свеце Postgres індэксы вельмі важныя для эфектыўнай навігацыі па сховішчы базы дадзеных (яго завуць "куча", heap). Postgres не падтрымлівае для яго кластарызацыю, і архітэктура MVCC прыводзіць да таго, што ў вас назапашваецца шмат версій аднаго і таго ж картэжа. Таму вельмі важна ўмець ствараць і суправаджаць эфектыўныя азначнікі для падтрымкі прыкладанняў.

Прапаную вашай увазе некалькі парад па аптымізацыі і паляпшэнню выкарыстання індэксаў.

Заўвага: паказаныя ніжэй запыты працуюць на не мадыфікаваным узоры базы дадзеных pagila.

Выкарыстанне якія пакрываюць азначнікаў (Covering Indexes)

Давайце разгледзім запыт на атрыманне адрасоў электроннай пошты для неактыўных карыстальнікаў. У табліцы customer ёсць калонка active, і запыт атрымліваецца нескладаным:

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)

У запыце выклікаецца поўная паслядоўнасць сканавання табліцы customer. Давайце створым азначнік для слупка active:

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)

Дапамагло, наступнае сканаванне ператварылася ўindex scan“. Гэта азначае, што Postgres праскануе індэксidx_cust1«, а затым далей працягне пошук па кучы табліцы, каб прачытаць значэння іншых калонак (у дадзеным выпадку, калонку email), якія патрэбныя запыту.

У PostgreSQL 11 з'явіліся якія пакрываюць азначнікі. Яны дазваляюць уключаць у сам індэкс адну або некалькі дадатковых калонак - іх значэння захоўваюцца ў сховішча дадзеных індэкса.

Калі б мы выкарыстоўвалі гэтую магчымасць і дадалі значэнне электроннай пошты ўнутр азначніка, то Postgres'у не спатрэбіцца шукаць у кучы табліцы значэнне email. Паглядзім, ці будзе гэта працаваць:

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)

«Index Only Scan» кажа нам, што запыту зараз досыць аднаго толькі азначніка, што дапамагае пазбягаць усіх дыскавых аперацый уводу/высновы для чытання кучы табліцы.

Сёння якія пакрываюць індэксы даступныя толькі для B-дрэваў. Аднак у гэтым выпадку намаганні па суправаджэнні будуць вышэйшыя.

Выкарыстанне частковых індэксаў

Частковыя індэксы індэксуюць толькі падмноства радкоў табліцы. Гэта дазваляе эканоміць памер азначнікаў і хутчэй выконваць сканаванне.

Дапусцім, нам трэба атрымаць спіс адрасоў электроннай пошты нашых кліентаў з Каліфорніі. Запыт будзе такім:

SELECT c.email FROM customer c
JOIN address a ON c.address_id = a.address_id
WHERE a.district = 'California';
which has a query plan that involves scanning both the tables that are joined:
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)

Што нам дадуць звычайныя азначнікі:

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)

сканаванне address было заменена сканаваннем індэкса idx_address1, а затым прасканаваная куча address.

Паколькі гэта часты запыт і яго трэба аптымізаваць, мы можам выкарыстоўваць частковы індэкс, які індэксуе толькі тыя радкі з адрасамі, у якіх раён ‘California’:

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)

Цяпер запыт счытвае толькі idx_address2 і не чапае табліцу address.

Выкарыстанне шматзначных індэксаў (Multi-Value Indexes)

Некаторыя калонкі, якія трэба праіндэксаваць, могуць не змяшчаць скалярнага тыпу дадзеных. Тыпы калонак накшталт jsonb, arrays и tsvector змяшчаць састаўныя або множныя значэнні. Калі вам трэба індэксаваць такія калонкі, то звычайна даводзіцца шукаць па ўсіх асобных значэннях у гэтых калонках.

Паспрабуем знайсці назвы ўсіх фільмаў, якія змяшчаюць нарэзкі з няўдалых дубляў. У табліцы film ёсць тэкставая калонка, якая называецца special_features. Калі ў фільма ёсць гэтая «асаблівая ўласцівасць», то ў калонцы змяшчаецца элемент у выглядзе тэкставага масіва. Behind The Scenes. Для пошуку ўсіх такіх фільмаў нам трэба абраць усе шэрагі з «Behind The Scenes» пры любых значэннях масіва special_features:

SELECT title FROM film WHERE special_features @> '{"Behind The Scenes"}';

Аператар укладзенасці (containment operator) @> правярае, ці з'яўляецца правая частка падмноствам левай часткі.

План запыту:

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)

Які запытвае поўнае сканіраванне кучы з коштам 67.

Паглядзім, ці дапаможа нам звычайны індэкс B-дрэва:

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)

Індэкс нават не разглядаўся. Індэкс B-дрэва не здагадваецца аб існаванні асобных элементаў у индексируемых значэннях.

Нам патрэбен 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)

GIN-індэкс падтрымлівае супастаўленне асобных значэнняў з праіндэксаванымі складовымі значэннямі, у выніку кошт плана запыту паменшыцца больш чым удвая.

Пазбаўляемся ад дублявання індэксаў

Індэксы назапашваюцца з часам, і часам новы індэкс можа змяшчаць тое ж вызначэнне, што і адзін з папярэдніх. Для атрымання зручных для чытання чалавекам SQL-вызначэнняў індэксаў можна выкарыстоўваць каталожнае ўяўленне pg_indexes. Вы таксама зможаце лёгка знаходзіць аднолькавыя азначэнні:

 SELECT array_agg(indexname) AS indexes, replace(indexdef, indexname, '') AS defn
    FROM pg_indexes
GROUP BY defn
  HAVING count(*) > 1;
And here’s the result when run on the stock pagila database:
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 Indexes)

Можа здарыцца так, што ў вас назапасіцца шмат азначнікаў, адзін з якіх індэксуе надмноства калонак, якія індэксуюць іншыя азначнікі. Гэта можа быць як пажадана, так і не - надмноства можа прывесці да сканавання толькі па індэксах, што добра, але пры гэтым яно можа займаць занадта шмат месца, або запыт, для аптымізацыі якога прызначалася гэта надмноства, ужо не выкарыстоўваецца.

Калі вам трэба аўтаматызаваць вызначэнне такіх індэксаў, то можна пачаць з pg_index з табліцы pg_catalog.

Невыкарыстоўваныя індэксы

Па меры развіцця прыкладанняў, якія выкарыстоўваюць базы даных, развіваюцца і выкарыстоўваюцца імі запыты. Дададзеныя раней індэксы могуць ужо не прымяняцца ніводным запытам. Пры кожным сканаванні індэкса ён адзначаецца дыспетчарам статыстыкі, і ва ўяўленні сістэмнага каталога pg_stat_user_indexes можна паглядзець значэнне idx_scan, якое з'яўляецца назапашвальным лічыльнікам. Адсочванне гэтага значэнне за нейкі прамежак часу (скажам, месяц) дасць добрае ўяўленне аб тым, якія індэксы не выкарыстоўваюцца і могуць быць выдаленыя.

Вось запыт на атрыманне бягучых лічыльнікаў сканавання ўсіх азначнікаў у схеме ‘public’:

SELECT relname, indexrelname, idx_scan
FROM   pg_catalog.pg_stat_user_indexes
WHERE  schemaname = 'public';
with output like this:
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)

Перастварэнне індэксаў з меншай колькасцю блакіровак

Часцяком індэксы даводзіцца пераствараць, напрыклад, калі яны раздзімаюцца ў памерах, і перастварэнне можа паскорыць сканіраванне. Таксама індэксы могуць пашкоджвацца. Змена параметраў азначніка таксама можа запатрабаваць яго перастварэння.

Уключаем паралельнае стварэнне індэксаў

У PostgreSQL 11 стварэнне азначніка B-Tree з'яўляецца канкурэнтным. Для паскарэння працэсу стварэння можа выкарыстоўвацца некалькі раўналежна якія працуюць воркераў. Аднак пераканайцеся, што гэтыя параметры канфігурацыі зададзены правільна:

SET max_parallel_workers = 32;
SET max_parallel_maintenance_workers = 16;

Значэнні па змаўчанні занадта малыя. У ідэале, гэтыя лікі трэба павялічваць разам з колькасцю ядраў працэсара. Больш падрабязна чытайце ў дакументацыі.

Фонавае стварэнне індэксаў

Вы можаце стварыць індэкс у фонавым рэжыме, скарыстаўшыся параметрам CONCURRENTLY каманды CREATE INDEX:

pagila=# CREATE INDEX CONCURRENTLY idx_address1 ON address(district);
CREATE INDEX

Гэтая працэдура стварэння азначніка адрозніваецца ад звычайнай тым, што яна не патрабуе блакавання табліцы, а значыць і не блакуе аперацыі запісу. З іншага боку, яна займае больш часу і спажывае больш рэсурсаў.

Postgres дае мноства гнуткіх магчымасцяў для стварэння індэксаў і шляхоў вырашэння любых прыватных выпадкаў, а гэтак жа дае спосабы кіравання базай дадзеных на выпадак выбухнога росту вашага прыкладання. Спадзяемся, што гэтыя парады дапамогуць вам зрабіць запыты хуткімі, а базу гатовай маштабавацца.

Крыніца: habr.com

Дадаць каментар