Databaseindeksering er brugen af specielle datastrukturer, der sigter mod at forbedre ydeevnen ved at opnå direkte adgang til datasider. Et databaseindeks fungerer som indeksafsnittet i en trykt bog:Ved at kigge i indeksafsnittet er det meget hurtigere at identificere den eller de sider, der indeholder det udtryk, vi er interesserede i. Vi kan nemt finde siderne og få direkte adgang til dem . Dette er i stedet for at scanne bogens sider sekventielt, indtil vi finder det udtryk, vi leder efter.
Indeks er et væsentligt værktøj i hænderne på en DBA. Brug af indekser kan give store præstationsgevinster for en række datadomæner. PostgreSQL er kendt for dets store udvidelsesmuligheder og den rige samling af både kerne- og 3. parts tilføjelser, og indeksering er ingen undtagelse fra denne regel. PostgreSQL-indekser dækker et rigt spektrum af tilfælde, fra de enkleste b-tree-indekser på skalartyper til geospatiale GiST-indekser til fts eller json eller array GIN-indekser.
Indekser, så vidunderlige som de ser ud til (og faktisk er!), kommer dog ikke gratis. Der er en vis straf, der følger med at skrive på en indekseret tabel. Så DBA bør, før de undersøger sine muligheder for at oprette et specifikt indeks, først sikre sig, at det nævnte indeks giver mening i første omgang, hvilket betyder, at gevinsterne ved oprettelsen af det vil opveje ydeevnetabet ved skrivninger.
PostgreSQL grundlæggende indeksterminologi
Før vi beskriver typerne af indekser i PostgreSQL og deres brug, lad os tage et kig på noget terminologi, som enhver DBA vil støde på før eller siden, når de læser dokumenterne.
- Indeksadgangsmetode (også kaldet Adgangsmetode ):Indekstypen (B-træ, GiST, GIN osv.)
- Type: datatypen for den indekserede kolonne
- Operatør: en funktion mellem to datatyper
- Operatorfamilie: krydsdatatypeoperator ved at gruppere operatorer af typer med lignende adfærd
- Operatorklasse (også nævnt som indeksstrategi ):definerer de operatorer, der skal bruges af indekset for en kolonne
I PostgreSQLs systemkatalog er adgangsmetoder gemt i pg_am, operatørklasser i pg_opclass, operatørfamilier i pg_opfamily. Afhængighederne af ovenstående er vist i diagrammet nedenfor:
Typer af indekser i PostgreSQL
PostgreSQL giver følgende indekstyper:
- B-træ: standardindekset, der gælder for typer, der kan sorteres
- Hash: håndterer kun ligestilling
- GiST: velegnet til ikke-skalære datatyper (f.eks. geometriske former, fts, arrays)
- SP-GiST: rumopdelt GIST, en udvikling af GiST til håndtering af ikke-balancerede strukturer (quadtrees, k-d trees, radix trees)
- GIN: velegnet til komplekse typer (f.eks. jsonb, fts, arrays )
- BRIN: en relativt ny type indeks, som understøtter data, der kan sorteres ved at gemme min/max værdier i hver blok
Lavt vil vi prøve at få vores hænder til at snavse med nogle eksempler fra den virkelige verden. Alle eksempler er udført med PostgreSQL 10.0 (med både 10 og 9 psql-klienter) på FreeBSD 11.1.
B-træindekser
Lad os antage, at vi har følgende tabel:
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)
Når vi definerer denne ret almindelige tabel, opretter PostgreSQL to unikke B-træ-indekser bag kulisserne:part_pkey og part_partno_key. Så enhver unik begrænsning i PostgreSQL er implementeret med et unikt INDEX. Lad os udfylde vores tabel med en million rækker af data:
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
Lad os nu prøve at lave nogle forespørgsler på vores bord. Først beder vi psql-klienten om at rapportere forespørgselstider ved at skrive \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
Vi observerer, at det kun tager brøkdele af millisekundet at få vores resultater. Vi forventede dette, da vi for begge kolonner brugt i ovenstående forespørgsler allerede har defineret de relevante indekser. Lad os nu prøve at forespørge på kolonnens delnavn, som der ikke findes noget indeks for.
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
Her ser vi tydeligt, at for den ikke-indekserede kolonne falder ydeevnen betydeligt. Lad os nu oprette et indeks på den kolonne og gentage forespørgslen:
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
Vores nye indeks part_partname_idx er også et B-træ indeks (standard). Først bemærker vi, at indeksoprettelsen på tabellen med millioner rækker tog en betydelig mængde tid, omkring 16 sekunder. Derefter observerer vi, at vores forespørgselshastighed blev øget fra 89 ms ned til 0,525 ms. B-træindekser kan udover at kontrollere for lighed også hjælpe med forespørgsler, der involverer andre operatører på ordnede typer, såsom <,<=,>=,>. Lad os prøve med <=og>=
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
Den første forespørgsel er meget hurtigere end den anden, ved at bruge EXPLAIN (eller EXPLAIN ANALYZE) søgeordene kan vi se, om det faktiske indeks er brugt eller ej:
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
I det første tilfælde vælger forespørgselsplanlæggeren at bruge indekset part_partname_idx. Vi observerer også, at dette vil resultere i en kun indeksscanning, hvilket betyder, at der slet ikke er adgang til datatabellerne. I det andet tilfælde bestemmer planlæggeren, at det ikke nytter noget at bruge indekset, da de returnerede resultater er en stor del af tabellen, i hvilket tilfælde en sekventiel scanning menes at være hurtigere.
Hash-indekser
Brug af hash-indekser til og med PgSQL 9.6 blev frarådet på grund af årsager, der havde at gøre med manglende WAL-skrivning. Fra PgSQL 10.0 var disse problemer rettet, men hash-indekser gav stadig lidt mening at bruge. Der er bestræbelser i PgSQL 11 på at gøre hash-indekser til en førsteklasses indeksmetode sammen med sine større brødre (B-tree, GiST, GIN). Så med dette i tankerne, lad os faktisk prøve et hash-indeks i aktion.
Vi vil berige vores deltabel med en ny kolonnedeltype og udfylde den med værdier af lige fordeling og derefter køre en forespørgsel, der tester for deltype lig med ‘Styring’:
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
Nu opretter vi et Hash-indeks for denne nye kolonne, og prøver den forrige forespørgsel igen:
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
Vi bemærker forbedringen efter brug af hash-indekset. Nu vil vi sammenligne ydeevnen af et hash-indeks på heltal med det tilsvarende b-tree-indeks.
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
Som vi ser, med brugen af hash-indekser, er hastigheden af forespørgsler, der kontrollerer for lighed, meget tæt på hastigheden af B-træ-indekser. Hash-indekser siges at være marginalt hurtigere for lighed end B-træer, faktisk var vi nødt til at prøve hver forespørgsel to eller tre gange, indtil hash-indeks gav et bedre resultat end b-tree-ækvivalenten.
Download Whitepaper Today PostgreSQL Management &Automation med ClusterControlFå flere oplysninger om, hvad du skal vide for at implementere, overvåge, administrere og skalere PostgreSQLDownload WhitepaperGiST-indekser
GiST (Generalized Search Tree) er mere end en enkelt slags indeks, men snarere en infrastruktur til at bygge mange indekseringsstrategier. Standard PostgreSQL-distributionen giver understøttelse af geometriske datatyper, tsquery og tsvector. Som bidrag er der implementeringer af mange andre operatørklasser. Ved at læse dokumenterne og bidragskataloget vil læseren observere, at der er et ret stort overlap mellem GiST og GIN use cases:int arrays, fuldtekstsøgning for at nævne de vigtigste tilfælde. I de tilfælde er GIN hurtigere, og den officielle dokumentation siger det eksplicit. GiST giver dog omfattende understøttelse af geometriske datatyper. Ligesom i skrivende stund er GiST (og SP-GiST) også den eneste meningsfulde metode, der kan bruges med udelukkelsesbegrænsninger. Vi vil se et eksempel på dette. Lad os antage (forbliver inden for maskinteknik), at vi har et krav om at definere maskintypevariationer for en bestemt maskintype, som er gyldige i en vis tidsperiode; og at der for en bestemt variation ikke kan eksistere nogen anden variation for den samme maskintype, hvis tidsperiode overlapper (konflikter) med den bestemte variationsperiode.
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)
);
Ovenfor fortæller vi PostgreSQL, at for hvert maskintypenavn (mtname) kan der kun være én variation (mtvar). Startdato angiver startdatoen for den periode, hvor denne maskintypevariation er gyldig, og slutdato angiver slutdatoen for denne periode. Null end_date betyder, at maskintypevariationen i øjeblikket er gyldig. Nu vil vi udtrykke det ikke-overlappende krav med en begrænsning. Måden at gøre dette på er med en udelukkelsesbegrænsning:
testdb=# alter table machine_type ADD CONSTRAINT machine_type_per EXCLUDE USING GIST (mtname WITH =,daterange(start_date,end_date) WITH &&);
EXCLUDE PostgreSQL-syntaksen giver os mulighed for at angive mange kolonner af forskellige typer og med en anden operator for hver enkelt. &&er den overlappende operator for datointervaller, og =er den almindelige lighedsoperator for varchar. Men så længe vi trykker på Enter, klager PostgreSQL med en besked:
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.
Hvad der mangler her, er GiST opclass support til varchar. Forudsat at vi har bygget og installeret btree_gist-udvidelsen, kan vi fortsætte med at oprette udvidelsen:
testdb=# create extension btree_gist ;
CREATE EXTENSION
Og prøv så igen at oprette begrænsningen og teste den:
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-indekser
SP-GiST, der står for space-partitioned GiST, er ligesom GiST en infrastruktur, der muliggør udvikling af mange forskellige strategier inden for domænet af ikke-balancerede diskbaserede datastrukturer. Standard PgSQL-distributionen tilbyder understøttelse af todimensionelle punkter, (enhver type) områder, tekst og inet-typer. Ligesom GiST kan SP-GiST bruges i udelukkelsesbegrænsninger på samme måde som eksemplet vist i forrige kapitel.
GIN-indekser
GIN (Generalized Inverted Index) som GiST og SP-GiST kan give mange indekseringsstrategier. GIN er velegnet, når vi ønsker at indeksere kolonner af sammensatte typer. Standard PostgreSQL-distributionen understøtter enhver array-type, jsonb og fuldtekstsøgning (tsvector). Som bidrag er der implementeringer af mange andre operatørklasser. Jsonb, en meget rost funktion af PostgreSQL (og en relativt nylig (9.4+) udvikling), er afhængig af GIN til indeksstøtte. En anden almindelig brug af GIN er indeksering til fuldtekstsøgning. Fuldtekstsøgning i PgSQL fortjener en artikel i sig selv, så vi dækker her kun indekseringsdelen. Lad os først forberede vores tabel ved ikke at give null-værdier til partdescr-kolonnen og opdatere en enkelt række med en meningsfuld værdi:
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
Derefter udfører vi en tekstsøgning på den nyligt opdaterede kolonne:
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)
Dette er ret langsomt, næsten 2 sekunder for at bringe vores resultat. Lad os nu prøve at oprette et GIN-indeks på typen tsvector og gentage forespørgslen ved at bruge en indeksvenlig syntaks:
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
Og vi får en 2000-dobling af hastigheden. Vi kan også bemærke den relativt korte tid, det tog indekset at blive oprettet. Du kan eksperimentere med at bruge GiST i stedet for GIN i ovenstående eksempel og måle ydeevnen af læsning, skrivning og indeksoprettelse for begge adgangsmetoder.
BRIN-indekser
BRIN (Block Range Index) er den nyeste tilføjelse til PostgreSQL's sæt af indekstyper, siden det blev introduceret i PostgreSQL 9.5, der kun har et par år som en standard kernefunktion. BRIN fungerer på meget store tabeller ved at gemme oversigtsoplysninger for et sæt sider kaldet "Blokområde". BRIN-indekser er tabsgivende (som GiST), og dette kræver både ekstra logik i PostgreSQL's forespørgselseksekutor, og også behovet for ekstra vedligeholdelse. Lad os se BRIN i aktion:
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
Her ser vi i gennemsnit en ~ 18-fold speedup ved brug af BRIN-indekset. BRINs rigtige hjem er dog inden for big data-domænet, så vi håber at teste denne relativt nye teknologi i virkelige scenarier i fremtiden.