Arbeiten von Indizes in PostgreSQL

73

Ich habe einige Fragen zum Arbeiten mit Indizes in PostgreSQL. Ich habe eine FriendsTabelle mit folgendem Index:

   Friends ( user_id1 ,user_id2) 

user_id1und user_id2sind Fremdschlüssel zum userTisch

  1. Sind diese gleichwertig? Wenn nicht, warum dann?

    Index(user_id1,user_id2) and Index(user_id2,user_id1)
  2. Wenn ich einen Primärschlüssel (user_id1, user_id2) erstelle, werden automatisch Indizes dafür und erstellt

    Wenn die Indizes in der ersten Frage nicht äquivalent sind, welcher Index wird dann für den obigen Primärschlüsselbefehl erstellt?

codecool
quelle

Antworten:

77

Hier sind die Ergebnisse der Abfrage einer Tabelle in der zweiten Spalte eines mehrspaltigen Index .
Die Effekte sind für jedermann leicht zu reproduzieren. Probieren Sie es einfach zu Hause.

Ich habe mit PostgreSQL 9.0.5 unter Debian mit einer mittelgroßen Tabelle einer realen Datenbank mit 23322 Zeilen getestet. Es implementiert die n: m-Beziehung zwischen den Tabellen adr(Adresse) und att(Attribut), aber das ist hier nicht relevant. Vereinfachtes Schema:

CREATE TABLE adratt (
  adratt_id serial PRIMARY KEY
, adr_id    integer NOT NULL
, att_id    integer NOT NULL
, log_up    timestamp(0) NOT NULL DEFAULT (now())::timestamp(0)
, CONSTRAINT adratt_uni UNIQUE (adr_id, att_id)
);

Die UNIQUEEinschränkung implementiert effektiv einen eindeutigen Index. Ich wiederholte den Test mit einem einfachen Index, um sicherzugehen, und erhielt die gleichen Ergebnisse wie erwartet.

CREATE INDEX adratt_idx ON adratt(adr_id, att_id)

Die Tabelle ist im adratt_uniIndex zusammengefasst und vor dem Test habe ich Folgendes ausgeführt:

CLUSTER adratt;
ANALYZE adratt;

Sequentielle Abfragen (adr_id, att_id)werden so schnell wie möglich ausgeführt. Der mehrspaltige Index wird weiterhin nur für eine Abfragebedingung in der zweiten Indexspalte verwendet.

Ich habe die Abfragen ein paar Mal ausgeführt, um den Cache zu füllen, und aus zehn Läufen die besten ausgewählt, um vergleichbare Ergebnisse zu erzielen.

1. Abfrage mit beiden Spalten

SELECT *
FROM   adratt
WHERE  att_id = 90
AND    adr_id = 10;

 adratt_id | adr_id | att_id |       log_up
-----------+--------+--------+---------------------
       123 |     10 |     90 | 2008-07-29 09:35:54
(1 row)

Ausgabe von EXPLAIN ANALYZE:

Index Scan using adratt_uni on adratt  (cost=0.00..3.48 rows=1 width=20) (actual time=0.022..0.025 rows=1 loops=1)
  Index Cond: ((adr_id = 10) AND (att_id = 90))
Total runtime: 0.067 ms

2. Abfrage mit der ersten Spalte

SELECT * FROM adratt WHERE adr_id = 10

 adratt_id | adr_id | att_id |       log_up
-----------+--------+--------+---------------------
       126 |     10 |     10 | 2008-07-29 09:35:54
       125 |     10 |     13 | 2008-07-29 09:35:54
      4711 |     10 |     21 | 2008-07-29 09:35:54
     29322 |     10 |     22 | 2011-06-06 15:50:38
     29321 |     10 |     30 | 2011-06-06 15:47:17
       124 |     10 |     62 | 2008-07-29 09:35:54
     21913 |     10 |     78 | 2008-07-29 09:35:54
       123 |     10 |     90 | 2008-07-29 09:35:54
     28352 |     10 |    106 | 2010-11-22 12:37:50
(9 rows)

Ausgabe von EXPLAIN ANALYZE:

Index Scan using adratt_uni on adratt  (cost=0.00..8.23 rows=9 width=20) (actual time=0.007..0.023 rows=9 loops=1)
  Index Cond: (adr_id = 10)
Total runtime: 0.058 ms

3. Abfrage über zweite Spalte

SELECT * FROM adratt WHERE att_id = 90

 adratt_id | adr_id | att_id |       log_up
-----------+--------+--------+---------------------
       123 |     10 |     90 | 2008-07-29 09:35:54
       180 |     39 |     90 | 2008-08-29 15:46:07
...
(83 rows)

Ausgabe von EXPLAIN ANALYZE:

Index Scan using adratt_uni on adratt  (cost=0.00..818.51 rows=83 width=20) (actual time=0.014..0.694 rows=83 loops=1)
  Index Cond: (att_id = 90)
Total runtime: 0.849 ms

4. Deaktivieren Sie Indexscan und Bitmapscan

SET enable_indexscan = off;
SELECT * FROM adratt WHERE att_id = 90

Ausgabe von EXPLAIN ANALYZE:

Bitmap Heap Scan on adratt  (cost=779.94..854.74 rows=83 width=20) (actual time=0.558..0.743 rows=83 loops=1)
  Recheck Cond: (att_id = 90)
  ->  Bitmap Index Scan on adratt_uni  (cost=0.00..779.86 rows=83 width=0) (actual time=0.544..0.544 rows=83 loops=1)
        Index Cond: (att_id = 90)
Total runtime: 0.894 ms
SET enable_bitmapscan = off
SELECT * FROM adratt WHERE att_id = 90

Ausgabe von EXPLAIN ANALYZE:

Seq Scan on adratt  (cost=0.00..1323.10 rows=83 width=20) (actual time=0.009..2.429 rows=83 loops=1)
  Filter: (att_id = 90)
Total runtime: 2.680 ms

Fazit

Wie erwartet wird der mehrspaltige Index nur für eine Abfrage in der zweiten Spalte verwendet.
Wie erwartet ist es weniger effektiv, aber die Abfrage ist immer noch dreimal schneller als ohne den Index.
Nach dem Deaktivieren von Index-Scans wählt der Abfrageplaner einen Bitmap-Heap-Scan, der fast genauso schnell ausgeführt wird. Erst wenn auch dies deaktiviert ist, wird auf einen sequentiellen Scan zurückgegriffen.

Erwin Brandstetter
quelle
Clustering wird einen Unterschied machen , wenn die Anzahl der Spiele im Index hoch genug ist (siehe hier für Beweis - man beachte die doppelten Durchläufe die Daten im Cache gespeicherte zu bekommen)
Jack Douglas
1
@JackDouglas: Ich habe mir das etwas genauer überlegt. Clustering kann generell helfen, da es effektiv auch a vacuum fullund a ist reindex. Ansonsten hilft es, die Suchläufe in der ersten oder beiden führenden Spalten stark zu indizieren , aber die Suchläufe in der zweiten Spalte zu beeinträchtigen. In einer frisch gruppierten Tabelle werden Zeilen mit demselben Wert in der zweiten Spalte so verteilt, dass maximal Blöcke gelesen werden müssen.
Erwin Brandstetter
28

zu 1) Ja und nein.

Bei einer Abfrage, die beide Spalten verwendet, spielt es zB where (user_id1, user_id2) = (1,2)keine Rolle, welcher Index erstellt wird.

Für eine Abfrage, die nur eine Bedingung für eine der Spalten enthält where user_id1 = 1, ist dies beispielsweise von Bedeutung, da der Optimierer normalerweise nur die "führenden" Spalten für einen Vergleich verwenden kann. Somit where user_id1 = 1könnte der Index (user_id1, user_id2) verwendet werden, aber nicht für alle Fälle ein Index (user_id2, user_id1).

