Накопительный баланс в SQL: что делать с задними корректировками
Счёт клиента показывал 800 рублей после списания 5 марта. Позже бухгалтер добавил корректировку на −100 рублей с датой операции 3 марта. Если приложение хранит только «текущий остаток после каждой строки» и обновляет новый ряд, старый остаток за 5 марта останется неверным. Надёжнее считать остаток из журнала изменений, когда отчёту нужен баланс на любую дату.
В журнале каждая запись несёт дельту: пополнение положительное, списание отрицательное. Баланс после операции — сумма дельт этого счёта от начала истории до выбранной строки. Оконная функция делает именно это без самосоединения каждой строки со всеми предыдущими.
Дата операции важнее порядка вставки
Корректировка с entry_id=3 была добавлена после списания с entry_id=2, но её posted_at раньше. Для отчёта по дате операции она должна оказаться между пополнением и списанием. Если отсортировать только по entry_id, итоговая сумма за все даты совпадёт, а промежуточные остатки будут неверны.
CREATE TABLE ledger (entry_id INTEGER, account_id INTEGER, posted_at TEXT, delta INTEGER);
INSERT INTO ledger VALUES
(1, 7, '2025-03-01', 1000),
(2, 7, '2025-03-05', -200),
(3, 7, '2025-03-03', -100),
(4, 8, '2025-03-02', 500);
SELECT entry_id, account_id, posted_at, delta,
SUM(delta) OVER (
PARTITION BY account_id
ORDER BY posted_at, entry_id
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
) AS balance_after
FROM ledger
ORDER BY account_id, posted_at, entry_id;
Для счёта 7 строки в порядке дат дадут остатки 1000, 900 и 700 рублей. Для счёта 8 отдельная сумма равна 500: PARTITION BY account_id не позволяет движениям одного клиента попасть в баланс другого. В этой схеме entry_id разрешает ничью, если у двух движений одинаковый posted_at.
На рабочей базе полезно хранить обе отметки: бизнес-дату операции и техническое время загрузки. Тогда можно отвечать на два вопроса. «Каков исправленный баланс на 5 марта?» — пересчитать по posted_at с учётом корректировки. «Какой баланс был виден в системе вечером 5 марта?» — исключить записи, загруженные позже того вечера. Без второго времени восстановить прежний вид отчёта невозможно.
Когда дельта не равна свободным деньгам
Оконная сумма описывает арифметику проводок, но смысл остатка задаёт журнал. Если есть холды, резервы или отмены, их нужно представить отдельными типами движения и определить, входят ли они в доступный баланс. Запрос не исправит журнал, в котором отмену записали как новую покупку или провели дважды. Сначала проверьте уникальность бизнес-операции и правила знака, затем считайте окно.
Для денежного отчёта храните сумму целым числом в копейках либо точным десятичным типом СУБД, а не двоичным float. В примере целые числа просто делают ход вычисления видимым. Перед вводом отчёта в работу проверьте нулевой баланс, две операции в один момент, заднюю корректировку и отдельный счёт.
Если часть истории хранится в архиве, полная сумма от самого начала может быть недоступна. Тогда нужен подтверждённый начальный остаток на дату отсечения и журнал движений после неё. Формула становится «начальный остаток + накопленная сумма дельт». Важно, чтобы начальный остаток и первая дельта не включали одну и ту же операцию дважды. При смене даты архива этот инвариант проверяют отдельной сверкой.
Корректировка задним числом меняет все более поздние промежуточные остатки, даже если итог на сегодня изменился всего на одну сумму. Поэтому материализованная таблица балансов требует пересчёта с даты корректировки, а не только обновления последней строки. Оконный запрос поверх журнала естественно даёт исправленную историю; для больших объёмов можно сохранять контрольные точки, но правила пересчёта останутся теми же.
Отрицательный остаток в результате не обязательно ошибка SQL. Он может показывать овердрафт, долг или пропущенное пополнение. Прежде чем заменять его нулём в отчёте, сравните последовательность проводок и договоритесь с владельцем процесса, допускается ли такой статус счёта. Иначе техническая «правка» скроет событие, которое требовало расследования.
На курсе «Продвинутый SQL» оконные рамки соединяются с финансовыми сверками и проверкой качества данных. Студент пишет запросы в браузере, проходит кейсы на неполных наборах и собирает аналитическую витрину. Откройте программу, чтобы отработать баланс как часть более широкого отчёта.
Для сверки уже готовых итогов по источникам пригодится разбор сравнения двух отчётов по ключам.
Продолжить на практике
- Продвинутый SQL: сложные запросы и задачи для собеседований ↗
Продолжение базового SQL: сложные JOIN, оконные рамки, временные ряды, retention, воронки и рекурсивные CTE. Разбирайте примеры и пишите запросы с автоматической проверкой — от отдельных расчётов до витрины интернет-магазина.
