Индексирането на база данни е използването на специални структури от данни, които целят подобряване на производителността чрез постигане на директен достъп до страниците с данни. Индексът на базата данни работи като индексната секция на печатна книга:гледайки в индексната секция, е много по-бързо да идентифицираме страницата(ите), които съдържат термина, който ни интересува. Можем лесно да намерим страниците и да получим директен достъп до тях . Това е вместо последователно сканиране на страниците на книгата, докато намерим термина, който търсим.
Индексите са основен инструмент в ръцете на DBA. Използването на индекси може да осигури големи печалби в производителността за различни области на данни. PostgreSQL е известен със своята голяма разширяемост и богатата колекция от основни добавки и добавки на трети страни и индексирането не е изключение от това правило. Индексите на PostgreSQL покриват богат спектър от случаи, от най-простите индекси на b-дърво за скаларни типове до геопространствени GiST индекси до fts или json или масив GIN индекси.
Индексите обаче, колкото и прекрасни да изглеждат (и всъщност са!), не идват безплатно. Има известно наказание, което върви с записите в индексирана таблица. Така че DBA, преди да проучи възможностите си за създаване на конкретен индекс, първо трябва да се увери, че споменатият индекс има смисъл на първо място, което означава, че печалбите от създаването му ще надвишават загубата на производителност при записвания.
Терминология на основния индекс на PostgreSQL
Преди да опишем типовете индекси в PostgreSQL и тяхното използване, нека да разгледаме някаква терминология, която всеки DBA ще срещне рано или късно, когато чете документите.
- Метод за достъп до индекс (наричан още като Метод на достъп ):Типът на индекса (B-tree, GiST, GIN и т.н.)
- Тип: типа данни на индексираната колона
- Оператор: функция между два типа данни
- Семейство оператори: оператор за кръстосани типове данни, чрез групиране на оператори от типове с подобно поведение
- Клас на оператора (също споменава като индексна стратегия ):дефинира операторите, които да се използват от индекса за колона
В системния каталог на PostgreSQL методите за достъп се съхраняват в pg_am, операторските класове в pg_opclass, семействата оператори в pg_opfamily. Зависимостите на горното са показани на диаграмата по-долу:
Типове индекси в PostgreSQL
PostgreSQL предоставя следните типове индекси:
- B-дърво: индексът по подразбиране, приложим за типове, които могат да бъдат сортирани
- Хеш: се справя само с равенството
- Общност: подходящ за нескаларни типове данни (напр. геометрични форми, fts, масиви)
- SP-GiST: GIST, разделен на пространство, еволюция на GiST за работа с небалансирани структури (квадродърва, k-d дървета, коренови дървета)
- ДЖИН: подходящ за сложни типове (напр. jsonb, fts, масиви)
- BRIN: сравнително нов тип индекс, който поддържа данни, които могат да бъдат сортирани чрез съхраняване на мин./максимални стойности във всеки блок.
Ниско ще се опитаме да си изцапаме ръцете с някои примери от реалния свят. Всички дадени примери са направени с PostgreSQL 10.0 (с 10 и 9 psql клиенти) на FreeBSD 11.1.
Индекси на B-дърво
Да предположим, че имаме следната таблица:
create table part (
id serial primary key,
partno varchar(20) NOT NULL UNIQUE,
partname varchar(80) NOT NULL,
partdescr text,
machine_id int NOT NULL
);
testdb=# \d part
Table "public.part"
Column | Type | Modifiers
------------+-----------------------+---------------------------------------------------
id | integer | not null default nextval('part_id_seq'::regclass)
partno | character varying(20)| not null
partname | character varying(80)| not null
partdescr | text |
machine_id | integer | not null
Indexes:
"part_pkey" PRIMARY KEY, btree (id)
"part_partno_key" UNIQUE CONSTRAINT, btree (partno)
Когато дефинираме тази доста обща таблица, PostgreSQL създава два уникални индекса на B-дървото зад кулисите:part_pkey и part_partno_key. Така че всяко уникално ограничение в PostgreSQL се изпълнява с уникален INDEX. Нека попълним нашата таблица с милион реда данни:
testdb=# with populate_qry as (select gs from generate_series(1,1000000) as gs )
insert into part (partno, partname,machine_id) SELECT 'PNo:'||gs, 'Part '||gs,0 from populate_qry;
INSERT 0 1000000
Сега нека се опитаме да направим някои запитвания на нашата маса. Първо казваме на psql клиента да отчита времената на заявка, като напише \timing:
testdb=# select * from part where id=100000;
id | partno | partname | partdescr | machine_id
--------+------------+-------------+-----------+------------
100000 | PNo:100000 | Part 100000 | | 0
(1 row)
Time: 0,284 ms
testdb=# select * from part where partno='PNo:100000';
id | partno | partname | partdescr | machine_id
--------+------------+-------------+-----------+------------
100000 | PNo:100000 | Part 100000 | | 0
(1 row)
Time: 0,319 ms
Забелязваме, че са необходими само части от милисекундата, за да получим нашите резултати. Очаквахме това, тъй като и за двете колони, използвани в горните заявки, вече сме дефинирали подходящите индекси. Сега нека се опитаме да направим заявка за име на колона, за която не съществува индекс.
testdb=# select * from part where partname='Part 100000';
id | partno | partname | partdescr | machine_id
--------+------------+-------------+-----------+------------
100000 | PNo:100000 | Part 100000 | | 0
(1 row)
Time: 89,173 ms
Тук виждаме ясно, че за неиндексираната колона производителността спада значително. Сега нека създадем индекс за тази колона и повторете заявката:
testdb=# create index part_partname_idx ON part(partname);
CREATE INDEX
Time: 15734,829 ms (00:15,735)
testdb=# select * from part where partname='Part 100000';
id | partno | partname | partdescr | machine_id
--------+------------+-------------+-----------+------------
100000 | PNo:100000 | Part 100000 | | 0
(1 row)
Time: 0,525 ms
Нашият нов индекс part_partname_idx също е индекс на B-дърво (по подразбиране). Първо отбелязваме, че създаването на индекс в таблицата с милиони редове отне значително време, около 16 секунди. След това наблюдаваме, че скоростта на нашата заявка е увеличена от 89 ms на 0,525 ms. Индексите на B-дървото, освен проверка за равенство, могат да помогнат и при заявки, включващи други оператори за подредени типове, като <,<=,>=,>. Нека опитаме с <=и>=
testdb=# select count(*) from part where partname>='Part 9999900';
count
-------
9
(1 row)
Time: 0,359 ms
testdb=# select count(*) from part where partname<='Part 9999900';
count
--------
999991
(1 row)
Time: 355,618 ms
Първата заявка е много по-бърза от втората, като използваме ключовите думи EXPLAIN (или EXPLAIN ANALYZE), можем да видим дали се използва действителният индекс или не:
testdb=# explain select count(*) from part where partname>='Part 9999900';
QUERY PLAN
-----------------------------------------------------------------------------------------
Aggregate (cost=8.45..8.46 rows=1 width=8)
-> Index Only Scan using part_partname_idx on part (cost=0.42..8.44 rows=1 width=0)
Index Cond: (partname >= 'Part 9999900'::text)
(3 rows)
Time: 0,671 ms
testdb=# explain select count(*) from part where partname<='Part 9999900';
QUERY PLAN
----------------------------------------------------------------------------------------
Finalize Aggregate (cost=14603.22..14603.23 rows=1 width=8)
-> Gather (cost=14603.00..14603.21 rows=2 width=8)
Workers Planned: 2
-> Partial Aggregate (cost=13603.00..13603.01 rows=1 width=8)
-> Parallel Seq Scan on part (cost=0.00..12561.33 rows=416667 width=0)
Filter: ((partname)::text <= 'Part 9999900'::text)
(6 rows)
Time: 0,461 ms
В първия случай плановникът на заявки избира да използва индекса part_partname_idx. Също така отбелязваме, че това ще доведе до сканиране само с индекс, което означава, че изобщо няма достъп до таблиците с данни. Във втория случай плановникът определя, че няма смисъл да се използва индексът, тъй като върнатите резултати са голяма част от таблицата, в който случай се смята, че последователното сканиране е по-бързо.
Хеш индекси
Използването на хеш индекси до и включително PgSQL 9.6 беше обезкуражено поради причини, свързани с липсата на WAL запис. От PgSQL 10.0 тези проблеми бяха отстранени, но все пак хеш индексите нямаха никакъв смисъл да се използват. В PgSQL 11 има усилия да направи хеш индексите първокласен индексен метод заедно с по-големите му братя (B-tree, GiST, GIN). И така, имайки това предвид, нека всъщност опитаме хеш индекс в действие.
Ще обогатим нашата таблица с части с нов тип на колона и ще я попълним със стойности с еднакво разпределение, след което ще изпълним заявка, която тества за тип част, равен на „Steering“:
testdb=# alter table part add parttype varchar(100) CHECK (parttype in ('Engine','Suspension','Driveline','Brakes','Steering','General')) NOT NULL DEFAULT 'General';
ALTER TABLE
Time: 42690,557 ms (00:42,691)
testdb=# with catqry as (select id,(random()*6)::int % 6 as cat from part)
update part SET parttype = CASE WHEN cat=1 THEN 'Engine' WHEN cat=2 THEN 'Suspension' WHEN cat=3 THEN 'Driveline' WHEN cat=4 THEN 'Brakes' WHEN cat=5 THEN 'Steering' ELSE 'General' END FROM catqry WHERE part.id=catqry.id;
UPDATE 1000000
Time: 46345,386 ms (00:46,345)
testdb=# select count(*) from part where id % 500 = 0 AND parttype = 'Steering';
count
-------
322
(1 row)
Time: 93,361 ms
Сега създаваме хеш индекс за тази нова колона и опитваме отново предишната заявка:
testdb=# create index part_parttype_idx ON part USING hash(parttype);
CREATE INDEX
Time: 95525,395 ms (01:35,525)
testdb=# analyze ;
ANALYZE
Time: 1986,642 ms (00:01,987)
testdb=# select count(*) from part where id % 500 = 0 AND parttype = 'Steering';
count
-------
322
(1 row)
Time: 63,634 ms
Отбелязваме подобрението след използване на хеш индекса. Сега ще сравним ефективността на хеш индекс за цели числа с еквивалентния индекс на b-дърво.
testdb=# update part set machine_id = id;
UPDATE 1000000
Time: 392548,917 ms (06:32,549)
testdb=# select * from part where id=500000;
id | partno | partname | partdescr | machine_id | parttype
--------+------------+-------------+-----------+------------+------------
500000 | PNo:500000 | Part 500000 | | 500000 | Suspension
(1 row)
Time: 0,316 ms
testdb=# select * from part where machine_id=500000;
id | partno | partname | partdescr | machine_id | parttype
--------+------------+-------------+-----------+------------+------------
500000 | PNo:500000 | Part 500000 | | 500000 | Suspension
(1 row)
Time: 97,037 ms
testdb=# create index part_machine_id_idx ON part USING hash(machine_id);
CREATE INDEX
Time: 4756,249 ms (00:04,756)
testdb=#
testdb=# select * from part where machine_id=500000;
id | partno | partname | partdescr | machine_id | parttype
--------+------------+-------------+-----------+------------+------------
500000 | PNo:500000 | Part 500000 | | 500000 | Suspension
(1 row)
Time: 0,297 ms
Както виждаме, с използването на хеш индекси, скоростта на заявките, които проверяват за равенство, е много близка до скоростта на индексите на B-дърво. Твърди се, че хеш индексите са малко по-бързи за равенство от B-дървесата, всъщност трябваше да опитаме всяка заявка два или три пъти, докато хеш индексът даде по-добър резултат от еквивалента на b-дървото.
Изтеглете Бялата книга днес Управление и автоматизация на PostgreSQL с ClusterControl Научете какво трябва да знаете, за да внедрите, наблюдавате, управлявате и мащабирате PostgreSQLD Изтеглете Бялата книгаGiST индекси
GiST (Generalized Search Tree) е повече от един вид индекс, а по-скоро инфраструктура за изграждане на много стратегии за индексиране. Разпределението на PostgreSQL по подразбиране осигурява поддръжка за геометрични типове данни, tsquery и tsvector. В contrib има реализации на много други операторски класове. Като чете документите и директорията contrib, читателят ще забележи, че има доста голямо припокриване между случаите на използване на GiST и GIN:int масиви, пълно текстово търсене за назоваване на основните случаи. В тези случаи GIN е по-бърз и в официалната документация изрично е посочено това. Въпреки това, GiST предоставя обширна поддръжка на геометрични типове данни. Също така, както по време на това писане, GiST (и SP-GiST) е единственият смислен метод, който може да се използва с ограничения за изключване. Ще видим пример за това. Да предположим (задържайки се в областта на машиностроенето), че имаме изискване да дефинираме вариации на типа машини за определен тип машина, които са валидни за определен период от време; и че за конкретна вариация не може да съществува друга вариация за същия тип машина, чийто период от време се припокрива (конфликти) с конкретния период на вариация.
create table machine_type (
id SERIAL PRIMARY KEY,
mtname varchar(50) not null,
mtvar varchar(20) not null,
start_date date not null,
end_date date,
CONSTRAINT machine_type_uk UNIQUE (mtname,mtvar)
);
По-горе казваме на PostgreSQL, че за всяко име на тип машина (mtname) може да има само една вариация (mtvar). Start_date обозначава началната дата на периода, в който е валиден този вариант на типа машина, а end_date обозначава крайната дата на този период. Нулева крайна_дата означава, че вариантът на типа машина в момента е валиден. Сега искаме да изразим изискването за неприпокриване с ограничение. Начинът да направите това е с ограничение за изключване:
testdb=# alter table machine_type ADD CONSTRAINT machine_type_per EXCLUDE USING GIST (mtname WITH =,daterange(start_date,end_date) WITH &&);
Синтаксисът EXCLUDE PostgreSQL ни позволява да посочим много колони от различни типове и с различен оператор за всяка една. &&е припокриващият се оператор за периоди от време, а =е общият оператор за равенство за varchar. Но стига да натиснем enter PostgreSQL се оплаква със съобщение:
ERROR: data type character varying has no default operator class for access method "gist"
HINT: You must specify an operator class for the index or define a default operator class for the data type.
Това, което липсва тук, е поддръжката на GiST opclass за varchar. При условие че сме изградили и инсталирали успешно разширението btree_gist, можем да продължим със създаването на разширението:
testdb=# create extension btree_gist ;
CREATE EXTENSION
И след това повторен опит да създадете ограничението и да го тествате:
testdb=# alter table machine_type ADD CONSTRAINT machine_type_per EXCLUDE USING GIST (mtname WITH =,daterange(start_date,end_date) WITH &&);
ALTER TABLE
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SH','2008-01-01','2013-01-01');
INSERT 0 1
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SG','2002-01-01','2009-01-01');
ERROR: conflicting key value violates exclusion constraint "machine_type_per"
DETAIL: Key (mtname, daterange(start_date, end_date))=(Subaru EJ20, [2002-01-01,2009-01-01)) conflicts with existing key (mtname, daterange(start_date, end_date))=(Subaru EJ20, [2008-01-01,2013-01-01)).
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SG','2002-01-01','2008-01-01');
INSERT 0 1
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SJ','2013-01-01',null);
INSERT 0 1
testdb=# insert into machine_type (mtname,mtvar,start_date,end_date) VALUES('Subaru EJ20','SJ2','2018-01-01',null);
ERROR: conflicting key value violates exclusion constraint "machine_type_per"
DETAIL: Key (mtname, daterange(start_date, end_date))=(Subaru EJ20, [2018-01-01,)) conflicts with existing key (mtname, daterange(start_date, end_date))=(Subaru EJ20, [2013-01-01,)).
SP-GiST индекси
SP-GiST, което означава GiST, разделен на пространство, като GiST, е инфраструктура, която позволява разработването на много различни стратегии в областта на небалансирани дискови базирани структури от данни. Разпределението на PgSQL по подразбиране предлага поддръжка за двуизмерни точки, (всякакъв тип) диапазони, текстови и inet типове. Подобно на GiST, SP-GiST може да се използва при ограничения за изключване, по подобен начин на примера, показан в предишната глава.
GIN индекси
GIN (Generalized Inverted Index) като GiST и SP-GiST може да осигури много стратегии за индексиране. GIN е подходящ, когато искаме да индексираме колони от съставни типове. Разпределението на PostgreSQL по подразбиране осигурява поддръжка за всеки тип масив, jsonb и пълнотекстово търсене (tsvector). В contrib има реализации на много други операторски класове. Jsonb, високо оценена функция на PostgreSQL (и сравнително скорошна (9.4+) разработка) разчита на GIN за поддръжка на индекси. Друга често срещана употреба на GIN е индексирането за пълнотекстово търсене. Пълното текстово търсене в PgSQL заслужава отделна статия, така че тук ще разгледаме само индексиращата част. Първо нека направим малко подготовка за нашата таблица, като дадем стойности, които не са нулеви, на колоната partdescr и актуализираме един ред със значима стойност:
testdb=# update part set partdescr ='';
UPDATE 1000000
Time: 383407,114 ms (06:23,407)
testdb=# update part set partdescr = 'thermostat for the cooling system' where id=500000;
UPDATE 1
Time: 2,405 ms
След това извършваме текстово търсене в ново актуализираната колона:
testdb=# select * from part where partdescr @@ 'thermostat';
id | partno | partname | partdescr | machine_id | parttype
--------+------------+-------------+-----------------------------------+------------+------------
500000 | PNo:500000 | Part 500000 | thermostat for the cooling system | 500000 | Suspension
(1 row)
Time: 2015,690 ms (00:02,016)
Това е доста бавно, почти 2 секунди, за да доведе до нашия резултат. Сега нека се опитаме да създадем GIN индекс на тип tsvector и да повторим заявката, използвайки удобен за индекса синтаксис:
testdb=# CREATE INDEX part_partdescr_idx ON part USING gin(to_tsvector('english',partdescr));
CREATE INDEX
Time: 1431,550 ms (00:01,432)
testdb=# select * from part where to_tsvector('english',partdescr) @@ to_tsquery('thermostat');
id | partno | partname | partdescr | machine_id | parttype
--------+------------+-------------+-----------------------------------+------------+------------
500000 | PNo:500000 | Part 500000 | thermostat for the cooling system | 500000 | Suspension
(1 row)
Time: 0,952 ms
И получаваме 2000-кратно ускоряване. Също така можем да отбележим сравнително краткото време, през което индексът е бил създаден. Можете да експериментирате с използването на GiST вместо GIN в горния пример и да измервате ефективността на четене, запис и създаване на индекс за двата метода на достъп.
BRIN индекси
BRIN (Block Range Index) е най-новото допълнение към набора от типове индекси на PostgreSQL, тъй като е въведен в PostgreSQL 9.5, като има само няколко години като стандартна основна функция. BRIN работи на много големи таблици, като съхранява обобщена информация за набор от страници, наречени „Block Range“. BRIN индексите са със загуба (като GiST) и това изисква както допълнителна логика в изпълнителя на заявки на PostgreSQL, така и необходимостта от допълнителна поддръжка. Нека видим BRIN в действие:
testdb=# select count(*) from part where machine_id BETWEEN 5000 AND 10000;
count
-------
5001
(1 row)
Time: 100,376 ms
testdb=# create index part_machine_id_idx_brin ON part USING BRIN(machine_id);
CREATE INDEX
Time: 569,318 ms
testdb=# select count(*) from part where machine_id BETWEEN 5000 AND 10000;
count
-------
5001
(1 row)
Time: 5,461 ms
Тук виждаме средно около 18-кратно ускорение чрез използването на индекса BRIN. Истинският дом на BRIN обаче е в областта на големите данни, така че се надяваме да тестваме тази сравнително нова технология в реални сценарии в бъдеще.