Nachdem ich damit herumgespielt habe (nachdem Erwin uns so freundlich ein Setup gezeigt hat, wo es funktioniert), scheint es, dass dies stark von der Datenverteilung der zweiten Spalte abhängt, obwohl ich noch nicht herausgefunden habe, in welcher Situation der Optimierer nachfolgende Spalten verwenden kann für eine WHERE-Bedingung.

Oracle 11 kann (manchmal) auch Spalten verwenden, die nicht am Anfang der Indexdefinition stehen.

Zu 2) Ja, es wird ein Index erstellt

Zitat aus dem Handbuch

Durch das Hinzufügen eines Primärschlüssels wird automatisch ein eindeutiger Btree-Index für die im Primärschlüssel verwendete Spalte oder Spaltengruppe erstellt.

Zu 2a) Primary Key (user_id1,user_id2)wird ein Index für (user_id1, user_id2) erstellt (was Sie sehr einfach selbst herausfinden können, indem Sie einfach einen solchen Primärschlüssel erstellen).

Ich empfehle Ihnen nachdrücklich, das Kapitel über Indizes im Handbuch zu lesen. Es beantwortet im Grunde alle oben gestellten Fragen.

Außerdem, welcher Index erstellt werden soll? von depesz erklärt die Reihenfolge von Indexspalten und anderen indexbezogenen Themen sehr gut.

ein Pferd ohne Name
quelle
11

Ad 1)
Es gibt Einschränkungen in PostgreSQL wie @a_horse_with_no_name beschreibt . Bis zur Version 8.0 konnten mehrspaltige Indizes nur für Abfragen in den führenden Spalten verwendet werden. Dies wurde in Version 8.1 verbessert. Das aktuelle Handbuch für Postgres 10 (aktualisiert) erklärt:

Ein mehrspaltiger B-Tree-Index kann mit Abfragebedingungen verwendet werden, an denen eine beliebige Teilmenge der Indexspalten beteiligt ist. Der Index ist jedoch am effizientesten, wenn Einschränkungen für die führenden (am weitesten links stehenden) Spalten bestehen. Die genaue Regel lautet, dass Gleichheitsbeschränkungen für führende Spalten sowie alle Ungleichheitsbeschränkungen für die erste Spalte, für die keine Gleichheitsbeschränkung gilt, verwendet werden, um den Teil des Indexes zu begrenzen, der gescannt wird. Einschränkungen für Spalten rechts von diesen Spalten sind im Index aktiviert, sodass Besuche in der eigentlichen Tabelle gespeichert werden, der zu durchsuchende Teil des Index wird jedoch nicht reduziert. Beispielsweise müsste bei einem Index für (a, b, c)und einer Abfragebedingung WHERE a = 5 AND b >= 42 AND c < 77der Index ab dem ersten Eintrag mit a= 5 und durchsucht werdenb= 42 bis zum letzten Eintrag mit a= 5. Indexeinträge mit c> = 77 würden übersprungen, müssten aber noch durchsucht werden. Dieser Index könnte im Prinzip für Abfragen verwendet werden, für die Einschränkungen gelten bund / oder cfür die keine Einschränkungen gelten. aIn den meisten Fällen müsste jedoch der gesamte Index gescannt werden. In den meisten Fällen würde der Planer eine sequentielle Tabellensuche der Verwendung des Index vorziehen.

Betonung meiner. Das kann ich aus Erfahrung bestätigen.
Siehe auch den Testfall, der meiner späteren Antwort hier hinzugefügt wurde .

Erwin Brandstetter
quelle
11

Dies ist eine Antwort auf Jacks Antwort , ein Kommentar würde nicht genügen.

Vor Version 9.2 gab es in PostgreSQL keine Deckungsindizes . Aufgrund des MVCC-Modells muss jedes Tupel in der Ergebnismenge besucht werden, um die Sichtbarkeit zu überprüfen. Möglicherweise denken Sie an Oracle.

