Postgres UPDATE… LIMIT 1


77

Ich habe eine Postgres-Datenbank, die Details zu Serverclustern enthält, z. B. den Serverstatus ("Aktiv", "Standby" usw.). Aktive Server müssen möglicherweise jederzeit auf einen Standby-Modus umschalten, und es ist mir egal, welcher Standby-Modus im Besonderen verwendet wird.

Ich möchte, dass eine Datenbankabfrage den Status eines Standbys ändert - NUR EINS - und die zu verwendende Server-IP zurückgibt. Die Auswahl kann beliebig sein: Da sich der Status des Servers mit der Abfrage ändert, spielt es keine Rolle, welcher Standby-Modus ausgewählt ist.

Kann ich meine Abfrage auf nur ein Update beschränken?

Folgendes habe ich bisher:

UPDATE server_info SET status = 'active' 
WHERE status = 'standby' [[LIMIT 1???]] 
RETURNING server_ip;

Postgres gefällt das nicht. Was könnte ich anders machen?


Wählen Sie einfach den Server im Code aus und fügen Sie ihn als Where Constrained hinzu. Auf diese Weise können Sie auch zusätzliche Bedingungen (älteste, neueste, aktuellste, am wenigsten geladene, gleiche DC, anderes Rack, am wenigsten Fehler) ohnehin zuerst überprüfen lassen. Die meisten Failover-Protokolle erfordern ohnehin einen gewissen Determinismus.
Eckes

@eckes Das ist eine interessante Idee. In meinem Fall hätte "Server im Code auswählen" bedeutet, zuerst eine Liste der verfügbaren Server aus der Datenbank zu lesen und dann einen Datensatz zu aktualisieren. Da viele Instanzen der Anwendung diese Aktion ausführen können, gibt es eine Racebedingung und eine atomare Operation ist erforderlich (oder war vor 5 Jahren). Die Wahl musste nicht deterministisch sein.
Superiorman

Antworten:


125

Ohne gleichzeitigen Schreibzugriff

Materialisieren Sie eine Auswahl in einem CTE und fügen Sie sie in die FROMKlausel 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 LIMITfü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 LIMITingUnterabfrage ausführt, die mehr UPDATEsals 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 LIMITUnterabfrage 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 READund 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 LOCKEDwurde in Postgres 9.5 hinzugefügt , ältere Versionen siehe unten. Das Handbuch:

Mit SKIP LOCKEDwerden 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 ( ROLLBACKoder aus anderen Gründen). Um sicherzugehen, führen Sie eine Endkontrolle durch:

SELECT NOT EXISTS (
   SELECT 1
   FROM   server_info
   WHERE  status = 'standby'
   );

SELECTsieht 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 ( UPDATEbis Sie keine Zeile mehr zurückbekommen; SELECT...), bis Sie erhalten true.

Verbunden:

Ohne SKIP LOCKEDin 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 WHEREBedingung erneut ausgewertet, und wenn sie nicht TRUEmehr vorhanden ist ( statussich 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.

idist eine eindeutige bigintSpalte (oder ein Typ mit einer impliziten Besetzung wie int4oder 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 ideindeutig integerangeben.
Da tableoides sich um eine bigintMenge handelt, kann sie theoretisch überlaufen integer. Wenn Sie paranoid genug sind, verwenden Sie (tableoid::bigint % 2147483648)::intstattdessen - lassen Sie eine theoretische "Hash-Kollision" für die wirklich paranoiden ...

Außerdem kann Postgres die WHEREBedingungen 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 0Hack verschachteln (Inlining wird verhindert) . Beispiel:

Oder (billiger für sequentielle Scans) verschachteln Sie die Bedingungen in einer CASEAnweisung wie:

WHERE  CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END

Doch das CASEwü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 CASEist 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:

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.