UPSERT для счётчика: конфликт должен иметь явный ключ

2 мин чтения

Увеличиваем счётчик через INSERT ON CONFLICT и отличаем конкурентный запрос от повторной доставки события.

Счётчик и защита от повторов — разные задачи
  1. Уникальный ключОпределяет строку при конфликте
  2. ON CONFLICTАтомарно увеличивает нужный счётчик
  3. Повтор событияДля дедупликации нужен отдельный ключ события

Два обработчика одновременно проверили счётчик: строки пока нет. Оба пробуют её создать — один получает конфликт. INSERT ON CONFLICT позволяет описать вставку и увеличение в одной команде PostgreSQL. Сначала зададим уникальный ключ, затем посмотрим, что такой UPSERT решает, а чего не решает.

Сначала определяем конфликт

CREATE TABLE demo_views (
    page_id bigint PRIMARY KEY,
    hits bigint NOT NULL DEFAULT 0
);
INSERT INTO demo_views AS current (page_id, hits)
VALUES (42, 1)
ON CONFLICT (page_id)
DO UPDATE SET hits = current.hits + EXCLUDED.hits
RETURNING page_id, hits;

Первичный ключ задаёт единственность строки страницы. При отсутствии записи вставится единица; при конфликте обновится уже существующий счётчик. Псевдотаблица EXCLUDED содержит предложенные значения вставки. Ссылка current обозначает хранимую строку, поэтому выражение использует её актуальное значение, а не число, прочитанное приложением несколько запросов назад.

Несколько увеличений счётчика

Повторите только INSERT ещё два раза и затем прочитайте строку страницы. При чистом начале ожидается значение три и одна запись. Для конкурентного опыта запустите одинаковую команду из двух соединений и сравните итог с числом успешно завершённых операций. Не делайте вывод о пропускной способности по нескольким ручным вызовам: такой опыт проверяет логику конфликта, а не производительность сервера.

SELECT page_id, hits FROM demo_views WHERE page_id = 42;
SELECT count(*) FROM demo_views WHERE page_id = 42;

Повтор события отличается от повтора запроса

Если очередь повторно доставляет одно событие, атомарный счётчик всё равно увеличится снова. Для защиты от этого нужен отдельный уникальный идентификатор обработанного события и транзакция, связывающая регистрацию события с изменением счётчика. Само слово UPSERT не обещает идемпотентность любой операции. Перед внедрением запишите, что именно считает метрика: принятые запросы, уникальные события или посетителей. Разные определения требуют разных ключей и проверок.

ON CONFLICT обрабатывает выбранный уникальный ключ; нарушение другого ограничения всё равно даст ошибку. При массовой вставке уберите повторы ключей внутри набора, чтобы команда не обновляла одну строку несколько раз. Проверяйте ответ целиком, прежде чем сообщать клиенту об успешном увеличении счётчика.

Источники и дальнейшее чтение