sql >> Databáze >  >> RDS >> PostgreSQL

Indexování databáze v PostgreSQL

Indexování databáze je použití speciálních datových struktur, jejichž cílem je zlepšit výkon tím, že se dosáhne přímého přístupu k datovým stránkám. Databázový rejstřík funguje jako rejstříková část tištěné knihy:pohledem do rejstříkové části je mnohem rychlejší identifikovat stránky, které obsahují výraz, který nás zajímá. Stránky můžeme snadno najít a přímo k nim přistupovat. . Toto je místo toho, abychom postupně skenovali stránky knihy, dokud nenajdeme hledaný výraz.

Indexy jsou základním nástrojem v rukou DBA. Použití indexů může poskytnout velké zvýšení výkonu pro různé datové domény. PostgreSQL je známý svou velkou rozšiřitelností a bohatou sbírkou základních doplňků i doplňků třetích stran a indexování není výjimkou z tohoto pravidla. Indexy PostgreSQL pokrývají bohaté spektrum případů, od nejjednodušších indexů b-stromu na skalárních typech přes geoprostorové indexy GiST až po indexy fts nebo json nebo pole GIN.

Indexy, jakkoli se zdají úžasné (a ve skutečnosti jsou!), však nejsou zadarmo. Existuje určitá penalizace, která souvisí se zápisy do indexované tabulky. Takže DBA, než prozkoumá své možnosti vytvoření konkrétního indexu, by se měla nejprve ujistit, že zmíněný index dává smysl především, což znamená, že zisky z jeho vytvoření převáží ztrátu výkonu při zápisech.

Základní terminologie indexů PostgreSQL

Než popíšeme typy indexů v PostgreSQL a jejich použití, podívejme se na nějakou terminologii, se kterou se dříve nebo později při čtení dokumentů setká každý DBA.

  • Metoda přístupu k indexu (nazývané také jako Přístupová metoda ):Typ indexu (B-strom, GiST, GIN atd.)
  • Typ: datový typ indexovaného sloupce
  • Operátor: funkce mezi dvěma datovými typy
  • Rodina operátorů: křížový datový typ operátor seskupením operátorů typů s podobným chováním
  • Třída operátora (také uváděno jako strategie indexu ):definuje operátory, které má index použít pro sloupec

V systémovém katalogu PostgreSQL jsou přístupové metody uloženy v pg_am, třídy operátorů v pg_opclass, rodiny operátorů v pg_opfamily. Závislosti výše uvedených jsou zobrazeny v níže uvedeném diagramu:

Typy indexů v PostgreSQL

PostgreSQL poskytuje následující typy indexů:

  • B-strom: výchozí index, použitelný pro typy, které lze třídit
  • Hash: řeší pouze rovnost
  • GiST: vhodné pro neskalární datové typy (např. geometrické tvary, stopy, pole)
  • SP-GiST: space partitioned GIST, evoluce GiST pro práci s nevyváženými strukturami (quadtrees, k-d stromy, radix trees)
  • GIN: vhodné pro komplexní typy (např. jsonb, fts, pole)
  • BRIN: relativně nový typ indexu, který podporuje data, která lze třídit uložením min/max hodnot v každém bloku

Zkusíme si ušpinit ruce některými příklady ze skutečného světa. Všechny uvedené příklady jsou provedeny s PostgreSQL 10.0 (s klienty 10 i 9 psql) na FreeBSD 11.1.

Indexy B-stromu

Předpokládejme, že máme následující tabulku:

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)

Když definujeme tuto poměrně běžnou tabulku, PostgreSQL vytvoří v zákulisí dva jedinečné indexy B-stromu:part_pkey a part_partno_key. Takže každé jedinečné omezení v PostgreSQL je implementováno s jedinečným INDEXem. Pojďme naplnit naši tabulku milionem řádků dat:

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

Nyní se pokusíme položit nějaké dotazy na našem stole. Nejprve řekneme klientovi psql, aby hlásil časy dotazů zadáním \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

Všimli jsme si, že k získání našich výsledků trvá pouze zlomky milisekundy. Očekávali jsme to, protože pro oba sloupce použité ve výše uvedených dotazech jsme již definovali příslušné indexy. Nyní se zkusme zeptat na název části sloupce, pro který neexistuje žádný index.

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

