PostgreSQL
 sql >> Datenbank >  >> RDS >> PostgreSQL

Zur Nützlichkeit von Ausdrucksindizes

Beim Unterrichten von PostgreSQL-Schulungen, sowohl zu Grundlagen- als auch zu fortgeschrittenen Themen, stelle ich oft fest, dass die Teilnehmer sehr wenig Ahnung haben, wie leistungsfähig die Ausdrucksindizes sein können (wenn sie sich dessen überhaupt bewusst sind). Lassen Sie mich Ihnen einen kurzen Überblick geben.

Nehmen wir also an, wir haben eine Tabelle mit einer Reihe von Zeitstempeln (ja, wir haben eine generate_series-Funktion, die Daten generieren kann):

CREATE TABLE t AS
SELECT d, repeat(md5(d::text), 10) AS padding
  FROM generate_series(timestamp '1900-01-01',
                       timestamp '2100-01-01',
                       interval '1 day') s(d);
VACUUM ANALYZE t;

Die Tabelle enthält auch eine Füllspalte, um sie etwas größer zu machen. Lassen Sie uns nun eine einfache Bereichsabfrage durchführen und nur einen Monat aus den ~200 Jahren auswählen, die in der Tabelle enthalten sind. Wenn Sie die Abfrage erklären, sehen Sie etwa Folgendes:

EXPLAIN SELECT * FROM t WHERE d BETWEEN '2001-01-01' AND '2001-02-01';

                               QUERY PLAN
------------------------------------------------------------------------
 Seq Scan on t  (cost=0.00..4416.75 rows=32 width=332)
   Filter: ((d >= '2001-01-01 00:00:00'::timestamp without time zone)
        AND (d <= '2001-02-01 00:00:00'::timestamp without time zone))
(2 rows)

und auf meinem Laptop läuft dies in ~ 20 ms. Nicht schlecht, wenn man bedenkt, dass dies die gesamte Tabelle mit ~75.000 Zeilen durchlaufen muss.

Aber lassen Sie uns einen Index für die Timestamp-Spalte erstellen (alle Indizes hier sind der Standardtyp, d. h. btree, sofern nicht ausdrücklich erwähnt):

CREATE INDEX idx_t_d ON t (d);

Und jetzt versuchen wir, die Abfrage erneut auszuführen:

                               QUERY PLAN
------------------------------------------------------------------------
 Index Scan using idx_t_d on t  (cost=0.29..9.97 rows=34 width=332)
   Index Cond: ((d >= '2001-01-01 00:00:00'::timestamp without time zone)
            AND (d <= '2001-02-01 00:00:00'::timestamp without time zone))
(2 rows)

und das läuft in 0,5 ms, also ungefähr 40x schneller. Aber das war natürlich ein einfacher Index, der direkt auf der Spalte erstellt wurde, kein Ausdrucksindex. Nehmen wir also an, wir müssen stattdessen Daten von jedem 1. Tag jedes Monats auswählen, indem wir eine Abfrage wie diese durchführen

SELECT * FROM t WHERE EXTRACT(day FROM d) = 1;

die jedoch den Index nicht verwenden kann, da sie einen Ausdruck für die Spalte auswerten muss, während der Index für die Spalte selbst erstellt wird, wie in EXPLAIN ANALYZE:

gezeigt
                               QUERY PLAN
------------------------------------------------------------------------
 Seq Scan on t  (cost=0.00..4416.75 rows=365 width=332)
                (actual time=0.045..40.601 rows=2401 loops=1)
   Filter: (date_part('day'::text, d) = '1'::double precision)
   Rows Removed by Filter: 70649
 Planning time: 0.209 ms
 Execution time: 43.018 ms
(5 rows)

Dies muss also nicht nur einen sequentiellen Scan durchführen, sondern auch die Auswertung, wodurch sich die Abfragedauer auf 43 ms erhöht.

Die Datenbank kann den Index aus mehreren Gründen nicht verwenden. Indizes (zumindest Btree-Indizes) verlassen sich auf die Abfrage sortierter Daten, die von der baumartigen Struktur bereitgestellt werden, und während die Bereichsabfrage davon profitieren kann, kann die zweite Abfrage (mit `extract`-Aufruf) dies nicht.

Hinweis:Ein weiteres Problem ist, dass der Satz von Operatoren, die von Indizes unterstützt werden (d. h. die direkt auf Indizes ausgewertet werden können), sehr begrenzt ist. Und die Funktion „Extrahieren“ wird nicht unterstützt, sodass die Abfrage das Sortierproblem nicht mithilfe eines Bitmap-Index-Scans umgehen kann.

Theoretisch könnte die Datenbank versuchen, die Bedingung in Bereichsbedingungen umzuwandeln, aber das ist extrem schwierig und spezifisch für den Ausdruck. In diesem Fall müssten wir unendlich viele solcher „pro Tag“-Bereiche generieren, da der Planer die Min/Max-Zeitstempel in der Tabelle nicht wirklich kennt. Also versucht die Datenbank es erst gar nicht.

Aber während die Datenbank nicht weiß, wie sie die Bedingungen umwandeln soll, tun es Entwickler oft. Zum Beispiel mit Bedingungen wie

(column + 1) >= 1000

es ist nicht schwer, es so umzuschreiben

column >= (1000 - 1)

was mit den Indizes gut funktioniert.

Was aber, wenn eine solche Transformation nicht möglich ist, wie zum Beispiel für die Beispielabfrage

SELECT * FROM t WHERE EXTRACT(day FROM d) = 1;

In diesem Fall müsste der Entwickler mit dem gleichen Problem mit unbekanntem Min/Max für die d-Spalte konfrontiert werden, und selbst dann würde es viele Bereiche generieren.

Nun, in diesem Blogbeitrag geht es um Ausdrucksindizes, und bisher haben wir nur reguläre Indizes verwendet, die direkt auf der Spalte aufbauen. Lassen Sie uns also den ersten Ausdrucksindex erstellen:

CREATE INDEX idx_t_expr ON t ((extract(day FROM d)));
ANALYZE t;

was uns dann diesen Erklärungsplan gibt

                               QUERY PLAN
------------------------------------------------------------------------
 Bitmap Heap Scan on t  (cost=47.35..3305.25 rows=2459 width=332)
                        (actual time=2.400..12.539 rows=2401 loops=1)
   Recheck Cond: (date_part('day'::text, d) = '1'::double precision)
   Heap Blocks: exact=2401
   ->  Bitmap Index Scan on idx_t_expr  (cost=0.00..46.73 rows=2459 width=0)
                                (actual time=1.243..1.243 rows=2401 loops=1)
         Index Cond: (date_part('day'::text, d) = '1'::double precision)
 Planning time: 0.374 ms
 Execution time: 17.136 ms
(7 rows)

Dies gibt uns zwar nicht die gleiche 40-fache Beschleunigung wie der Index im ersten Beispiel, aber das ist irgendwie zu erwarten, da diese Abfrage weitaus mehr Tupel zurückgibt (2401 vs. 32). Außerdem sind diese über die ganze Tabelle verteilt und nicht so lokalisiert wie im ersten Beispiel. Es ist also eine nette 2-fache Beschleunigung, und in vielen realen Fällen werden Sie viel größere Verbesserungen sehen.

Aber die Möglichkeit, Indizes für Bedingungen mit komplexen Ausdrücken zu verwenden, ist hier nicht die interessanteste Information – das ist sozusagen der Grund, warum Menschen Ausdrucksindizes erstellen. Aber das ist nicht der einzige Vorteil.

Wenn Sie sich die beiden oben vorgestellten EXPLAIN-Pläne ansehen (ohne und mit dem Ausdrucksindex), fällt Ihnen vielleicht Folgendes auf:

                               QUERY PLAN
------------------------------------------------------------------------
 Seq Scan on t  (cost=0.00..4416.75 rows=365 width=332)
                (actual time=0.045..40.601 rows=2401 loops=1)
 ...
                               QUERY PLAN
------------------------------------------------------------------------
 Bitmap Heap Scan on t  (cost=47.35..3305.25 rows=2459 width=332)
                        (actual time=2.400..12.539 rows=2401 loops=1)
 ...