PostgreSQL-Entwickler sprechen von "Nur-Index-Scans" . Tatsächlich wurde die Funktion mit Postgres 9.2 veröffentlicht. Lesen Sie die Commit-Nachricht .
Depesz hat einen sehr informativen Blog-Beitrag geschrieben .

True-Covering-Indizes (Update) werden mit der INCLUDEKlausel mit Postgres 11 eingeführt.

Das ist auch ein bisschen abwegig:

Dies beruht auf der Tatsache, dass ein vollständiger Scan eines Index aufgrund der zusätzlichen Spalten in der Tabelle, die nicht im Index enthalten sind, häufig schneller ist als ein vollständiger Scan der indizierten Tabelle.

Wie in den Kommentaren zu meiner anderen Antwort angegeben, habe ich auch Tests mit einer Tabelle mit zwei ganzen Zahlen und sonst nichts durchgeführt. Der Index enthält dieselben Spalten wie die Tabelle. Die Größe eines Btree-Index beträgt ungefähr 2/3 der Größe der Tabelle. Nicht genug, um eine Beschleunigung von Faktor 3 zu erklären. Ich habe mehr Tests durchgeführt, basierend auf Ihrem Setup, vereinfacht auf zwei Spalten und mit 100000 Zeilen. Bei meiner PostgreSQL 9.0-Installation waren die Ergebnisse konsistent.

Wenn die Tabelle zusätzliche Spalten enthält, wird die Beschleunigung mit dem Index beträchtlicher, aber dies ist sicherlich nicht der einzige Faktor .

Um die wichtigsten Punkte zusammenzufassen:

  • Mehrspaltenindizes können mit Abfragen für nicht führende Spalten verwendet werden, aber die Beschleunigung liegt nur bei Faktor 3 für ausgewählte Kriterien (kleiner Prozentsatz der Zeilen im Ergebnis). Höher für größere Tupel, niedriger für größere Teile der Tabelle in der Ergebnismenge.

  • Erstellen Sie einen zusätzlichen Index für diese Spalten, wenn die Leistung wichtig ist.

  • Wenn alle beteiligten Spalten in einem Index enthalten sind (der den Index abdeckt) und alle beteiligten Zeilen (pro Block) für alle Transaktionen sichtbar sind, können Sie in S. 9.2 oder höher einen "Nur-Index-Scan" durchführen .

Erwin Brandstetter
quelle
7
  1. Sind diese gleichwertig? Wenn nicht, warum dann?

    Index (Benutzer_ID1, Benutzer_ID2) und Index (Benutzer_ID2, Benutzer_ID1)

Diese sind nicht gleichwertig und im Allgemeinen ist der Index (Balken, Baz) für Abfragen des Formulars nicht effizient select * from foo where baz=?

Erwin hat gezeigt, dass solche Indizes zwar eine Abfrage beschleunigen können, aber dieser Effekt ist begrenzt und nicht in der gleichen Größenordnung, wie Sie allgemein erwarten, dass ein Index eine Suche verbessert. Er beruht auf der Tatsache, dass ein Index häufig vollständig durchsucht wird Schneller als ein vollständiger Scan der indizierten Tabelle aufgrund der zusätzlichen Spalten in der Tabelle, die nicht im Index enthalten sind.

Zusammenfassung: Indizes können bei Abfragen auch für nicht führende Spalten hilfreich sein, jedoch auf eine von zwei sekundären und relativ geringfügigen Arten und nicht auf die dramatische Weise, die ein Index normalerweise aufgrund seiner Btree-Struktur bietet

nb die zwei Möglichkeiten, die der Index bietet, sind, wenn ein vollständiger Index-Scan erheblich günstiger ist als ein vollständiger Tabellenscan und entweder: 1. die Tabellensuche ist günstig (weil es nur wenige gibt oder sie in Clustern vorliegen), oder 2. Der Index deckt, also gibt es überhaupt keine Tabellensuche , siehe Erwins Kommentare hier

