Ohne gleichzeitigen Schreibzugriff
Materialisieren Sie eine Auswahl in einem CTE und fügen Sie sie in die FROM
Klausel des CTE ein UPDATE
.
WITH cte AS (
SELECT server_ip -- pk column or any (set of) unique column(s)
FROM server_info
WHERE status = 'standby'
LIMIT 1 -- arbitrary pick (cheapest)
)
UPDATE server_info s
SET status = 'active'
FROM cte
WHERE s.server_ip = cte.server_ip
RETURNING server_ip;
Ich hatte ursprünglich eine einfache Unterabfrage hier, aber das kann die LIMIT
für bestimmte Abfragepläne umgehen, wie Feike betonte:
Der Planer kann sich dafür entscheiden, einen Plan zu generieren, der eine verschachtelte Schleife über der LIMITing
Unterabfrage ausführt, die mehr UPDATEs
als Folgendes verursacht LIMIT
:
Update on buganalysis [...] rows=5
-> Nested Loop
-> Seq Scan on buganalysis
-> Subquery Scan on sub [...] loops=11
-> Limit [...] rows=2
-> LockRows
-> Sort
-> Seq Scan on buganalysis
Testfall wird reproduziert
Die Lösung bestand darin, die LIMIT
Unterabfrage in einem eigenen CTE zu verpacken , da beim Materialisieren des CTE bei verschiedenen Iterationen der verschachtelten Schleife keine unterschiedlichen Ergebnisse zurückgegeben werden.
Oder verwenden Sie eine schwach korrelierte Unterabfrage für den einfachen Fall mitLIMIT
1
. Einfacher, schneller:
UPDATE server_info
SET status = 'active'
WHERE server_ip = (
SELECT server_ip
FROM server_info
WHERE status = 'standby'
LIMIT 1
)
RETURNING server_ip;
Mit gleichzeitigem Schreibzugriff
Angenommen, die StandardisolationsstufeREAD COMMITTED
für all dies. Strengere Isolationsstufen ( REPEATABLE READ
und SERIALIZABLE
) können weiterhin zu Serialisierungsfehlern führen. Sehen:
Fügen Sie unter gleichzeitiger Schreiblast hinzu FOR UPDATE SKIP LOCKED
, um die Zeile zu sperren, um Rennbedingungen zu vermeiden. SKIP LOCKED
wurde in Postgres 9.5 hinzugefügt , ältere Versionen siehe unten. Das Handbuch:
Mit SKIP LOCKED
werden alle markierten Zeilen, die nicht sofort gesperrt werden können, übersprungen. Das Überspringen gesperrter Zeilen bietet eine inkonsistente Ansicht der Daten. Dies ist daher nicht für allgemeine Zwecke geeignet, kann jedoch verwendet werden, um Sperrenkonflikte zu vermeiden, wenn mehrere Konsumenten auf eine warteschlangenartige Tabelle zugreifen.
UPDATE server_info
SET status = 'active'
WHERE server_ip = (
SELECT server_ip
FROM server_info
WHERE status = 'standby'
LIMIT 1
FOR UPDATE SKIP LOCKED
)
RETURNING server_ip;
Wenn keine qualifizierende, nicht gesperrte Zeile mehr vorhanden ist, passiert in dieser Abfrage nichts (es wird keine Zeile aktualisiert) und Sie erhalten ein leeres Ergebnis. Für unkritische Vorgänge bedeutet dies, dass Sie fertig sind.
Bei gleichzeitigen Transaktionen sind möglicherweise Zeilen gesperrt, die Aktualisierung wird jedoch nicht abgeschlossen ( ROLLBACK
oder aus anderen Gründen). Um sicherzugehen, führen Sie eine Endkontrolle durch:
SELECT NOT EXISTS (
SELECT 1
FROM server_info
WHERE status = 'standby'
);
SELECT
sieht auch gesperrte Zeilen. Wenn dies nicht der Fall ist true
, werden noch eine oder mehrere Zeilen verarbeitet, und Transaktionen können noch zurückgesetzt werden. (Oder in der Zwischenzeit wurden neue Zeilen hinzugefügt.) Warten Sie ein wenig, und wiederholen Sie dann die beiden Schritte ( UPDATE
bis Sie keine Zeile mehr zurückbekommen; SELECT
...), bis Sie erhalten true
.
Verbunden:
Ohne SKIP LOCKED
in PostgreSQL 9.4 oder älter
UPDATE server_info
SET status = 'active'
WHERE server_ip = (
SELECT server_ip
FROM server_info
WHERE status = 'standby'
LIMIT 1
FOR UPDATE
)
RETURNING server_ip;
Gleichzeitige Transaktionen, die versuchen, dieselbe Zeile zu sperren, werden gesperrt, bis die erste ihre Sperre aufhebt.
Wenn die erste zurückgesetzt wurde, übernimmt die nächste Transaktion die Sperre und fährt normal fort. andere in der Warteschlange warten weiter.
Wenn das erste Commit ausgeführt wird, wird die WHERE
Bedingung erneut ausgewertet, und wenn sie nicht TRUE
mehr vorhanden ist ( status
sich geändert hat), gibt der CTE (etwas überraschend) keine Zeile zurück. Nichts passiert. Das ist das gewünschte Verhalten , wenn alle Transaktionen aktualisieren mögen die gleiche Zeile .
Aber nicht, wenn jede Transaktion die nächste Zeile aktualisieren möchte . Und da wir nur eine beliebige (oder zufällige ) Zeile aktualisieren möchten , ist es sinnlos, überhaupt zu warten.
Wir können die Situation mit Hilfe von entsperren Beratungssperren :
UPDATE server_info
SET status = 'active'
WHERE server_ip = (
SELECT server_ip
FROM server_info
WHERE status = 'standby'
AND pg_try_advisory_xact_lock(id)
LIMIT 1
FOR UPDATE
)
RETURNING server_ip;
Auf diese Weise wird die nächste noch nicht gesperrte Zeile aktualisiert. Jede Transaktion erhält eine neue Zeile, mit der gearbeitet werden kann. Ich hatte Hilfe von Czech Postgres Wiki für diesen Trick.
id
ist eine eindeutige bigint
Spalte (oder ein Typ mit einer impliziten Besetzung wie int4
oder int2
).
Wenn Advisory-Sperren für mehrere Tabellen in Ihrer Datenbank gleichzeitig verwendet werden, sollten Sie pg_try_advisory_xact_lock(tableoid::int, id)
- hier id
eindeutig integer
angeben.
Da tableoid
es sich um eine bigint
Menge handelt, kann sie theoretisch überlaufen integer
. Wenn Sie paranoid genug sind, verwenden Sie (tableoid::bigint % 2147483648)::int
stattdessen - lassen Sie eine theoretische "Hash-Kollision" für die wirklich paranoiden ...
Außerdem kann Postgres die WHERE
Bedingungen in beliebiger Reihenfolge testen . Es könnte vorherpg_try_advisory_xact_lock()
eine Sperre testen und erwerben , was zu zusätzlichen Hinweissperren für nicht verwandte Zeilen führen könnte, wenn dies nicht zutrifft. Verwandte Frage zu SO: status = 'standby'
status = 'standby'
Normalerweise können Sie dies einfach ignorieren. Um sicherzustellen, dass nur qualifizierende Zeilen gesperrt sind, können Sie das / die Prädikat (e) in einem CTE wie oben oder einer Unterabfrage mit dem OFFSET 0
Hack verschachteln (Inlining wird verhindert) . Beispiel:
Oder (billiger für sequentielle Scans) verschachteln Sie die Bedingungen in einer CASE
Anweisung wie:
WHERE CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END
Doch das CASE
würde Trick auch Postgres halten verwenden , einen Index auf status
. Wenn ein solcher Index verfügbar ist, ist zunächst keine zusätzliche Verschachtelung erforderlich: Bei einem Index-Scan werden nur qualifizierende Zeilen gesperrt.
Da Sie nicht sicher sein können, dass in jedem Aufruf ein Index verwendet wird, können Sie einfach:
WHERE status = 'standby'
AND CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END
Das CASE
ist logisch redundant, dient aber dem besprochenen Zweck.
Wenn der Befehl Teil einer langen Transaktion ist, sollten Sie Sperren auf Sitzungsebene in Betracht ziehen, die manuell freigegeben werden können (und müssen). So können Sie entsperren, sobald Sie mit der gesperrten Zeile fertig sind: pg_try_advisory_lock()
undpg_advisory_unlock()
. Das Handbuch:
Einmal auf Sitzungsebene erworben, wird eine Beratungssperre gehalten, bis sie explizit freigegeben wird oder die Sitzung endet.
Verbunden: