Index für Primärschlüssel wird beim einfachen Join nicht verwendet

16

Ich habe die folgenden Tabellen- und Indexdefinitionen:

CREATE TABLE munkalap (
    munkalap_id serial PRIMARY KEY,
    ...
);

CREATE TABLE munkalap_lepes (
    munkalap_lepes_id serial PRIMARY KEY,
    munkalap_id integer REFERENCES munkalap (munkalap_id),
    ...
);

CREATE INDEX idx_munkalap_lepes_munkalap_id ON munkalap_lepes (munkalap_id);

Warum wird in der folgenden Abfrage keiner der Indizes für munkalap_id verwendet?

EXPLAIN ANALYZE SELECT ml.* FROM munkalap m JOIN munkalap_lepes ml USING (munkalap_id);

QUERY PLAN
Hash Join  (cost=119.17..2050.88 rows=38046 width=214) (actual time=0.824..18.011 rows=38046 loops=1)
  Hash Cond: (ml.munkalap_id = m.munkalap_id)
  ->  Seq Scan on munkalap_lepes ml  (cost=0.00..1313.46 rows=38046 width=214) (actual time=0.005..4.574 rows=38046 loops=1)
  ->  Hash  (cost=78.52..78.52 rows=3252 width=4) (actual time=0.810..0.810 rows=3253 loops=1)
        Buckets: 1024  Batches: 1  Memory Usage: 115kB
        ->  Seq Scan on munkalap m  (cost=0.00..78.52 rows=3252 width=4) (actual time=0.003..0.398 rows=3253 loops=1)
Total runtime: 19.786 ms

Es ist das gleiche, auch wenn ich einen Filter hinzufüge:

EXPLAIN ANALYZE SELECT ml.* FROM munkalap m JOIN munkalap_lepes ml USING (munkalap_id) WHERE NOT lezarva;

QUERY PLAN
Hash Join  (cost=79.60..1545.79 rows=1006 width=214) (actual time=0.616..10.824 rows=964 loops=1)
  Hash Cond: (ml.munkalap_id = m.munkalap_id)
  ->  Seq Scan on munkalap_lepes ml  (cost=0.00..1313.46 rows=38046 width=214) (actual time=0.007..5.061 rows=38046 loops=1)
  ->  Hash  (cost=78.52..78.52 rows=86 width=4) (actual time=0.587..0.587 rows=87 loops=1)
        Buckets: 1024  Batches: 1  Memory Usage: 4kB
        ->  Seq Scan on munkalap m  (cost=0.00..78.52 rows=86 width=4) (actual time=0.014..0.560 rows=87 loops=1)
              Filter: (NOT lezarva)
Total runtime: 10.911 ms
dezso
quelle

Antworten:

21

Viele Leute haben gehört, dass "sequentielle Scans schlecht sind" und versuchen, sie aus ihren Plänen zu streichen, aber es ist nicht so einfach. Wenn eine Abfrage jede Zeile in einer Tabelle abdeckt, ist ein sequentieller Scan der schnellste Weg, um diese Zeilen abzurufen. Dies ist der Grund, warum Ihre ursprüngliche Join-Abfrage seq scan verwendet hat, da alle Zeilen in beiden Tabellen erforderlich waren.

Bei der Planung einer Abfrage schätzt der Postgres-Planer die Kosten für verschiedene Vorgänge (Berechnungen, sequentielle und zufällige E / A-Vorgänge) nach verschiedenen möglichen Schemata und wählt den Plan aus, für den er die niedrigsten Kosten veranschlagt. Wenn E / A-Vorgänge von rotierenden Speichern (Festplatten) ausgeführt werden, sind E / A-Vorgänge in der Regel wesentlich langsamer als sequenzielle E / A-Vorgänge. In der Standardkonfiguration pg für random_page_cost und seq_page_cost wird ein Kostenunterschied von 4: 1 geschätzt.

Diese Überlegungen kommen zum Tragen, wenn eine Join- oder Filtermethode in Betracht gezogen wird, bei der ein Index mit einem Index verglichen wird, der nacheinander eine Tabelle durchsucht. Wenn ein Index verwendet wird, kann der Plan eine Zeile schnell über den Index finden und muss dann einen zufälligen Blocklesevorgang berücksichtigen, um die Zeilendaten aufzulösen. Im Fall Ihrer zweiten Abfrage, die ein Filterprädikat hinzugefügt hat WHERE NOT lezarva, können Sie in den EXPLAIN ANALYZE-Ergebnissen sehen, wie sich dies auf die Planungsschätzungen ausgewirkt hat. Der Planer schätzt 1006 Zeilen, die sich aus dem Join ergeben (was ziemlich genau der tatsächlichen Ergebnismenge von 964 entspricht). Angesichts der Tatsache, dass die größere Tabelle munkalap_lepes ungefähr 38 KB Zeilen enthält, sieht der Planer, dass der Join auf ungefähr 1006/38046 oder 1/38 der Zeilen in der Tabelle zugreifen muss. Es ist auch bekannt, dass die durchschnittliche Zeilenbreite 214 Bytes beträgt und ein Block 8 KB groß ist, sodass ungefähr 38 Zeilen / Block vorhanden sind.