Prüfstand:

create table foo(bar integer not null, baz integer not null, qux text not null);

insert into foo(bar, baz, qux)
select random()*100, random()*100, 'some random text '||g from generate_series(1,10000) g;

Abfrage 1 (kein Index, Treffer 74 Puffer ):

explain (buffers, analyze, verbose) select max(qux) from foo where baz=0;
                                                  QUERY PLAN
--------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=181.41..181.42 rows=1 width=32) (actual time=3.301..3.302 rows=1 loops=1)
   Output: max(qux)
   Buffers: shared hit=74
   ->  Seq Scan on stack.foo  (cost=0.00..181.30 rows=43 width=32) (actual time=0.043..3.228 rows=52 loops=1)
         Output: bar, baz, qux
         Filter: (foo.baz = 0)
         Buffers: shared hit=74
 Total runtime: 3.335 ms

Abfrage 2 (mit Index - der Optimierer ignoriert den Index - Treffer) 74 Puffer ):

create index bar_baz on foo(bar, baz);

explain (buffers, analyze, verbose) select max(qux) from foo where baz=0;
                                                  QUERY PLAN
--------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=199.12..199.13 rows=1 width=32) (actual time=3.277..3.277 rows=1 loops=1)
   Output: max(qux)
   Buffers: shared hit=74
   ->  Seq Scan on stack.foo  (cost=0.00..199.00 rows=50 width=32) (actual time=0.043..3.210 rows=52 loops=1)
         Output: bar, baz, qux
         Filter: (foo.baz = 0)
         Buffers: shared hit=74
 Total runtime: 3.311 ms

Abfrage 2 (mit Index - und wir täuschen den Optimierer vor, ihn zu verwenden):

explain (buffers, analyze, verbose) select max(qux) from foo where bar>-1000 and baz=0;
                                                       QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=115.56..115.57 rows=1 width=32) (actual time=1.495..1.495 rows=1 loops=1)
   Output: max(qux)
   Buffers: shared hit=36 read=30
   ->  Bitmap Heap Scan on stack.foo  (cost=73.59..115.52 rows=17 width=32) (actual time=1.370..1.428 rows=52 loops=1)
         Output: bar, baz, qux
         Recheck Cond: ((foo.bar > (-1000)) AND (foo.baz = 0))
         Buffers: shared hit=36 read=30
         ->  Bitmap Index Scan on bar_baz  (cost=0.00..73.58 rows=17 width=0) (actual time=1.356..1.356 rows=52 loops=1)
               Index Cond: ((foo.bar > (-1000)) AND (foo.baz = 0))
               Buffers: shared read=30
 Total runtime: 1.535 ms

Der Zugriff über den Index ist in diesem Fall also doppelt so schnell wie bei 30 Puffern - was in Bezug auf die Indizierung "etwas schneller" ist! der Daten in der Tabelle

Im Gegensatz dazu verwenden Abfragen in der führenden Spalte die Btree-Struktur des Index - in diesem Fall 2 Puffer :

explain (buffers, analyze, verbose) select max(qux) from foo where bar=0;
                                                       QUERY PLAN
------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=75.70..75.71 rows=1 width=32) (actual time=0.172..0.173 rows=1 loops=1)
   Output: max(qux)
   Buffers: shared hit=38
   ->  Bitmap Heap Scan on stack.foo  (cost=4.64..75.57 rows=50 width=32) (actual time=0.036..0.097 rows=59 loops=1)
         Output: bar, baz, qux
         Recheck Cond: (foo.bar = 0)
         Buffers: shared hit=38
         ->  Bitmap Index Scan on bar_baz  (cost=0.00..4.63 rows=50 width=0) (actual time=0.024..0.024 rows=59 loops=1)
               Index Cond: (foo.bar = 0)
               Buffers: shared hit=2
 Total runtime: 0.209 ms
Jack Douglas
quelle