Zde jasně vidíme, že u neindexovaného sloupce výkon výrazně klesá. Nyní vytvořte index pro tento sloupec a zopakujte dotaz:

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

Náš nový index part_partname_idx je také index B-stromu (výchozí). Nejprve si všimneme, že vytvoření indexu v tabulce milionů řádků trvalo značné množství času, asi 16 sekund. Poté pozorujeme, že rychlost našeho dotazu byla zvýšena z 89 ms na 0,525 ms. Indexy B-stromu mohou kromě kontroly rovnosti pomoci také s dotazy zahrnujícími jiné operátory na uspořádané typy, jako jsou <,<=,>=,>. Zkusme to s <=a>=

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

První dotaz je mnohem rychlejší než druhý, pomocí klíčových slov EXPLAIN (nebo EXPLAIN ANALYZE) můžeme zjistit, zda je skutečný index použit nebo ne:

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

V prvním případě se plánovač dotazů rozhodne použít index part_partname_idx. Pozorujeme také, že to bude mít za následek skenování pouze na základě indexu, což znamená, že k datovým tabulkám nebude mít vůbec žádný přístup. Ve druhém případě plánovač zjistí, že nemá smysl index používat, protože vrácené výsledky tvoří velkou část tabulky, v takovém případě se sekvenční skenování považuje za rychlejší.

Hash indexy

Použití hash indexů až do PgSQL 9.6 včetně bylo odrazeno z důvodů souvisejících s nedostatkem zápisu WAL. Od PgSQL 10.0 byly tyto problémy opraveny, ale stále nemělo smysl používat hash indexy. V PgSQL 11 jsou snahy udělat z hash indexů prvotřídní indexovou metodu spolu s jejími většími bratry (B-strom, GiST, GIN). S ohledem na to tedy zkusme hash index v akci.

Naši tabulku součástí obohatíme o nový sloupec parttype a naplníme ji hodnotami se stejnou distribucí a poté spustíme dotaz, který otestuje parttype rovný ‚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

Nyní vytvoříme hash index pro tento nový sloupec a zopakujeme předchozí dotaz:

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

Zaznamenáváme zlepšení po použití hash indexu. Nyní porovnáme výkon hash indexu na celých číslech s ekvivalentním indexem b-stromu.

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

Jak vidíme, s použitím hash indexů je rychlost dotazů, které kontrolují rovnost, velmi blízká rychlosti indexů B-stromu. Říká se, že hash indexy jsou z hlediska rovnosti o něco rychlejší než B-stromy, ve skutečnosti jsme museli každý dotaz vyzkoušet dvakrát nebo třikrát, dokud hash index neposkytl lepší výsledek než ekvivalent b-stromu.

Stáhněte si Whitepaper Today Správa a automatizace PostgreSQL s ClusterControlZjistěte, co potřebujete vědět k nasazení, monitorování, správě a škálování PostgreSQLStáhněte si Whitepaper

Indexy GiST

GiST (Generalized Search Tree) je více než jeden druh indexu, ale spíše infrastruktura pro vytváření mnoha strategií indexování. Výchozí distribuce PostgreSQL poskytuje podporu pro geometrické datové typy, tsquery a tsvector. V contrib jsou implementace mnoha dalších tříd operátorů. Při čtení dokumentů a adresáře contrib si čtenář všimne, že mezi případy použití GiST a GIN existuje poměrně velké překrývání:pole int, fulltextové vyhledávání pro pojmenování hlavních případů. V těchto případech je GIN rychlejší a oficiální dokumentace to výslovně uvádí. GiST však poskytuje rozsáhlou podporu geometrických datových typů. Stejně jako v době psaní tohoto článku je GiST (a SP-GiST) jedinou smysluplnou metodou, kterou lze použít s omezeními vyloučení. Na tomto příkladu uvidíme. Předpokládejme (zůstaneme-li v oblasti strojírenství), že máme požadavek na definování typových variant strojů pro konkrétní typ stroje, které jsou platné pro určité časové období; a že pro konkrétní variaci nemůže existovat žádná jiná variace pro stejný typ stroje, jehož časové období se překrývá (konfliktuje) s konkrétním variačním obdobím.

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)
);

