sql >> Databasteknik >  >> RDS >> PostgreSQL

Databasindexering i PostgreSQL

Databasindexering är användningen av speciella datastrukturer som syftar till att förbättra prestanda, genom att uppnå direkt åtkomst till datasidor. Ett databasindex fungerar som indexavsnittet i en tryckt bok:genom att titta i indexavsnittet går det mycket snabbare att identifiera sidorna som innehåller termen vi är intresserade av. Vi kan enkelt hitta sidorna och komma åt dem direkt . Detta är istället för att skanna sidorna i boken i tur och ordning, tills vi hittar termen vi letar efter.

Index är ett viktigt verktyg i händerna på en DBA. Att använda index kan ge stora prestandavinster för en mängd olika datadomäner. PostgreSQL är känt för sin stora utökningsbarhet och den rika samlingen av både kärn- och tredjepartstillägg, och indexering är inget undantag från denna regel. PostgreSQL-index täcker ett rikt spektrum av fall, från de enklaste b-tree-indexen på skalära typer till geospatiala GiST-index till fts eller json eller array GIN-index.

Index, men hur underbara de än verkar (och faktiskt är!) kommer inte gratis. Det finns ett visst straff som följer med skrivningar på en indexerad tabell. Så DBA, innan den undersöker sina alternativ för att skapa ett specifikt index, bör först se till att det nämnda indexet är vettigt i första hand, vilket innebär att vinsterna från att det skapas kommer att uppväga prestandaförlusten vid skrivningar.

PostgreSQL Basic Index-terminologi

Innan vi beskriver typerna av index i PostgreSQL och deras användning, låt oss ta en titt på lite terminologi som alla DBA kommer att stöta på förr eller senare när de läser dokumenten.

  • Indexåtkomstmetod (kallas även åtkomstmetod ):Indextypen (B-träd, GiST, GIN, etc)
  • Typ: datatypen för den indexerade kolumnen
  • Operator: en funktion mellan två datatyper
  • Operatorfamilj: korsdatatypoperator, genom att gruppera operatorer av typer med liknande beteende
  • Operatorklass (även nämnt som indexstrategi ):definierar de operatorer som ska användas av indexet för en kolumn

I PostgreSQL:s systemkatalog lagras åtkomstmetoder i pg_am, operatörsklasser i pg_opclass, operatörsfamiljer i pg_opfamily. Beroendena för ovanstående visas i diagrammet nedan:

Typer av index i PostgreSQL

PostgreSQL tillhandahåller följande indextyper:

  • B-träd: standardindexet, tillämpligt för typer som kan sorteras
  • Hash: hanterar enbart jämställdhet
  • GiST: lämplig för icke-skalära datatyper (t.ex. geometriska former, fts, arrayer)
  • SP-GiST: space partitioned GIST, en utveckling av GiST för att hantera obalanserade strukturer (quadtrees, k-d trees, radix trees)
  • GIN: lämplig för komplexa typer (t.ex. jsonb, fts, arrays )
  • BRIN: en relativt ny typ av index som stöder data som kan sorteras genom att lagra min/max-värden i varje block

Lågt ska vi försöka få händerna smutsiga med några verkliga exempel. Alla exempel som ges är gjorda med PostgreSQL 10.0 (med både 10 och 9 psql-klienter) på FreeBSD 11.1.

B-trädindex

Låt oss anta att vi har följande tabell:

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 definierar denna ganska vanliga tabell skapar PostgreSQL två unika B-trädindex bakom kulisserna:part_pkey och part_partno_key. Så varje unik begränsning i PostgreSQL implementeras med ett unikt INDEX. Låt oss fylla vår tabell med en miljon rader med 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

Låt oss nu försöka göra några frågor på vårt bord. Först säger vi till psql-klienten att rapportera frågetider genom att skriva \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 observerar att det bara tar bråkdelar av millisekunden att få våra resultat. Vi förväntade oss detta eftersom vi redan har definierat lämpliga index för båda kolumnerna som används i ovanstående frågor. Låt oss nu försöka fråga efter kolumns delnamn, för vilket det inte finns något 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

