Без одночасного доступу до запису
Матеріалізуйте виділення в CTE та приєднайтесь до нього в FROM
пункті 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;
Спочатку у мене був простий підзапрос, але це може призвести до LIMIT
певних планів запитів, як зазначив Фейке :
Планувальник може вибрати , щоб створити план , який виконується вкладений цикл по LIMITing
підзапиту, в результаті чого більш UPDATEs
ніж 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
Відтворення тестового випадку
Способом виправити вищезазначене було загортати LIMIT
підзапит у власний CTE, оскільки CTE матеріалізується, він не повертає різних результатів на різних ітераціях вкладеного циклу.
Або використовувати низько корельований підзапит для простого випадку зLIMIT
1
. Простіше, швидше:
UPDATE server_info
SET status = 'active'
WHERE server_ip = (
SELECT server_ip
FROM server_info
WHERE status = 'standby'
LIMIT 1
)
RETURNING server_ip;
З одночасним доступом на запис
Припускаючи рівень ізоляції за замовчуваннямREAD COMMITTED
для всього цього. Більш жорсткі рівні ( REPEATABLE READ
та SERIALIZABLE
) ізоляції все ще можуть призвести до помилок серіалізації. Побачити:
Під одночасним завантаженням запису додайте FOR UPDATE SKIP LOCKED
для блокування рядка, щоб уникнути перегонів. SKIP LOCKED
додано в Postgres 9.5 , для старих версій див. нижче. Посібник:
З SKIP LOCKED
, будь-які вибрані рядки, які неможливо негайно заблокувати, пропускаються. Пропуск заблокованих рядків забезпечує непослідовність перегляду даних, тому це не підходить для роботи загального призначення, але його можна використовувати, щоб уникнути суперечок із блокуванням, коли декілька споживачів звертаються до таблиці, що нагадує чергу.
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;
Якщо відсутній кваліфікований розблокований рядок, у цьому запиті нічого не відбувається (жодна рядок не оновлюється), і ви отримуєте порожній результат. Для некритичних операцій, це означає, що ви зробили.
Однак, одночасні транзакції можуть мати заблоковані рядки, але потім не закінчуйте оновлення ( ROLLBACK
або інші причини). Щоб впевнитись, проведіть остаточну перевірку:
SELECT NOT EXISTS (
SELECT 1
FROM server_info
WHERE status = 'standby'
);
SELECT
також бачить заблоковані рядки. Wile, яка не повертається true
, один або кілька рядків все ще обробляються, і транзакції все ще можуть бути повернуті назад. (Або нові рядки були додані тим часом.) Почекайте трохи, а потім обведіть два кроки: ( UPDATE
поки не отримаєте жодного ряду назад; SELECT
...), поки не отримаєте true
.
Пов'язані:
Без SKIP LOCKED
в PostgreSQL 9.4 або новіших версій
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;
Одночасні транзакції, що намагаються заблокувати той самий рядок, блокуються, поки перша не звільнить його блокування.
Якщо перша була відкатана назад, наступна транзакція бере блокування і проходить нормально; інші в черзі продовжують чекати.
Якщо перший здійснено, WHERE
умова переоцінюється, і якщо це більше не TRUE
було ( status
змінилося), CTE (дещо дивно) не повертає жодного рядка. Нічого не відбувається. Це бажана поведінка, коли всі транзакції хочуть оновити один і той же рядок .
Але не тоді , коли кожна транзакція хоче оновити на наступну рядок . А оскільки ми просто хочемо оновити довільний (або випадковий ) рядок , чекати взагалі немає сенсу.
Ми можемо розблокувати ситуацію за допомогою дорадчих блокувань :
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;
Таким чином, наступний ряд, який ще не заблокований, буде оновлений. Кожна транзакція отримує новий рядок, з яким потрібно працювати. Я мав допомогу з чеського Postgres Wiki для цього фокусу.
id
будь-який унікальний bigint
стовпець (або будь-який тип із неявним символом типу int4
або int2
).
Якщо довідкові блокування використовуються для декількох таблиць у вашій базі даних одночасно, розмежуйте їх pg_try_advisory_xact_lock(tableoid::int, id)
- id
це унікальний integer
тут.
Оскільки tableoid
це bigint
кількість, вона теоретично може переповнювати integer
. Якщо ви є параноїком, використовуйте (tableoid::bigint % 2147483648)::int
замість цього - залишаючи теоретичне "хеш-зіткнення" для справді параноїка ...
Також Postgres може безкоштовно перевіряти WHERE
умови в будь-якому порядку. Він може перевірити pg_try_advisory_xact_lock()
та придбати замок раніше status = 'standby'
, що може призвести до додаткових дорадчих блокувань на непов'язаних рядках, де status = 'standby'
це неправда. Пов’язане запитання щодо SO:
Як правило, ви можете просто проігнорувати це. Щоб гарантувати, що лише класифіковані рядки заблоковані, ви можете вкласти предикат (-ів) у CTE, як вище, або підзапит з OFFSET 0
хаком (запобігає вбудованому) . Приклад:
Або (дешевше для послідовного сканування) вкладіть такі умови у CASE
виписці, як:
WHERE CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END
ОднакCASE
трюк буде також тримати Postgres використовувати індекс на status
. Якщо такий індекс доступний, для початку вам не потрібні додаткові вставки: у скануванні індексу будуть заблоковані лише кваліфіковані рядки.
Оскільки ви не можете бути впевнені, що індекс використовується в кожному дзвінку, ви можете просто:
WHERE status = 'standby'
AND CASE WHEN status = 'standby' THEN pg_try_advisory_xact_lock(id) END
CASE
Логічно зайвий, але це сервера обговорювана мети.
Якщо команда є частиною довгої транзакції, розгляньте блокування на рівні сеансу, які можна (і повинні бути) звільнені вручну. Таким чином, ви можете розблокувати, як тільки ви закінчите з заблокованим рядком: pg_try_advisory_lock()
іpg_advisory_unlock()
. Посібник:
Після придбання на рівні сеансу дорадчий блокування утримується, поки явно не буде відпущено або сесія закінчиться.
Пов'язані: