Die Datenbankindizierung ist die Verwendung spezieller Datenstrukturen, die darauf abzielen, die Leistung zu verbessern, indem ein direkter Zugriff auf Datenseiten erreicht wird. Ein Datenbankindex funktioniert wie der Indexteil eines gedruckten Buches:Durch einen Blick in den Indexteil ist es viel schneller, die Seite(n) zu identifizieren, die den für uns interessanten Begriff enthalten. Wir können die Seiten leicht finden und direkt darauf zugreifen . Anstatt die Seiten des Buches nacheinander zu durchsuchen, bis wir den Begriff finden, nach dem wir suchen.
Indizes sind ein wesentliches Werkzeug in den Händen eines DBA. Die Verwendung von Indizes kann große Leistungssteigerungen für eine Vielzahl von Datendomänen bieten. PostgreSQL ist bekannt für seine große Erweiterbarkeit und die reichhaltige Sammlung von Kern- und Drittanbieter-Add-Ons, und die Indizierung ist keine Ausnahme von dieser Regel. PostgreSQL-Indizes decken ein breites Spektrum an Fällen ab, von den einfachsten B-Tree-Indizes für skalare Typen über Geodaten-GiST-Indizes bis hin zu fts- oder json- oder Array-GIN-Indizes.
Indizes, so wunderbar sie scheinen (und tatsächlich sind!), sind jedoch nicht kostenlos. Es gibt eine gewisse Strafe, die mit Schreibvorgängen in eine indizierte Tabelle einhergeht. Daher sollte der DBA, bevor er seine Optionen zum Erstellen eines bestimmten Index prüft, zunächst sicherstellen, dass der besagte Index überhaupt sinnvoll ist, was bedeutet, dass die Gewinne aus seiner Erstellung den Leistungsverlust bei Schreibvorgängen überwiegen.
Grundlegende PostgreSQL-Indexterminologie
Bevor wir die Indextypen in PostgreSQL und ihre Verwendung beschreiben, werfen wir einen Blick auf einige Begriffe, auf die jeder DBA beim Lesen der Dokumentation früher oder später stoßen wird.
- Indexzugriffsmethode (auch als Zugriffsmethode bezeichnet ):Der Indextyp (B-Tree, GiST, GIN usw.)
- Typ: der Datentyp der indizierten Spalte
- Betreiber: eine Funktion zwischen zwei Datentypen
- Operator-Familie: datentypübergreifender Operator, indem Operatoren von Typen mit ähnlichem Verhalten gruppiert werden
- Operator-Klasse (auch als Indexstrategie bezeichnet ):definiert die Operatoren, die vom Index für eine Spalte verwendet werden sollen
Im Systemkatalog von PostgreSQL werden Zugriffsmethoden in pg_am gespeichert, Operatorklassen in pg_opclass, Operatorfamilien in pg_opfamily. Die Abhängigkeiten des Obigen sind im folgenden Diagramm dargestellt:
Indextypen in PostgreSQL
PostgreSQL bietet die folgenden Indextypen:
- B-Baum: der Standardindex, der für sortierbare Typen anwendbar ist
- Hash: behandelt nur Gleichheit
- GiST: geeignet für nicht skalare Datentypen (z. B. geometrische Formen, fts, Arrays)
- SP-GiST: raumpartitioniertes GIST, eine Weiterentwicklung von GiST zum Umgang mit nicht balancierten Strukturen (Quadtrees, k-d-Bäume, Radix-Bäume)
- GIN: geeignet für komplexe Typen (z. B. jsonb, fts, arrays )
- BRIN: ein relativ neuer Indextyp, der Daten unterstützt, die sortiert werden können, indem Min/Max-Werte in jedem Block gespeichert werden
Niedrig, wir werden versuchen, uns mit einigen Beispielen aus der realen Welt die Hände schmutzig zu machen. Alle angegebenen Beispiele wurden mit PostgreSQL 10.0 (mit 10- und 9-psql-Clients) auf FreeBSD 11.1 erstellt.
B-Baum-Indizes
Nehmen wir an, wir haben die folgende Tabelle:
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)
Wenn wir diese ziemlich häufige Tabelle definieren, erstellt PostgreSQL hinter den Kulissen zwei eindeutige B-Tree-Indizes:part_pkey und part_partno_key. Daher wird jede eindeutige Einschränkung in PostgreSQL mit einem eindeutigen INDEX implementiert. Lassen Sie uns unsere Tabelle mit einer Million Datenzeilen füllen:
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
Lassen Sie uns nun versuchen, einige Abfragen für unsere Tabelle durchzuführen. Zuerst weisen wir den psql-Client an, Abfragezeiten zu melden, indem wir \timing:
eingebentestdb=# 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
Wir beobachten, dass es nur Bruchteile von Millisekunden dauert, um unsere Ergebnisse zu erhalten. Wir haben dies erwartet, da wir für beide in den obigen Abfragen verwendeten Spalten bereits die entsprechenden Indizes definiert haben. Lassen Sie uns nun versuchen, die Spalte partname abzufragen, für die kein Index existiert.
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 sehen wir deutlich, dass für die nicht indizierte Spalte die Performance deutlich abfällt. Lassen Sie uns nun einen Index für diese Spalte erstellen und die Abfrage wiederholen:
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
Unser neuer Index part_partname_idx ist ebenfalls ein B-Tree-Index (der Standard). Zunächst stellen wir fest, dass die Indexerstellung für die Tabelle „Millionen Zeilen“ sehr viel Zeit in Anspruch genommen hat, etwa 16 Sekunden. Dann beobachten wir, dass unsere Abfragegeschwindigkeit von 89 ms auf 0,525 ms erhöht wurde. B-Tree-Indizes können neben der Prüfung auf Gleichheit auch bei Abfragen mit anderen Operatoren für geordnete Typen hilfreich sein, z. B. <,<=,>=,>. Versuchen wir es mit <=und>=
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
Die erste Abfrage ist viel schneller als die zweite, durch die Verwendung der Schlüsselwörter EXPLAIN (oder EXPLAIN ANALYZE) können wir sehen, ob der tatsächliche Index verwendet wird oder nicht:
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
Im ersten Fall verwendet der Abfrageplaner den Index part_partname_idx. Wir beobachten auch, dass dies zu einem reinen Index-Scan führt, was bedeutet, dass überhaupt kein Zugriff auf die Datentabellen erfolgt. Im zweiten Fall stellt der Planer fest, dass es keinen Sinn macht, den Index zu verwenden, da die zurückgegebenen Ergebnisse einen großen Teil der Tabelle ausmachen, in diesem Fall wird angenommen, dass ein sequenzieller Scan schneller ist.
Hash-Indizes
Von der Verwendung von Hash-Indizes bis einschließlich PgSQL 9.6 wurde aus Gründen, die mit dem Mangel an WAL-Schreiben zu tun haben, abgeraten. Ab PgSQL 10.0 wurden diese Probleme behoben, aber die Verwendung von Hash-Indizes machte immer noch wenig Sinn. In PgSQL 11 gibt es Bemühungen, Hash-Indizes zusammen mit ihren größeren Brüdern (B-Tree, GiST, GIN) zu einer erstklassigen Indexmethode zu machen. Lassen Sie uns in diesem Sinne also einen Hash-Index in Aktion ausprobieren.
Wir werden unsere part-Tabelle mit einer neuen Spalte parttype anreichern und sie mit Werten gleicher Verteilung füllen und dann eine Abfrage ausführen, die testet, ob parttype gleich „Steering“ ist:
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
Jetzt erstellen wir einen Hash-Index für diese neue Spalte und wiederholen die vorherige Abfrage:
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
Wir stellen die Verbesserung nach der Verwendung des Hash-Index fest. Jetzt werden wir die Leistung eines Hash-Indexes für Ganzzahlen mit dem entsprechenden B-Tree-Index vergleichen.
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
Wie wir sehen, ist die Geschwindigkeit von Abfragen, die auf Gleichheit prüfen, bei der Verwendung von Hash-Indizes sehr nahe an der Geschwindigkeit von B-Tree-Indizes. Hash-Indizes sollen für Gleichheit geringfügig schneller sein als B-Bäume, tatsächlich mussten wir jede Abfrage zwei- oder dreimal versuchen, bis der Hash-Index ein besseres Ergebnis lieferte als das Äquivalent von B-Trees.
Laden Sie noch heute das Whitepaper PostgreSQL-Verwaltung und -Automatisierung mit ClusterControl herunterErfahren Sie, was Sie wissen müssen, um PostgreSQL bereitzustellen, zu überwachen, zu verwalten und zu skalierenLaden Sie das Whitepaper herunterGiST-Indizes
GiST (Generalized Search Tree) ist mehr als eine einzelne Art von Index, sondern vielmehr eine Infrastruktur zum Aufbau vieler Indizierungsstrategien. Die standardmäßige PostgreSQL-Distribution bietet Unterstützung für geometrische Datentypen, tsquery und tsvector. In contrib gibt es Implementierungen vieler anderer Operatorklassen. Beim Lesen der Dokumentation und des Contrib-Verzeichnisses wird der Leser feststellen, dass es eine ziemlich große Überschneidung zwischen GiST- und GIN-Anwendungsfällen gibt:int-Arrays, Volltextsuche, um die Hauptfälle zu nennen. In diesen Fällen ist GIN schneller, und die offizielle Dokumentation gibt dies ausdrücklich an. GiST bietet jedoch umfangreiche Unterstützung für geometrische Datentypen. Außerdem ist GiST (und SP-GiST) zum Zeitpunkt des Verfassens dieses Artikels die einzige sinnvolle Methode, die mit Ausschlussbeschränkungen verwendet werden kann. Wir werden ein Beispiel dazu sehen. Nehmen wir an (um auf dem Gebiet des Maschinenbaus zu bleiben), dass wir die Anforderung haben, Maschinentypvarianten für einen bestimmten Maschinentyp zu definieren, die für einen bestimmten Zeitraum gültig sind; und dass für eine bestimmte Änderung keine andere Änderung für denselben Maschinentyp existieren kann, deren Zeitraum sich mit dem bestimmten Änderungszeitraum überschneidet (kollidiert).
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)
);
Oben teilen wir PostgreSQL mit, dass es für jeden Maschinentypnamen (mtname) nur eine Variante (mtvar) geben kann. Start_date bezeichnet das Anfangsdatum des Zeitraums, in dem diese Maschinentypvariation gültig ist, und end_date bezeichnet das Enddatum dieses Zeitraums. Null end_date bedeutet, dass die Maschinentypvariante derzeit gültig ist. Nun wollen wir die Nichtüberlappungsbedingung durch eine Nebenbedingung ausdrücken. Der Weg, dies zu tun, ist mit einer Ausschlussbeschränkung:
testdb=# alter table machine_type ADD CONSTRAINT machine_type_per EXCLUDE USING GIST (mtname WITH =,daterange(start_date,end_date) WITH &&);
Die EXCLUDE PostgreSQL-Syntax ermöglicht es uns, viele Spalten unterschiedlichen Typs und mit jeweils einem anderen Operator anzugeben. &&ist der überlappende Operator für Datumsbereiche und =ist der gemeinsame Gleichheitsoperator für varchar. Aber solange wir die Eingabetaste drücken, beschwert sich PostgreSQL mit einer Meldung:
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.
Was hier fehlt, ist die GiST-Opclass-Unterstützung für varchar. Vorausgesetzt, wir haben die btree_gist-Erweiterung erfolgreich erstellt und installiert, können wir mit der Erstellung der Erweiterung fortfahren:
testdb=# create extension btree_gist ;
CREATE EXTENSION
Und dann erneut versuchen, die Einschränkung zu erstellen und zu 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-Indizes
SP-GiST, was für Space-partitioned GiST steht, ist wie GiST eine Infrastruktur, die die Entwicklung vieler verschiedener Strategien im Bereich nicht-ausgeglichener festplattenbasierter Datenstrukturen ermöglicht. Die standardmäßige PgSQL-Distribution bietet Unterstützung für zweidimensionale Punkte, Bereiche (jeder Art), Text- und Inet-Typen. Wie GiST kann auch SP-GiST in Ausschlussbeschränkungen verwendet werden, ähnlich wie in dem im vorherigen Kapitel gezeigten Beispiel.
GIN-Indizes
GIN (Generalized Inverted Index) wie GiST und SP-GiST können viele Indizierungsstrategien bereitstellen. GIN eignet sich, wenn wir Spalten zusammengesetzter Typen indizieren möchten. Die standardmäßige PostgreSQL-Distribution bietet Unterstützung für jeden Array-Typ, jsonb und Volltextsuche (tsvector). In contrib gibt es Implementierungen vieler anderer Operatorklassen. Jsonb, ein hochgelobtes Feature von PostgreSQL (und eine relativ neue (9.4+) Entwicklung) verlässt sich auf GIN für die Indexunterstützung. Eine weitere häufige Verwendung von GIN ist die Indizierung für die Volltextsuche. Die Volltextsuche in PgSQL verdient einen eigenen Artikel, daher behandeln wir hier nur den Indexierungsteil. Lassen Sie uns zunächst einige Vorbereitungen für unsere Tabelle treffen, indem Sie der Spalte partdescr Nicht-Null-Werte zuweisen und eine einzelne Zeile mit einem sinnvollen Wert aktualisieren:
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
Dann führen wir eine Textsuche in der neu aktualisierten Spalte durch:
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)
Das ist ziemlich langsam, fast 2 Sekunden, um unser Ergebnis zu bringen. Versuchen wir nun, einen GIN-Index für den Typ tsvector zu erstellen, und wiederholen Sie die Abfrage mit einer indexfreundlichen 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
Und wir bekommen eine 2000-fache Beschleunigung. Wir können auch die relativ kurze Zeit bemerken, die es dauerte, den Index zu erstellen. Sie können im obigen Beispiel mit der Verwendung von GiST anstelle von GIN experimentieren und die Leistung von Lese-, Schreib- und Indexerstellung für beide Zugriffsmethoden messen.
BRIN-Indizes
BRIN (Block Range Index) ist die neueste Ergänzung zu den Indextypen von PostgreSQL, seit es in PostgreSQL 9.5 eingeführt wurde und nur wenige Jahre als Standard-Kernfunktion verwendet wurde. BRIN arbeitet mit sehr großen Tabellen, indem es zusammenfassende Informationen für eine Reihe von Seiten namens „Block Range“ speichert. BRIN-Indizes sind verlustbehaftet (wie GiST) und dies erfordert sowohl zusätzliche Logik im Query Executor von PostgreSQL als auch zusätzliche Wartung. Sehen wir uns BRIN in Aktion an:
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 sehen wir im Durchschnitt eine ~18-fache Beschleunigung durch die Verwendung des BRIN-Index. Das eigentliche Zuhause von BRIN liegt jedoch im Bereich Big Data, daher hoffen wir, diese relativ neue Technologie in Zukunft in Szenarien der realen Welt zu testen.