Без одночасного доступу до запису
Матеріалізуйте виділення в 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() . Посібник:
Після придбання на рівні сеансу дорадчий блокування утримується, поки явно не буде відпущено або сесія закінчиться.
Пов'язані: