Wie gehe ich mit einem fehlerhaften Abfrageplan um, der durch die exakte Gleichheit des Bereichstyps verursacht wird?


28

Ich führe ein Update durch, bei dem ich eine genaue Gleichheit für eine tstzrangeVariable benötige . ~ 1 Million Zeilen werden geändert, und die Abfrage dauert ~ 13 Minuten. Das Ergebnis EXPLAIN ANALYZEist zu sehen, hier , und die tatsächlichen Ergebnisse sind sehr verschieden von denen der Anfrageplaner geschätzt. Das Problem besteht darin, dass der Index-Scan für t_rangeeine einzelne Zeile erwartet, die zurückgegeben wird.

Dies scheint mit der Tatsache zusammenzuhängen, dass Statistiken zu Reichweitentypen anders gespeichert werden als solche anderer Typen. Mit Blick auf die pg_statsAnsicht für die Spalte, n_distinct-1 und anderen Bereichen (zB most_common_vals, most_common_freqs) sind leer.

Es müssen jedoch t_rangeirgendwo Statistiken gespeichert sein . Ein extrem ähnliches Update, bei dem ich 'within' für t_range anstelle einer exakten Gleichheit verwende, dauert ungefähr 4 Minuten und verwendet einen wesentlich anderen Abfrageplan (siehe hier ). Der zweite Abfrageplan ist für mich sinnvoll, da jede Zeile in der temporären Tabelle und ein wesentlicher Teil der Verlaufstabelle verwendet werden. Noch wichtiger ist, dass der Abfrageplaner eine ungefähr korrekte Anzahl von Zeilen für den aktivierten Filter vorhersagt t_range.

Die Verteilung von t_rangeist etwas ungewöhnlich. Ich verwende diese Tabelle, um den Verlaufsstatus einer anderen Tabelle zu speichern, und die Änderungen an der anderen Tabelle treten alle auf einmal in großen Speicherauszügen auf, sodass es nicht viele unterschiedliche Werte von gibt t_range. Hier sind die Zählungen, die jedem der eindeutigen Werte von entsprechen t_range:

                              t_range                              |  count  
-------------------------------------------------------------------+---------
 ["2014-06-12 20:58:21.447478+00","2014-06-27 07:00:00+00")        |  994676
 ["2014-06-12 20:58:21.447478+00","2014-08-01 01:22:14.621887+00") |   36791
 ["2014-06-27 07:00:00+00","2014-08-01 07:00:01+00")               | 1000403
 ["2014-06-27 07:00:00+00",infinity)                               |   36791
 ["2014-08-01 07:00:01+00",infinity)                               |  999753

Die oben angegebenen Zählungen t_rangesind vollständig, sodass die Kardinalität ~ 3M beträgt (von denen ~ 1M von beiden Aktualisierungsabfragen betroffen sind).

Warum ist die Leistung von Abfrage 1 viel schlechter als die von Abfrage 2? In meinem Fall ist Abfrage 2 ein guter Ersatz, aber wenn wirklich eine exakte Bereichsgleichheit erforderlich war, wie kann ich Postgres dazu bringen, einen intelligenteren Abfrageplan zu verwenden?

Tabellendefinition mit Indizes (Löschen irrelevanter Spalten):

       Column        |   Type    |                                  Modifiers                                   
---------------------+-----------+------------------------------------------------------------------------------
 history_id          | integer   | not null default nextval('gtfs_stop_times_history_history_id_seq'::regclass)
 t_range             | tstzrange | not null
 trip_id             | text      | not null
 stop_sequence       | integer   | not null
 shape_dist_traveled | real      | 
Indexes:
    "gtfs_stop_times_history_pkey" PRIMARY KEY, btree (history_id)
    "gtfs_stop_times_history_t_range" gist (t_range)
    "gtfs_stop_times_history_trip_id" btree (trip_id)

Abfrage 1:

UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND sth.t_range = '["2014-08-01 07:00:01+00",infinity)'::tstzrange;

Abfrage 2:

UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND '2014-08-01 07:00:01+00'::timestamptz <@ sth.t_range;

Q1 aktualisiert 999753 Zeilen und Q2 aktualisiert 999753 + 36791 = 1036544 (dh die temporäre Tabelle ist so, dass jede Zeile, die der Zeitbereichsbedingung entspricht, aktualisiert wird).

Ich habe diese Abfrage als Antwort auf den Kommentar von @ ypercube versucht :

Abfrage 3:

UPDATE gtfs_stop_times_history sth
SET shape_dist_traveled = tt.shape_dist_traveled
FROM gtfs_stop_times_temp tt
WHERE sth.trip_id = tt.trip_id
AND sth.stop_sequence = tt.stop_sequence
AND sth.t_range <@ '["2014-08-01 07:00:01+00",infinity)'::tstzrange
AND '["2014-08-01 07:00:01+00",infinity)'::tstzrange <@ sth.t_range;

