Database Indexing is het gebruik van speciale datastructuren die gericht zijn op het verbeteren van de prestaties door directe toegang tot datapagina's. Een database-index werkt als de indexsectie van een gedrukt boek:door in de indexsectie te kijken, is het veel sneller om de pagina('s) te identificeren die de term bevatten waarin we geïnteresseerd zijn. We kunnen de pagina's gemakkelijk lokaliseren en direct openen . Dit is in plaats van de pagina's van het boek opeenvolgend te scannen, totdat we de term vinden die we zoeken.
Indexen zijn een essentieel hulpmiddel in de handen van een DBA. Het gebruik van indexen kan grote prestatieverbeteringen opleveren voor verschillende gegevensdomeinen. PostgreSQL staat bekend om zijn grote uitbreidbaarheid en de rijke verzameling van zowel core-add-ons als add-ons van derden, en indexering is geen uitzondering op deze regel. PostgreSQL-indexen bestrijken een rijk spectrum aan gevallen, van de eenvoudigste b-tree-indexen op scalaire typen tot geospatiale GiST-indexen tot fts- of json- of array-GIN-indexen.
Indexen zijn echter, hoe geweldig ze ook lijken (en eigenlijk zijn!) niet gratis. Er is een bepaalde straf die hoort bij schrijven op een geïndexeerde tabel. Dus de DBA moet, voordat ze haar opties voor het maken van een specifieke index onderzoekt, zich er eerst van vergewissen dat de genoemde index in de eerste plaats zinvol is, wat betekent dat de voordelen van het maken ervan opwegen tegen het prestatieverlies bij schrijven.
PostgreSQL Basic Index-terminologie
Voordat we de typen indexen in PostgreSQL en hun gebruik beschrijven, laten we eens kijken naar de terminologie die een DBA vroeg of laat tegenkomt bij het lezen van de documenten.
- Indextoegangsmethode (ook wel Toegangsmethode genoemd) ):Het indextype (B-tree, GiST, GIN, etc)
- Type: het gegevenstype van de geïndexeerde kolom
- Operator: een functie tussen twee datatypes
- Operatorfamilie: operator voor kruisgegevenstypes, door operatoren van typen met soortgelijk gedrag te groeperen
- Operatorklasse (ook genoemd als indexstrategie ):definieert de operators die door de index voor een kolom moeten worden gebruikt
In de systeemcatalogus van PostgreSQL worden toegangsmethoden opgeslagen in pg_am, operatorklassen in pg_opclass, operatorfamilies in pg_opfamily. De afhankelijkheden van het bovenstaande worden weergegeven in het onderstaande diagram:
Soorten indexen in PostgreSQL
PostgreSQL biedt de volgende indextypen:
- B-boom: de standaardindex, van toepassing op typen die kunnen worden gesorteerd
- Hash: behandelt alleen gelijkheid
- GiST: geschikt voor niet-scalaire gegevenstypen (bijv. geometrische vormen, fts, arrays)
- SP-GiST: space partitioned GIST, een evolutie van GiST voor het hanteren van niet-gebalanceerde structuren (quadtrees, k-d trees, radix trees)
- GIN: geschikt voor complexe typen (bijv. jsonb, fts, arrays )
- BRIN: een relatief nieuw type index dat gegevens ondersteunt die kunnen worden gesorteerd door min/max-waarden in elk blok op te slaan
Laag zullen we proberen onze handen vuil te maken met enkele voorbeelden uit de echte wereld. Alle gegeven voorbeelden zijn gedaan met PostgreSQL 10.0 (met zowel 10 als 9 psql-clients) op FreeBSD 11.1.
B-tree-indexen
Laten we aannemen dat we de volgende tabel hebben:
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)
Wanneer we deze vrij algemene tabel definiëren, creëert PostgreSQL twee unieke B-tree-indexen achter de schermen:part_pkey en part_partno_key. Dus elke unieke beperking in PostgreSQL wordt geïmplementeerd met een unieke INDEX. Laten we onze tabel vullen met een miljoen rijen gegevens:
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
Laten we nu proberen wat vragen op onze tafel te doen. Eerst vertellen we de psql-client om de querytijden te melden door \timing:
. te typentestdb=# 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
We merken dat het slechts fracties van milliseconden kost om onze resultaten te krijgen. We hadden dit verwacht omdat we voor beide kolommen die in de bovenstaande zoekopdrachten worden gebruikt, al de juiste indexen hebben gedefinieerd. Laten we nu proberen een query uit te voeren op de kolomonderdeelnaam, waarvoor geen index bestaat.
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
Hier zien we duidelijk dat voor de niet-geïndexeerde kolom de prestatie aanzienlijk daalt. Laten we nu een index voor die kolom maken en de vraag herhalen:
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
Onze nieuwe index part_partname_idx is ook een B-tree index (de standaard). Ten eerste merken we op dat het maken van de index op de tabel met miljoen rijen een aanzienlijke hoeveelheid tijd kostte, ongeveer 16 seconden. Vervolgens zien we dat onze querysnelheid werd opgevoerd van 89 ms naar 0,525 ms. B-tree-indexen kunnen, naast het controleren op gelijkheid, ook helpen bij query's waarbij andere operators zijn betrokken op geordende typen, zoals <,<=,>=,>. Laten we proberen met <=en>=
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
De eerste zoekopdracht is veel sneller dan de tweede, door de EXPLAIN (of EXPLAIN ANALYZE) trefwoorden te gebruiken, kunnen we zien of de daadwerkelijke index wordt gebruikt of niet:
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
In het eerste geval kiest de queryplanner ervoor om de index part_partname_idx te gebruiken. We zien ook dat dit resulteert in een alleen-index scan, wat betekent dat er helemaal geen toegang tot de datatabellen is. In het tweede geval bepaalt de planner dat het geen zin heeft om de index te gebruiken, aangezien de geretourneerde resultaten een groot deel van de tabel uitmaken, in welk geval een sequentiële scan sneller wordt geacht.
Hash-indexen
Het gebruik van hash-indexen tot en met PgSQL 9.6 werd afgeraden vanwege een gebrek aan WAL-schrijven. Vanaf PgSQL 10.0 waren die problemen opgelost, maar hash-indexen hadden nog steeds weinig zin om te gebruiken. Er zijn pogingen in PgSQL 11 om van hash-indexen een eersteklas indexmethode te maken, samen met zijn grotere broers (B-tree, GiST, GIN). Laten we daarom, met dit in gedachten, eens een hash-index in actie proberen.
We zullen onze onderdeeltabel verrijken met een nieuwe kolom onderdeeltype en deze vullen met waarden van gelijke verdeling, en dan een query uitvoeren die test op onderdeeltype gelijk aan '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
Nu maken we een hash-index voor deze nieuwe kolom en proberen we de vorige zoekopdracht opnieuw:
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
We merken de verbetering op na gebruik van de hash-index. Nu zullen we de prestaties van een hash-index op gehele getallen vergelijken met de equivalente b-tree-index.
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
Zoals we zien, ligt met het gebruik van hash-indexen de snelheid van zoekopdrachten die controleren op gelijkheid heel dicht bij de snelheid van B-tree-indexen. Van hash-indexen wordt gezegd dat ze iets sneller zijn voor gelijkheid dan B-trees, in feite moesten we elke zoekopdracht twee of drie keer proberen totdat de hash-index een beter resultaat gaf dan het equivalent van de b-tree.
Download de whitepaper vandaag PostgreSQL-beheer en -automatisering met ClusterControlLees wat u moet weten om PostgreSQL te implementeren, bewaken, beheren en schalenDownload de whitepaperGiST-indexen
GiST (Generalized Search Tree) is meer dan een enkel soort index, maar eerder een infrastructuur om veel indexeringsstrategieën op te bouwen. De standaard PostgreSQL-distributie biedt ondersteuning voor geometrische gegevenstypen, tsquery en tsvector. Daarnaast zijn er implementaties van vele andere operatorklassen. Door de documenten en de contrib-dir te lezen, zal de lezer opmerken dat er een vrij grote overlap is tussen GiST- en GIN-gebruiksgevallen:int-arrays, zoeken in volledige tekst om de belangrijkste gevallen te noemen. In die gevallen is GIN sneller, en de officiële documentatie vermeldt dat expliciet. GiST biedt echter uitgebreide ondersteuning voor geometrische gegevenstypes. Op het moment van schrijven is GiST (en SP-GiST) ook de enige zinvolle methode die kan worden gebruikt met uitsluitingsbeperkingen. We zullen hiervan een voorbeeld zien. Laten we veronderstellen (blijvend op het gebied van werktuigbouwkunde) dat we een vereiste hebben om machinetypevariaties te definiëren voor een bepaald machinetype, die geldig zijn voor een bepaalde tijdsperiode; en dat voor een bepaalde variatie geen andere variatie kan bestaan voor hetzelfde machinetype waarvan de periode overlapt (conflicteert) met de specifieke variatieperiode.
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)
);
Hierboven vertellen we PostgreSQL dat er voor elke machinetypenaam (mtname) slechts één variatie (mtvar) kan zijn. Begindatum geeft de begindatum aan van de periode waarin deze variatie van het machinetype geldig is, en einddatum geeft de einddatum van deze periode aan. Null end_date betekent dat de variatie van het machinetype momenteel geldig is. Nu willen we de niet-overlappende eis met een beperking uitdrukken. De manier om dit te doen is met een uitsluitingsbeperking:
testdb=# alter table machine_type ADD CONSTRAINT machine_type_per EXCLUDE USING GIST (mtname WITH =,daterange(start_date,end_date) WITH &&);
De EXCLUDE PostgreSQL-syntaxis stelt ons in staat om veel kolommen van verschillende typen te specificeren en met voor elke kolom een andere operator. &&is de overlappende operator voor datumbereiken en =is de algemene gelijkheidsoperator voor varchar. Maar zolang we op enter drukken klaagt PostgreSQL met een bericht:
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.
Wat hier ontbreekt, is GiST opclass-ondersteuning voor varchar. Op voorwaarde dat we de btree_gist-extensie met succes hebben gebouwd en geïnstalleerd, kunnen we doorgaan met het maken van de extensie:
testdb=# create extension btree_gist ;
CREATE EXTENSION
En dan opnieuw proberen om de beperking te creëren en te testen:
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-indexen
SP-GiST, wat staat voor in de ruimte gepartitioneerde GiST, zoals GiST, is een infrastructuur die de ontwikkeling mogelijk maakt voor veel verschillende strategieën op het gebied van niet-gebalanceerde schijfgebaseerde datastructuren. De standaard PgSQL-distributie biedt ondersteuning voor tweedimensionale punten, (elk type) bereiken, tekst en inet-typen. Net als GiST kan SP-GiST worden gebruikt bij uitsluitingsbeperkingen, op een vergelijkbare manier als het voorbeeld in het vorige hoofdstuk.
GIN-indexen
GIN (Generalized Inverted Index) zoals GiST en SP-GiST kunnen veel indexeringsstrategieën bieden. GIN is geschikt wanneer we kolommen van samengestelde typen willen indexeren. De standaard PostgreSQL-distributie biedt ondersteuning voor elk type array, jsonb en zoeken in volledige tekst (tsvector). Daarnaast zijn er implementaties van vele andere operatorklassen. Jsonb, een veelgeprezen functie van PostgreSQL (en een relatief recente (9,4+) ontwikkeling) vertrouwt op GIN voor indexondersteuning. Een ander veelgebruikt gebruik van GIN is indexering voor zoeken in volledige tekst. Zoeken in volledige tekst in PgSQL verdient een apart artikel, dus we behandelen hier alleen het indexeringsgedeelte. Laten we eerst wat voorbereidingen treffen voor onze tabel, door geen null-waarden te geven aan de kolom partdescr en een enkele rij bij te werken met een zinvolle waarde:
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
Vervolgens voeren we een tekstzoekopdracht uit in de nieuw bijgewerkte kolom:
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)
Dit is vrij traag, bijna 2 seconden om ons resultaat te brengen. Laten we nu proberen een GIN-index van het type tsvector te maken en de query herhalen met een indexvriendelijke syntaxis:
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
En we krijgen een 2000-voudige versnelling. We merken ook de relatief korte tijd op die nodig was om de index te maken. U kunt experimenteren met het gebruik van GiST in plaats van GIN in het bovenstaande voorbeeld en de prestaties meten van lezen, schrijven en indexcreatie voor beide toegangsmethoden.
BRIN-indexen
BRIN (Block Range Index) is de nieuwste toevoeging aan de reeks indextypen van PostgreSQL, sinds het werd geïntroduceerd in PostgreSQL 9.5, met slechts een paar jaar als standaard kernfunctie. BRIN werkt aan zeer grote tabellen door samenvattende informatie op te slaan voor een reeks pagina's genaamd "Block Range". BRIN-indexen zijn lossy (zoals GiST) en dit vereist zowel extra logica in de query-executor van PostgreSQL, als ook de noodzaak van extra onderhoud. Laten we BRIN eens in actie zien:
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
Hier zien we gemiddeld een ~ 18-voudige versnelling door het gebruik van de BRIN-index. De echte thuisbasis van BRIN ligt echter in het domein van big data, dus we hopen deze relatief nieuwe technologie in de toekomst in real-world scenario's te testen.