Mit diesen Statistiken geht der Planer davon aus, dass der Join alle oder die meisten Datenblöcke der Tabelle lesen muss. Da die Indexsuche ebenfalls nicht kostenlos ist und die Berechnung zum Scannen eines Blocks, der eine Filterbedingung auswertet, im Vergleich zu E / A sehr kostengünstig ist, hat sich der Planer dafür entschieden, die Tabelle sequentiell zu scannen und Index-Overhead und zufällige Lesevorgänge bei der Berechnung des seq-Scans zu vermeiden wird schneller sein.

In der realen Welt sind Daten häufig über den Seitencache des Betriebssystems im Arbeitsspeicher verfügbar, sodass nicht für jedes Lesen von Blöcken E / A erforderlich ist. Es kann ziemlich schwierig sein vorherzusagen, wie effektiv ein Cache für eine bestimmte Abfrage sein wird, aber der Pg-Planer verwendet einige einfache Heuristiken. Der Konfigurationswert effective_cache_size informiert die Planer über die Wahrscheinlichkeit, dass tatsächlich IO-Kosten anfallen. Ein größerer Wert führt dazu, dass zufällige E / A-Vorgänge mit geringeren Kosten veranlasst werden, und tendiert daher möglicherweise zu einer indexgesteuerten Methode über einen sequentiellen Scan.

dbenhur
quelle
Vielen Dank, dies ist die bisher beste (und prägnanteste) Beschreibung, die ich gelesen habe. Einige wichtige Punkte geklärt.
Dezso
1
Hervorragende Erklärung. Die Berechnung der Zeilen / Datenseite ist allerdings etwas abwegig. Sie müssen den Seitenkopf (24 Bytes) + 4 Bytes für jeden Zeiger HeapTupleHeaderpro Zeilenelement + den Zeilenkopf (23 Bytes pro Zeile) + die NULL-Bitmaske + die Ausrichtung gemäß MAXALIGN berücksichtigen. Schließlich ist ein unbekannter Auffüllbetrag aufgrund der Datenausrichtung abhängig von den Datentypen der Spalten und deren Reihenfolge. Insgesamt befinden sich in diesem Fall nicht mehr als 33 Zeilen auf einer 8-KB-Seite. (Ohne Berücksichtigung von TOAST.)
Erwin Brandstetter
1
@ErwinBrandstetter Vielen Dank, dass Sie genauere Zeilengrößenberechnungen durchgeführt haben. Ich war immer davon ausgegangen, dass die von EXPLAIN ausgegebene Schätzung der Zeilenbreite zeilenbezogene Überlegungen wie den Header und die NULL-Bitmaske enthalten würde, jedoch keinen Overhead auf Seitenebene.
Dbenhur
1
@dbenhur: Sie können eine schnelle EXPLAIN ANALYZE SELECT foo from barÜberprüfung mit einer einfachen Dummy-Tabelle durchführen. Der tatsächliche Speicherplatz auf der Festplatte hängt auch von der Datenausrichtung ab. Dies ist schwer zu berücksichtigen, wenn nur einige Zeilen abgerufen werden. Die Zeilenbreite in EXPLAINstellt den grundlegenden Platzbedarf für den abgerufenen Satz von Spalten dar.
Erwin Brandstetter
5

Sie rufen alle Zeilen aus beiden Tabellen ab, sodass die Verwendung eines Index-Scans keinen wirklichen Vorteil bietet. Ein Index-Scan ist nur dann sinnvoll, wenn Sie nur wenige Zeilen aus einer Tabelle auswählen (in der Regel weniger als 10% -15%).

ein Pferd ohne Name
quelle
Ja, Sie haben Recht :) Ich habe versucht, die Situation mit einem spezifischeren Fall zu klären, siehe die letzte Abfrage.
Dezso
@dezso: Gleiches. Wenn Sie einen Index haben (lezarva, munkalap_id)und dieser selektiv genug ist, kann er verwendet werden. Das NOTmacht das weniger wahrscheinlich.
Ypercubeᵀᴹ
Aufgrund Ihres Vorschlags habe ich einen Teilindex hinzugefügt, der verwendet wird, sodass das halbe Problem gelöst ist. Aber ich würde nicht erwarten, dass der Index für den Fremdschlüssel unbrauchbar wird, da ich mich gegen nur 87 Werte im Vergleich zu den ursprünglichen 3252
anschließen
1
@dezso Die Zeilen sind durchschnittlich 214 Bytes breit, sodass Sie etwas weniger als 40 Zeilen pro 8-KByte-Datenblock haben. Die Selektivität des Index beträgt ebenfalls etwa 1/40 (1006/38046). Pg zeigt also, dass das sequentielle Lesen aller Blöcke billiger ist als das wahrscheinliche zufällige Lesen von etwa der gleichen Anzahl von Blöcken, wenn der Index verwendet wird. Diese geschätzten Kompromisse können durch die Konfigurationswerte effective_cache_size und random_page_cost beeinflusst werden.
Dbenhur
@dbenhur: könntest du deinem Kommentar eine richtige Antwort geben?
Dezso