Обновление 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
))

(будьте осторожны при использовании ctids, так как значения являются переходными. Здесь мы в безопасности, так как замки блокируют любые изменения.)

к сожалению, планировщик будет использовать только 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) или даже транзакциями (на самом деле каждая выпущенная инструкция завернута в транзакцию, если она еще не в транзакции).

общие концепции решения (комбинация) следующие:

  1. чтобы знать, что блокировки могут произойти, поймать их в приложении, проверьте Коды Ошибок на class 40 или 40P01 и повторить транзакцию.

  2. резервные замки. Использовать SELECT ... FOR UPDATE. Избегайте явных блокировок как можно дольше. Блокировки заставят другие транзакции ждать освобождения блокировки, что наносит ущерб параллелизму, но может предотвратить транзакции в тупиках. Проверьте пример блокировки в главе 13. Особенно тот, в котором транзакция A ждет B и B ждет A(банковский счет).

  3. выбрать другой Уровень Изоляции, например слабого типа READ COMMITED, если это возможно. Будьте в курсе LOST UPDATEs в 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