Här ser vi tydligt att för den oindexerade kolumnen sjunker prestandan avsevärt. Låt oss nu skapa ett index för den kolumnen och upprepa frågan:

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

Vårt nya index part_partname_idx är också ett B-trädindex (standard). Först noterar vi att indexskapandet i tabellen med miljoner rader tog en betydande tid, cirka 16 sekunder. Sedan observerar vi att vår frågehastighet höjdes från 89 ms ner till 0,525 ms. B-tree-index kan, förutom att kontrollera för likhet, också hjälpa till med frågor som involverar andra operatörer på ordnade typer, såsom <,<=,>=,>. Låt oss försöka med <=och>=

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örsta frågan är mycket snabbare än den andra, genom att använda EXPLAIN (eller EXPLAIN ANALYZE) nyckelord kan vi se om det faktiska indexet används eller inte:

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örsta fallet väljer frågeplaneraren att använda indexet part_partname_idx. Vi observerar också att detta kommer att resultera i en endast index-skanning vilket innebär att ingen tillgång till datatabellerna alls. I det andra fallet fastställer planeraren att det inte är någon mening med att använda indexet eftersom de returnerade resultaten är en stor del av tabellen, i vilket fall en sekventiell genomsökning anses vara snabbare.

Hashindex

Användning av hashindex upp till och inklusive PgSQL 9.6 avråddes på grund av skäl som hade att göra med bristande WAL-skrivning. Från och med PgSQL 10.0 var dessa problem fixade, men hashindexen var fortfarande lite meningsfulla att använda. Det finns ansträngningar i PgSQL 11 för att göra hashindex till en förstklassig indexmetod tillsammans med sina större bröder (B-tree, GiST, GIN). Så, med detta i åtanke, låt oss faktiskt prova ett hashindex i aktion.

Vi kommer att berika vår deltabell med en ny kolumn deltyp och fylla i den med värden med lika fördelning och sedan köra en fråga som testar för deltyp lika med 'Styrning':

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 skapar vi ett Hash-index för denna nya kolumn och försöker igen med föregående fråga:

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 noterar förbättringen efter att ha använt hashindex. Nu ska vi jämföra prestandan för ett hashindex på heltal mot motsvarande 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

Som vi ser, med användning av hashindex, är hastigheten för frågor som kontrollerar jämlikhet mycket nära hastigheten för B-trädindex. Hashindex sägs vara marginellt snabbare för jämlikhet än B-träd, i själva verket var vi tvungna att prova varje fråga två eller tre gånger tills hashindex gav ett bättre resultat än motsvarigheten till b-tree.

Ladda ner Whitepaper Today PostgreSQL Management &Automation med ClusterControlLäs om vad du behöver veta för att distribuera, övervaka, hantera och skala PostgreSQLDladda Whitepaper

GiST-index

GiST (Generalized Search Tree) är mer än ett enda slags index, utan snarare en infrastruktur för att bygga många indexeringsstrategier. StandardpostgreSQL-distributionen ger stöd för geometriska datatyper, tsquery och tsvector. Som bidrag finns implementeringar av många andra operatörsklasser. Genom att läsa dokumenten och bidragsfilen kommer läsaren att observera att det finns en ganska stor överlappning mellan användningsfallen för GiST och GIN:int-matriser, fulltextsökning för att nämna huvudfallen. I dessa fall är GIN snabbare, och det står det uttryckligen i den officiella dokumentationen. GiST tillhandahåller dock omfattande stöd för geometriska datatyper. Som när detta skrivs är GiST (och SP-GiST) den enda meningsfulla metoden som kan användas med uteslutningsbegränsningar. Vi kommer att se ett exempel på detta. Låt oss anta (om vi stannar inom området maskinteknik) att vi har ett krav på att definiera maskintypsvariationer för en viss maskintyp, som är giltiga under en viss tidsperiod; och att för en viss variant kan ingen annan variation existera för samma maskintyp vars tidsperiod överlappar (konflikterar) med den specifika variationsperioden.

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