Der Abfrageplan und die Ergebnisse (siehe hier ) lagen zwischen den beiden vorherigen Fällen (~ 6 Minuten).

2016/02/05 EDIT

Nachdem ich nach 1,5 Jahren keinen Zugriff mehr auf die Daten hatte, erstellte ich eine Testtabelle mit derselben Struktur (ohne Indizes) und ähnlicher Kardinalität. jjanes 'antwort schlug vor, dass die ursache die ordnung der temporären tabelle sein könnte, die für die aktualisierung verwendet wurde. Ich konnte die Hypothese nicht direkt testen, da ich keinen Zugriff darauf habe track_io_timing(mit Amazon RDS).

  1. Die Gesamtergebnisse waren viel schneller (um einen Faktor von mehreren). Ich vermute, das liegt an der Entfernung der Indizes, was mit Erwins Antwort übereinstimmt .

  2. In diesem Testfall haben die Abfragen 1 und 2 im Wesentlichen dieselbe Zeit in Anspruch genommen, da beide den Merge-Join verwendet haben. Das heißt, ich konnte nicht auslösen, was auch immer Postgres veranlasste, den Hash-Join zu wählen. Daher habe ich keine Klarheit darüber, warum Postgres den Hash-Join mit schlechter Leistung überhaupt gewählt hat.


1
Was passiert , wenn Sie den Gleichheits Zustand umgewandelt (a = b)zu zwei „enthält“ Bedingungen: (a @> b AND b @> a)? Ändert sich der Plan?
ypercubeᵀᴹ

@ypercube: Der Plan ändert sich erheblich, obwohl er noch nicht ganz optimal ist - siehe meine Änderung # 2.
Abeboparebop

1
Eine andere Idee wäre, einen regulären Btree-Index für hinzuzufügen, (lower(t_range),upper(t_range))da Sie die Gleichheit prüfen.
ypercubeᵀᴹ

Antworten:


9

Der größte Zeitunterschied in Ihren Ausführungsplänen befindet sich auf dem obersten Knoten, dem UPDATE selbst. Dies deutet darauf hin, dass der Großteil Ihrer Zeit während des Updates für die E / A-Vorgänge aufgewendet wird. Sie können dies überprüfen, indem Sie track_io_timingdie Abfragen aktivieren und mit ausführenEXPLAIN (ANALYZE, BUFFERS)

In den verschiedenen Plänen werden Zeilen angezeigt, die in verschiedenen Reihenfolgen aktualisiert werden sollen. Eine ist in trip_idOrdnung und die andere ist in der Reihenfolge, in der sie in der Temp-Tabelle physisch vorhanden sind.

Die physische Reihenfolge der zu aktualisierenden Tabelle scheint mit der trip_id-Spalte zu korrelieren, und die Aktualisierung der Zeilen in dieser Reihenfolge führt zu effizienten E / A-Mustern mit Vorauslese- / sequentiellen Lesevorgängen. Während die physische Reihenfolge der temporären Tabelle zu vielen zufälligen Lesevorgängen zu führen scheint.

Wenn Sie order by trip_idder Anweisung, mit der die temporäre Tabelle erstellt wurde, eine hinzufügen können, ist das Problem möglicherweise für Sie gelöst.

PostgreSQL berücksichtigt bei der Planung des UPDATE-Vorgangs nicht die Auswirkungen der E / A-Bestellung. (Im Gegensatz zu SELECT-Operationen, bei denen diese berücksichtigt werden). Wenn PostgreSQL cleverer wäre, würde es entweder erkennen, dass ein Plan eine effizientere Reihenfolge ergibt, oder es würde einen expliziten Sortierknoten zwischen dem Update und seinem untergeordneten Knoten einfügen, sodass das Update Zeilen in der Reihenfolge ctid erhält.

Sie haben Recht, dass PostgreSQL die Selektivität von Gleichheitsverknüpfungen in Bereichen schlecht einschätzt. Dies hängt jedoch nur tangential mit Ihrem Grundproblem zusammen. Eine effizientere Abfrage des ausgewählten Teils Ihres Updates kann versehentlich dazu führen, dass Zeilen in einer besseren Reihenfolge in das eigentliche Update eingefügt werden. In diesem Fall ist dies jedoch meistens ein Zufall.


Leider kann ich keine Änderungen vornehmen track_io_timingund (seit anderthalb Jahren!) Habe ich keinen Zugriff mehr auf die Originaldaten. Ich habe Ihre Theorie jedoch getestet, indem ich Tabellen mit demselben Schema und ähnlicher Größe (Millionen von Zeilen) erstellt und zwei verschiedene Aktualisierungen ausgeführt habe - eine, in der die temporäre Aktualisierungstabelle wie die Originaltabelle sortiert war, und eine andere, in der sie sortiert war quasi zufällig. Leider dauern die beiden Aktualisierungen ungefähr gleich lange, was bedeutet, dass die Reihenfolge der Aktualisierungstabelle diese Abfrage nicht beeinflusst.
Abeboparebop

7

