Ich führe ein Update durch, bei dem ich eine genaue Gleichheit für eine tstzrange
Variable benötige . ~ 1 Million Zeilen werden geändert, und die Abfrage dauert ~ 13 Minuten. Das Ergebnis EXPLAIN ANALYZE
ist 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_range
eine 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_stats
Ansicht für die Spalte, n_distinct
-1 und anderen Bereichen (zB most_common_vals
, most_common_freqs
) sind leer.
Es müssen jedoch t_range
irgendwo 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_range
ist 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_range
sind 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).
Die Gesamtergebnisse waren viel schneller (um einen Faktor von mehreren). Ich vermute, das liegt an der Entfernung der Indizes, was mit Erwins Antwort übereinstimmt .
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.
(a = b)
zu zwei „enthält“ Bedingungen:(a @> b AND b @> a)
? Ändert sich der Plan?(lower(t_range),upper(t_range))
da Sie die Gleichheit prüfen.Antworten:
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_timing
die 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_id
Ordnung 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_id
der 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.
quelle
track_io_timing
und (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.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
UPDATE
ein 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 senken
UPDATE
. Wenn Sie es für andere Zwecke benötigen, erstellen Sie es nach dem erneutUPDATE
. Dies wäre immer noch schneller als die Aufrechterhaltung des Index während der großenUPDATE
.Für ein
UPDATE
Drittel aller Zeilen lohnt es sich wahrscheinlich, auch alle anderen Indizes zu löschen - und sie nach dem erneut zu erstellenUPDATE
. 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 verwendenCREATE 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 sichtstzrange
nicht 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:
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:Und die Abfragen müssten die gleichen Ausdrücke in der
WHERE
Klausel verwenden.Man könnte versucht sein, nur den gesamten Wert zu indizieren, auf den gewechselt wird
text
:- Dieser Ausdruck ist jedoch nicht(cast(t_range AS text))
IMMUTABLE
der Fall, da die Textdarstellung vontimestamptz
Werten von der aktuellentimezone
Einstellung abhängt . Sie müssten zusätzliche Schritte in eineIMMUTABLE
Wrapper-Funktion einfügen, die eine kanonische Form erzeugt, und einen Funktionsindex dafür erstellen ...Zusätzliche Maßnahmen / alternative Ideen
Wenn Sie
shape_dist_traveled
bereits denselben Wert wiett.shape_dist_traveled
für mehr als einige Ihrer aktualisierten Zeilen haben können (und Sie sich nicht auf Nebenwirkungen IhrerUPDATE
ähnlichen Trigger verlassen ...), können Sie Ihre Abfrage beschleunigen, indem Sie leere Aktualisierungen ausschließen:Selbstverständlich gelten alle allgemeinen Hinweise zur Leistungsoptimierung. Das Postgres Wiki ist ein guter Ausgangspunkt.
VACUUM FULL
Das wäre Gift für Sie, da einige tote Tupel (oder der von ihnen reservierte PlatzFILLFACTOR
) derUPDATE
Leistung 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:
quelle