Výše jsme PostgreSQL řekli, že pro každý název typu stroje (mtname) může existovat pouze jedna varianta (mtvar). Start_date označuje počáteční datum období, ve kterém je tato varianta typu stroje platná, a end_date označuje datum ukončení tohoto období. Null end_date znamená, že varianta typu počítače je aktuálně platná. Nyní chceme vyjádřit nepřekrývající se požadavek s omezením. Způsob, jak toho dosáhnout, je s omezením vyloučení:

testdb=# alter table machine_type ADD CONSTRAINT machine_type_per EXCLUDE USING GIST (mtname WITH =,daterange(start_date,end_date) WITH &&);

Syntaxe EXCLUDE PostgreSQL nám umožňuje specifikovat mnoho sloupců různých typů a s jiným operátorem pro každý z nich. &&je překrývající se operátor pro rozsahy dat a =je běžný operátor rovnosti pro varchar. Ale dokud stiskneme enter, PostgreSQL si stěžuje se zprávou:

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.

Co zde chybí, je podpora opclass GiST pro varchar. Pokud jsme úspěšně sestavili a nainstalovali rozšíření btree_gist, můžeme pokračovat ve vytváření rozšíření:

testdb=# create extension btree_gist ;
CREATE EXTENSION

A pak se znovu pokusit vytvořit omezení a otestovat ho:

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,)).

Indexy SP-GiST

SP-GiST, což je zkratka pro prostorově rozdělený GiST, stejně jako GiST, je infrastruktura, která umožňuje vývoj mnoha různých strategií v oblasti nevyvážených datových struktur na disku. Výchozí distribuce PgSQL nabízí podporu pro dvourozměrné body, (jakéhokoli typu) rozsahy, text a typy inet. Podobně jako GiST lze i SP-GiST použít v omezeních vyloučení, podobným způsobem jako v příkladu uvedeném v předchozí kapitole.

Indexy GIN

GIN (Generalized Inverted Index) jako GiST a SP-GiST může poskytnout mnoho strategií indexování. GIN se hodí, když chceme indexovat sloupce složených typů. Výchozí distribuce PostgreSQL poskytuje podporu pro jakýkoli typ pole, jsonb a fulltextové vyhledávání (tsvector). V contrib jsou implementace mnoha dalších tříd operátorů. Jsonb, vysoce chválená funkce PostgreSQL (a relativně nedávný (9.4+) vývoj), se při podpoře indexů spoléhá na GIN. Dalším běžným využitím GIN je indexování pro fulltextové vyhledávání. Fulltextové vyhledávání v PgSQL si zaslouží samostatný článek, takže zde pokryjeme pouze část indexování. Nejprve udělejme určitou přípravu naší tabulky tím, že sloupci partdescr nedáváme hodnoty null a aktualizujeme jeden řádek smysluplnou hodnotou:

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

Poté provedeme textové vyhledávání v nově aktualizovaném sloupci:

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)

To je docela pomalé, téměř 2 sekundy k dosažení našeho výsledku. Nyní se pokusíme vytvořit index GIN na typu tsvector a zopakovat dotaz pomocí syntaxe vhodné pro index:

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

A dosáhneme 2000násobného zrychlení. Můžeme si také všimnout relativně krátké doby, po kterou byl index vytvořen. Ve výše uvedeném příkladu můžete experimentovat s použitím GiST místo GIN a měřit výkon čtení, zápisu a vytváření indexu pro obě přístupové metody.

Indexy BRIN

BRIN (Block Range Index) je nejnovějším přírůstkem do sady typů indexů PostgreSQL, protože byl představen v PostgreSQL 9.5 a má jen několik let jako standardní základní funkci. BRIN pracuje na velmi velkých tabulkách tak, že ukládá souhrnné informace pro sadu stránek nazvanou „Rozsah bloků“. Indexy BRIN jsou ztrátové (jako GiST) a to vyžaduje jak extra logiku ve vykonavateli dotazů PostgreSQL, tak i potřebu další údržby. Pojďme se podívat na BRIN v akci:

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

Zde vidíme v průměru ~ 18násobné zrychlení díky použití indexu BRIN. Skutečný domov BRIN je však v oblasti velkých dat, takže doufáme, že tuto relativně novou technologii v budoucnu otestujeme ve scénářích reálného světa.


  1. Existuje alternativa k TOP v MySQL?

  2. Proč se řádky vrácené vysvětlením nerovnají count()?

  3. Jaké je nejlepší řešení pro sdružování databázových připojení v pythonu?

  4. Jak funguje funkce OCTET_LENGTH() v MySQL