В PostgreSQL оператор UPDATE начал выполняться при READ COMMITTED, но целевая строка в этот момент заблокир...

В PostgreSQL оператор UPDATE начал выполняться при READ COMMITTED, но целевая строка в этот момент заблокирована другой транзакцией. Как изменится результат после снятия блокировки и почему?

CREATE TABLE products (
    id integer PRIMARY KEY,
    price integer NOT NULL
);
INSERT INTO products VALUES (1, 100);

-- Сессия 1
BEGIN;
UPDATE products SET price = 120 WHERE id = 1;

-- Сессия 2
BEGIN;
UPDATE products SET price = price + 10
WHERE id = 1 AND price = 100;
Проходите собеседования с ИИ помощником Hintsage

Краткий ответ

В PostgreSQL при уровне READ COMMITTED второй UPDATE после ожидания блокировки повторно проверяет условие WHERE уже на актуальной версии строки. После фиксации первой транзакции цена станет 120, условие price = 100 перестанет выполняться, поэтому второй оператор изменит 0 строк, а не применит обновление к устаревшему состоянию.

Исторический контекст

Многоверсионность и уровень READ COMMITTED были введены как компромисс между согласованностью чтения и конкурентностью. Читающие операции не должны постоянно блокировать записывающие, но запись не может молча затирать изменения, которые появились во время ожидания.

Поэтому PostgreSQL использует снимок для начала SQL-оператора, а при конфликте записи дополнительно учитывает новую версию строки после получения блокировки. Это позволяет не обновлять строку на основании уже неактуального значения.

Постановка проблемы

Вторая транзакция видела строку с price = 100, но не могла немедленно изменить её: первая транзакция удерживала блокировку. Если бы PostgreSQL без проверки применил price = price + 10 к новой версии, результат зависел бы от устаревшего решения второй транзакции.

Такое поведение особенно важно для условных обновлений: WHERE может выражать бизнес-предусловие, например «увеличить цену только если она всё ещё равна 100». Игнорирование повторной проверки нарушило бы смысл этого предусловия.

Подробное решение

Сессия 1 изменяет цену на 120, но до COMMIT удерживает блокировку строки. Сессия 2 находит строку в своём снимке, пытается обновить её и ждёт завершения первой транзакции.

После COMMIT сессии 1 PostgreSQL обнаруживает, что целевая строка имеет новую версию. В режиме READ COMMITTED оператор продолжает работу с этой актуальной версией и повторно оценивает его условие WHERE.

Поскольку актуальная цена равна 120, предикат price = 100 ложен. Сессия 2 завершает оператор без изменения строки; после её COMMIT цена остаётся 120.

Это поведение относится к PostgreSQL и не следует автоматически переносить на любую СУБД или любой уровень изоляции. Если второму оператору нужно гарантированно выполнить действие на основании актуального состояния, приложение должно проверить число изменённых строк и решить, повторять операцию, сообщать о конфликте или выполнять другую бизнес-логику.

При вычислениях вида SET price = price + 10 PostgreSQL использует актуальную версию строки после ожидания. Но это не превращает произвольную последовательность чтения и записи в атомарную бизнес-операцию: отдельный предварительный SELECT и последующий UPDATE могут потребовать явной блокировки или более строгой схемы конкуренции.

-- Сессия 1 COMMIT; -- Сессия 2 продолжает после ожидания -- условие price = 100 проверяется заново -- результат: UPDATE 0 COMMIT;

Ситуация из практики

Сервис применяет скидку только к товару с исходной ценой 100. Пока один оператор меняет цену, другой выполняет условный UPDATE. Вариант с предварительным SELECT без блокировки ненадёжен: прочитанное значение может устареть до записи.

Можно использовать SELECT ... FOR UPDATE, но это увеличивает время удержания блокировки и усложняет код. Можно сразу выполнить условный UPDATE и проверить количество изменённых строк: такой вариант короче и позволяет явно обработать конфликт.

В данном случае выбран второй вариант. Если результат равен нулю, сервис перечитывает товар и сообщает, что скидка не применена из-за изменения цены. В результате устаревшее предусловие не приводит к неверному обновлению, а блокировка удерживается только во время самого оператора.

Что кандидаты часто упускают

1. Дополнительный вопрос: изменится ли поведение, если первая транзакция откатится?

Да. После отката блокировка будет снята, но подтверждённого изменения строки не останется. Вторая транзакция сможет применить UPDATE к исходной версии с price = 100, поэтому оператор, вероятно, изменит одну строку.

2. Дополнительный вопрос: почему предварительный SELECT не заменяет проверку количества изменённых строк?

Обычный SELECT и последующий UPDATE — разные операции. Между ними другая транзакция может изменить или удалить строку, поэтому результат чтения уже не гарантирует выполнение условия при записи.

Проверка количества изменённых строк относится непосредственно к условному UPDATE и показывает, было ли предусловие истинно в момент фактической записи. Если требуется удержать строку между чтением и изменением, применяется SELECT ... FOR UPDATE либо единый условный оператор.

3. Дополнительный вопрос: гарантирует ли READ COMMITTED, что весь многооператорный сценарий увидит одну картину данных?

Нет. В PostgreSQL снимок обычно создаётся для каждого отдельного оператора, поэтому последующий оператор той же транзакции может увидеть изменения, зафиксированные после начала предыдущего оператора.

Повторная проверка строки после ожидания блокировки защищает конкретный UPDATE, но не делает весь сценарий сериализуемым. Для единой картины данных нужен подходящий уровень изоляции, явные блокировки или повтор всей транзакции при конфликте сериализации.