Ovan säger vi till PostgreSQL att för varje maskintypsnamn (mtname) kan det bara finnas en variant (mtvar). Start_date anger startdatumet för perioden under vilken denna maskintypsvariation är giltig, och slutdatum anger slutdatumet för denna period. Null end_date betyder att maskintypsvarianten för närvarande är giltig. Nu vill vi uttrycka det icke-överlappande kravet med en begränsning. Sättet att göra detta är med en undantagsbegränsning:

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

Syntaxen EXCLUDE PostgreSQL tillåter oss att specificera många kolumner av olika typer och med olika operatorer för var och en. &&är den överlappande operatorn för datumintervall, och =är den vanliga likhetsoperatorn för varchar. Men så länge vi trycker på enter klagar PostgreSQL med ett meddelande:

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.

Det som saknas här är GiST opclass-stöd för varchar. Förutsatt att vi framgångsrikt har byggt och installerat tillägget btree_gist, kan vi fortsätta med att skapa tillägget:

testdb=# create extension btree_gist ;
CREATE EXTENSION

Och försöker sedan skapa begränsningen igen och testa 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-index

SP-GiST som står för space-partitioned GiST, liksom GiST, är en infrastruktur som möjliggör utveckling av många olika strategier inom domänen av obalanserade diskbaserade datastrukturer. Standard PgSQL-distribution erbjuder stöd för tvådimensionella punkter, (valfri typ) intervall, text- och inettyper. Precis som GiST kan SP-GiST användas i undantagsbegränsningar, på liknande sätt som exemplet i föregående kapitel.

GIN-index

GIN (Generalized Inverted Index) som GiST och SP-GiST kan tillhandahålla många indexeringsstrategier. GIN passar när vi vill indexera kolumner av sammansatta typer. StandardpostgreSQL-distributionen ger stöd för alla arraytyper, jsonb och fulltextsökning (tsvector). Som bidrag finns implementeringar av många andra operatörsklasser. Jsonb, en mycket berömd funktion i PostgreSQL (och en relativt ny (9.4+) utveckling) förlitar sig på GIN för indexstöd. En annan vanlig användning av GIN är indexering för fulltextsökning. Fulltextsökning i PgSQL förtjänar en artikel i sig, så vi täcker här endast indexeringsdelen. Låt oss först förbereda vår tabell genom att inte ge nollvärden till partdescr-kolumnen och uppdatera en enda rad med ett meningsfullt värde:

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

Sedan gör vi en textsökning på den nyligen uppdaterade kolumnen:

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)

Detta är ganska långsamt, nästan 2 sekunder för att ge vårt resultat. Låt oss nu försöka skapa ett GIN-index på typ tsvector och upprepa frågan med en indexvänlig syntax:

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

Och vi får en 2000-faldig hastighet. Vi kan också notera den relativt korta tid som det tog att skapa indexet. Du kan experimentera med att använda GiST istället för GIN i exemplet ovan och mäta prestanda för läsning, skrivning och indexskapande för båda åtkomstmetoderna.

BRIN-index

BRIN (Block Range Index) är det senaste tillskottet till PostgreSQL:s uppsättning indextyper, sedan det introducerades i PostgreSQL 9.5, med bara några år som en standard kärnfunktion. BRIN fungerar på mycket stora tabeller genom att lagra sammanfattningsinformation för en uppsättning sidor som kallas "Blockintervall". BRIN-index är förlustbringande (som GiST) och detta kräver både extra logik i PostgreSQL:s frågeexekutor, och även behovet av extra underhåll. Låt oss 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

Här ser vi i genomsnitt en ~ 18-faldig hastighetsökning genom användning av BRIN-index. BRIN:s verkliga hem är dock i domänen för big data, så vi hoppas kunna testa denna relativt nya teknik i verkliga scenarier i framtiden.


  1. Introduktion till datakopplingar och relationer

  2. oracle raderingsfråga tar för lång tid

  3. Bestäm vilken MySQL-konfigurationsfil som används

  4. MySQL – Fix Error – WordPress Database Error Duplicate Entry for key PRIMARY for Query INSERT INTO wp_options