Обновление Postgres с помощью ORDER BY, как это сделать?
Мне нужно сделать обновление Postgres для коллекции записей, и я пытаюсь предотвратить тупик, который появился в стресс-тестах.
типичным разрешением для этого является обновление записей в определенном порядке, например, по ID-но, похоже, Postgres не разрешает порядок для обновления.
предполагая, что мне нужно сделать обновление, например:
UPDATE BALANCES WHERE ID IN (SELECT ID FROM some_function() ORDER BY ID);
приводит к блокировкам при одновременном выполнении 200 запросов. Что делать?
Я ищете общее решение, а не конкретные обходные пути, как в обновление с заказом по
Это чувствует что там должно быть лучшее решение, чем писать функцию курсора. Кроме того, если нет лучшего способа, как будет выглядеть функция курсора оптимально? Записи обновления
2 ответов
насколько я знаю, нет никакого способа сделать это напрямую через UPDATE
оператор; единственный способ гарантировать порядок блокировки-явно получить блокировки с SELECT ... ORDER BY ID FOR UPDATE
, например:
UPDATE Balances
SET Balance = 0
WHERE ID IN (
SELECT ID FROM Balances
WHERE ID IN (SELECT ID FROM some_function())
ORDER BY ID
FOR UPDATE
)
это имеет обратную сторону повторения ID
поиск индекса на Balances
таблица. В вашем простом примере вы можете избежать этих накладных расходов, получив физический адрес строки (представленный ctid
колонки системы) во время запроса блокировки и использования что водить UPDATE
:
UPDATE Balances
SET Balance = 0
WHERE ctid = ANY(ARRAY(
SELECT ctid FROM Balances
WHERE ID IN (SELECT ID FROM some_function())
ORDER BY ID
FOR UPDATE
))
(будьте осторожны при использовании ctid
s, так как значения являются переходными. Здесь мы в безопасности, так как замки блокируют любые изменения.)
к сожалению, планировщик будет использовать только ctid
в узком наборе случаев (вы можете сказать, работает ли он, ища узел "tid Scan" в EXPLAIN
выход). Для обработки более сложных запросов в пределах одного UPDATE
заявление, например, если ваш новый баланс возвращается some_function()
наряду с ID, вам нужно будет вернуться к поиску на основе ID:
UPDATE Balances
SET Balance = Locks.NewBalance
FROM (
SELECT Balances.ID, some_function.NewBalance
FROM Balances
JOIN some_function() ON some_function.ID = Balances.ID
ORDER BY Balances.ID
FOR UPDATE
) Locks
WHERE Balances.ID = Locks.ID
если накладные расходы производительности являются проблемой, вам нужно будет прибегнуть к использованию курсора, который будет выглядеть примерно так:
DO $$
DECLARE
c CURSOR FOR
SELECT Balances.ID, some_function.NewBalance
FROM Balances
JOIN some_function() ON some_function.ID = Balances.ID
ORDER BY Balances.ID
FOR UPDATE;
BEGIN
FOR row IN c LOOP
UPDATE Balances
SET Balance = row.NewBalance
WHERE CURRENT OF c;
END LOOP;
END
$$
в общем, параллелизм сложно. Особенно с 200 операторами (я предполагаю, что вы не только query = SELECT) или даже транзакциями (на самом деле каждая выпущенная инструкция завернута в транзакцию, если она еще не в транзакции).
общие концепции решения (комбинация) следующие:
чтобы знать, что блокировки могут произойти, поймать их в приложении, проверьте Коды Ошибок на
class 40
или40P01
и повторить транзакцию.резервные замки. Использовать
SELECT ... FOR UPDATE
. Избегайте явных блокировок как можно дольше. Блокировки заставят другие транзакции ждать освобождения блокировки, что наносит ущерб параллелизму, но может предотвратить транзакции в тупиках. Проверьте пример блокировки в главе 13. Особенно тот, в котором транзакция A ждет B и B ждет A(банковский счет).выбрать другой Уровень Изоляции, например слабого типа
READ COMMITED
, если это возможно. Будьте в курсеLOST UPDATE
s вREAD COMMITED
режим. Предотвратить их сREPEATABLE READ
.
напишите ваши заявления с блокировками в том же порядке в каждой транзакции, например, по имени таблицы в алфавитном порядке.
LOCK / USE A -- Transaction 1
LOCK / USE B -- Transaction 1
LOCK / USE C -- Transaction 1
-- D not used -- Transaction 1
-- A not used -- Transaction 2
LOCK / USE B -- Transaction 2
-- C not used -- Transaction 2
LOCK / USE D -- Transaction 2
С общим порядком блокировки A B C D
. Таким образом, транзакции могут чередоваться в любом относительном порядке и по-прежнему иметь хороший шанс не блокировать (в зависимости от ваших операторов у вас могут быть другие проблемы сериализации). Операторы транзакций будут выполняться в указанном ими порядке, но может быть, что транзакция 1 запускает свои первые 2, затем xact 2 запускает первый, затем 1 завершает и, наконец, Xact 2 Завершает.
кроме того, вы должны понимать, что оператор, включающий несколько строк, не выполняется атомарно в параллельной ситуации. Другими словами, если у вас есть два оператора A и B, включающие несколько строки, то они могут быть выполнены в следующем порядке:
a1 b1 a2 a3 a4 b2 b3
но не как блок a, за которым следуют b.
То же самое относится к оператору с подзапросом.
Вы посмотрели планы запросов с помощью EXPLAIN
?
в вашем случае, вы можете попробовать
UPDATE BALANCES WHERE ID IN (
SELECT ID FROM some_function() FOR UPDATE -- LOCK using FOR UPDATE
-- other transactions will WAIT / BLOCK temporarily on conc. write access
);
если возможно, то, что вы хотите сделать, вы можете также использовать выбрать ... ДЛЯ ОБНОВЛЕНИЯ SKIP LOCK, который пропустит уже заблокированные данные, чтобы вернуть параллелизм, который теряется в ожидании еще одна транзакция для освобождения блокировки (для обновления). Но это не будет применять обновление к заблокированным строкам, которое может потребоваться логике приложения. Поэтому запустите это позже (см. пункт 1).
Читайте также ПОТЕРЯННОЕ ОБНОВЛЕНИЕ о LOST UPDATE
и
ПРОПУСТИТЬ ЗАБЛОКИРОВАН о SKIP LOCKED
. Очередь может быть идеей в вашем случае, что прекрасно объясняется в SKIP LOCKED
ссылка, хотя реляционные СУБД не предназначены для очередей.
HTH