Richtig – Erstellen des Expressionsindex deutlich verbesserte Schätzungen. Ohne den Index haben wir nur Statistiken (MCV + Histogramm) für Rohtabellenspalten, sodass die Datenbank nicht weiß, wie sie den Ausdruck schätzen soll

EXTRACT(day FROM d) = 1

Daher wird stattdessen eine Standardschätzung für Gleichheitsbedingungen angewendet, die 0,5 % aller Zeilen beträgt – da die Tabelle 73050 Zeilen enthält, erhalten wir am Ende eine Schätzung von nur 365 Zeilen. Es ist üblich, in realen Anwendungen viel schlimmere Schätzfehler zu sehen.

Mit dem Index hat die Datenbank jedoch auch Statistiken zu Spalten des Index gesammelt, und in diesem Fall enthält die Spalte Ergebnisse des Ausdrucks. Und während der Planung bemerkt der Optimierer dies und erstellt eine viel bessere Schätzung.

Dies ist ein großer Vorteil und kann bei der Behebung einiger Fälle von schlechten Abfrageplänen helfen, die durch ungenaue Schätzungen verursacht wurden. Doch die meisten Menschen kennen dieses praktische Tool nicht.

Und die Nützlichkeit dieses Tools hat mit der Einführung des JSONB-Datentyps in 9.4 nur noch zugenommen, da dies die einzige Möglichkeit ist, Statistiken über den Inhalt der JSONB-Dokumente zu sammeln.

Bei der Indizierung von JSONB-Dokumenten gibt es zwei grundlegende Indizierungsstrategien. Sie können entweder einen GIN/GiST-Index über das gesamte Dokument erstellen, z. so

CREATE INDEX ON t USING GIN (jsonb_column);

Dadurch können Sie beliebige Pfade in der JSONB-Spalte abfragen, den Containment-Operator verwenden, um Unterdokumente abzugleichen usw. Das ist großartig, aber Sie haben immer noch nur die grundlegenden Statistiken pro Spalte, die
nicht sehr nützlich sind wie die Dokumente werden als skalare Werte behandelt (und niemand gleicht ganze Dokumente ab oder verwendet eine Reihe von Dokumenten).

Ausdrucksindizes, die beispielsweise so erstellt werden:

CREATE INDEX ON t ((jsonb_column->'id'));

wird nur für den bestimmten Ausdruck nützlich sein, d. h. dieser neu erstellte Index wird für

nützlich sein
SELECT * FROM t WHERE jsonb_column ->> 'id' = 123;

aber nicht für Abfragen, die auf andere JSON-Schlüssel zugreifen, wie zum Beispiel „Wert“

SELECT * FROM t WHERE jsonb_column ->> 'value' = 'xxxx';

Das soll nicht heißen, dass GIN/GiST-Indizes für das gesamte Dokument nutzlos sind, aber Sie müssen sich entscheiden. Entweder erstellen Sie einen fokussierten Ausdrucksindex, der beim Abfragen eines bestimmten Schlüssels nützlich ist und den zusätzlichen Vorteil von Statistiken zum Ausdruck bietet. Oder Sie erstellen einen GIN/GiST-Index für das gesamte Dokument, der Abfragen zu beliebigen Schlüsseln verarbeiten kann, jedoch ohne Statistik.

In diesem Fall können Sie jedoch auch einen Kuchen essen, da Sie beide Indizes gleichzeitig erstellen können und die Datenbank auswählt, welcher von ihnen für einzelne Abfragen verwendet wird. Und dank der Ausdrucksindizes haben Sie genaue Statistiken.

Leider können Sie nicht den ganzen Kuchen essen, da Ausdrucksindizes und GIN/GiST-Indizes unterschiedliche Bedingungen verwenden

-- expression (btree)
SELECT * FROM t WHERE jsonb_column ->> 'id' = 123;

-- GIN/GiST
SELECT * FROM t WHERE jsonb_column @> '{"id" : 123}';

der Planer kann sie also nicht gleichzeitig verwenden – Ausdrucksindizes für die Schätzung und GIN/GiST für die Ausführung.