Ich bin mir nicht ganz sicher, warum die Selektivität eines Gleichheitsprädikats durch den GiST-Index für das Prädikat so radikal überbewertet wird tstzrange Spalte . Das ist zwar per se interessant, scheint aber für Ihren speziellen Fall irrelevant.

Da Sie UPDATEein Drittel (!) Aller vorhandenen 3M-Zeilen ändern, hilft ein Index überhaupt nicht . Im Gegenteil, wenn Sie den Index zusätzlich zur Tabelle inkrementell aktualisieren, entstehen Ihrem Index erhebliche KostenUPDATE .

Behalten Sie einfach Ihre einfache Abfrage 1 bei . Die einfache, radikale Lösung besteht darin , den Index vor dem zu senkenUPDATE . Wenn Sie es für andere Zwecke benötigen, erstellen Sie es nach dem erneut UPDATE. Dies wäre immer noch schneller als die Aufrechterhaltung des Index während der großenUPDATE .

Für ein UPDATEDrittel aller Zeilen lohnt es sich wahrscheinlich, auch alle anderen Indizes zu löschen - und sie nach dem erneut zu erstellen UPDATE. Der einzige Nachteil: Sie benötigen zusätzliche Berechtigungen und eine exklusive Sperre für den Tisch (nur für einen kurzen Moment, wenn Sie verwenden CREATE INDEX CONCURRENTLY).

Die Idee von @ypercube, einen Btree anstelle des GiST-Index zu verwenden, scheint grundsätzlich gut zu sein. Aber nicht für ein Drittel aller Zeilen (bei denen kein Index anfänglich von Vorteil ist) und nicht für nur (lower(t_range),upper(t_range)), da es sich tstzrangenicht um einen diskreten Bereich handelt.

Die meisten diskreten Bereichstypen haben eine kanonische Form, was das Konzept der "Gleichheit" vereinfacht: Die Unter- und Obergrenze des Wertes in kanonischer Form definieren ihn. Die Dokumentation:

Ein diskreter Bereichstyp sollte eine Kanonisierung aufweisen , die die gewünschte Schrittgröße für den Elementtyp kennt. Die Kanonisierungsfunktion ist damit beauftragt, Äquivalenzwerte des Bereichstyps in identische Darstellungen umzuwandeln, insbesondere konsequent inklusive oder exklusive Grenzen. Wenn keine Kanonisierungsfunktion angegeben ist, werden Bereiche mit unterschiedlicher Formatierung immer als ungleich behandelt, auch wenn sie in der Realität möglicherweise denselben Wertesatz darstellen.

Die Einbau-Bereichstypen int4range, int8rangeund die daterangegesamte Nutzung eine kanonische Form , die die untere Grenze , und umfasst nicht die obere Grenze enthält; das heißt [),. Benutzerdefinierte Bereichstypen können jedoch andere Konventionen verwenden.

Dies ist nicht der Fall tstzrange, wenn die Inklusivität von Ober- und Untergrenze für die Gleichheit berücksichtigt werden muss. Ein möglicher Btree-Index müsste lauten:

(lower(t_range), upper(t_range), lower_inc(t_range), upper_inc(t_range))

Und die Abfragen müssten die gleichen Ausdrücke in der WHEREKlausel verwenden.

Man könnte versucht sein, nur den gesamten Wert zu indizieren, auf den gewechselt wird text: (cast(t_range AS text))- Dieser Ausdruck ist jedoch nicht IMMUTABLEder Fall, da die Textdarstellung von timestamptzWerten von der aktuellen timezoneEinstellung abhängt . Sie müssten zusätzliche Schritte in eine IMMUTABLEWrapper-Funktion einfügen, die eine kanonische Form erzeugt, und einen Funktionsindex dafür erstellen ...

Zusätzliche Maßnahmen / alternative Ideen

Wenn Sie shape_dist_traveledbereits denselben Wert wie tt.shape_dist_traveledfür mehr als einige Ihrer aktualisierten Zeilen haben können (und Sie sich nicht auf Nebenwirkungen Ihrer UPDATEähnlichen Trigger verlassen ...), können Sie Ihre Abfrage beschleunigen, indem Sie leere Aktualisierungen ausschließen:

WHERE ...
AND   shape_dist_traveled IS DISTINCT FROM tt.shape_dist_traveled;

Selbstverständlich gelten alle allgemeinen Hinweise zur Leistungsoptimierung. Das Postgres Wiki ist ein guter Ausgangspunkt.

VACUUM FULLDas wäre Gift für Sie, da einige tote Tupel (oder der von ihnen reservierte Platz FILLFACTOR) der UPDATELeistung zuträglich sind .

Bei so vielen aktualisierten Zeilen und wenn Sie es sich leisten können (kein gleichzeitiger Zugriff oder andere Abhängigkeiten), ist es möglicherweise sogar noch schneller, eine komplett neue Tabelle zu schreiben, anstatt sie direkt zu aktualisieren. Anweisungen in dieser verwandten Antwort:

Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.