Senza accesso simultaneo alla scrittura
Materializza una selezione in un CTE e unisciti ad essa nella FROM
clausola del 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;
Inizialmente avevo una semplice subquery qui, ma questo può eludere il LIMIT
per determinati piani di query come ha sottolineato Feike :
Il pianificatore può scegliere di generare un piano che esegue un ciclo nidificato sulla LIMITing
sottoquery, causando più UPDATEs
di LIMIT
, ad esempio:
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
Riproduzione del test case
Il modo per risolvere quanto sopra era avvolgere la LIMIT
subquery nel proprio CTE, poiché il CTE è materializzato non restituirà risultati diversi su diverse iterazioni del loop nidificato.
Oppure usa una subquery poco correlata per il caso semplice conLIMIT
1
. Più semplice, più veloce:
UPDATE server_info
SET status = 'active'
WHERE server_ip = (
SELECT server_ip
FROM server_info
WHERE status = 'standby'
LIMIT 1
)
RETURNING server_ip;
Con accesso in scrittura simultanea
Supponendo il livello di isolamento predefinitoREAD COMMITTED
per tutto questo. Livelli di isolamento più severi ( REPEATABLE READ
e SERIALIZABLE
) possono comunque causare errori di serializzazione. Vedere:
Con carico di scrittura simultaneo, aggiungi FOR UPDATE SKIP LOCKED
per bloccare la riga per evitare le condizioni di gara. SKIP LOCKED
è stato aggiunto in Postgres 9.5 , per le versioni precedenti vedi sotto. Il manuale:
Con SKIP LOCKED
, tutte le righe selezionate che non possono essere immediatamente bloccate vengono ignorate. Saltare le righe bloccate offre una visione incoerente dei dati, quindi non è adatto per lavori di carattere generale, ma può essere utilizzato per evitare contese di blocco con più utenti che accedono a una tabella simile a una coda.
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;
Se non è disponibile alcuna riga qualificante, sbloccata, non accade nulla in questa query (nessuna riga viene aggiornata) e si ottiene un risultato vuoto. Per operazioni non critiche ciò significa che hai finito.
Tuttavia, le transazioni simultanee potrebbero avere righe bloccate, ma non completare l'aggiornamento ( ROLLBACK
o altri motivi). Per essere sicuro esegui un controllo finale:
SELECT NOT EXISTS (
SELECT 1
FROM server_info
WHERE status = 'standby'
);
SELECT
vede anche le file bloccate. Se non viene restituito true
, una o più righe sono ancora in fase di elaborazione e le transazioni potrebbero comunque essere ripristinate. (O nel frattempo sono state aggiunte nuove righe.) Aspetta un po ', quindi esegui i due passaggi in sequenza: ( UPDATE
finché non ottieni nessuna riga indietro; SELECT
...) finché non ottieni true
.
Relazionato:
Senza SKIP LOCKED
PostgreSQL 9.4 o precedente
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;
Le transazioni simultanee che tentano di bloccare la stessa riga vengono bloccate fino a quando la prima non rilascia il blocco.
Se il primo è stato ripristinato, la transazione successiva prende il blocco e procede normalmente; altri in coda continuano ad aspettare.
Se il primo commit, la WHERE
condizione viene rivalutata e se non è TRUE
più ( status
è cambiata) il CTE (in qualche modo sorprendentemente) non restituisce alcuna riga. Non succede nulla. Questo è il comportamento desiderato quando tutte le transazioni vogliono aggiornare la stessa riga .
Ma non quando ogni transazione vuole aggiornare la riga successiva . E poiché vogliamo solo aggiornare una riga arbitraria (o casuale ) , non ha senso aspettare affatto.
Possiamo sbloccare la situazione con l'aiuto di blocchi consultivi :
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;
In questo modo, la riga successiva non ancora bloccata verrà aggiornata. Ogni transazione ottiene una nuova riga con cui lavorare. Ho ricevuto aiuto dal ceco Postgres Wiki per questo trucco.
id
essere una bigint
colonna univoca (o qualsiasi tipo con un cast implicito come int4
o int2
).
Se i blocchi di avviso sono in uso contemporaneamente per più tabelle nel database, non chiarire la questione pg_try_advisory_xact_lock(tableoid::int, id)
: id
essere univoci integer
qui.
Poiché tableoid
è una bigint
quantità, può teoricamente traboccare integer
. Se sei abbastanza paranoico, usa (tableoid::bigint % 2147483648)::int
invece - lasciando una "collisione dell'hash" teorica per i veri paranoici ...
Inoltre, Postgres è libero di testare le WHERE
condizioni in qualsiasi ordine. Si potrebbe testare pg_try_advisory_xact_lock()
e acquisire un blocco prima status = 'standby'
, che potrebbe comportare ulteriori serrature consultivo su righe indipendenti, quando status = 'standby'
non è vero. Domanda correlata su SO:
In genere, puoi semplicemente ignorarlo. Per garantire che solo le righe qualificanti siano bloccate, è possibile nidificare i predicati in un CTE come sopra o una sottoquery con l' OFFSET 0
hack (impedisce l'inline) . Esempio:
Oppure (più economico per le scansioni sequenziali) annidare le condizioni in CASE
un'istruzione come:
WHERE CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END
Tuttavia, il CASE
trucco impedirebbe anche a Postgres di utilizzare un indice status
. Se un tale indice è disponibile, non è necessario nidificare ulteriormente per iniziare: solo le righe qualificanti verranno bloccate in una scansione dell'indice.
Dal momento che non puoi essere sicuro che un indice venga utilizzato in ogni chiamata, puoi semplicemente:
WHERE status = 'standby'
AND CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END
Il CASE
è logicamente ridondante, ma i server lo scopo discusso.
Se il comando fa parte di una transazione lunga, considerare i blocchi a livello di sessione che possono essere (e devono essere) rilasciati manualmente. Quindi puoi sbloccare non appena hai finito con la riga bloccata: pg_try_advisory_lock()
epg_advisory_unlock()
. Il manuale:
Una volta acquisito a livello di sessione, viene mantenuto un blocco di avviso fino al rilascio esplicito o alla fine della sessione.
Relazionato: