Главное требование к финансовому SQL#
Запрос должен сохранять финансовый смысл: валюта, знак, учётный период, уровень детализации и временной срез не могут быть случайными техническими деталями.
До написания запроса желательно зафиксировать в одном предложении его зерно: например, «одна строка на счёт, валюту и учётную дату, с учётом событий, известных на указанную дату».
SQL ниже иллюстрирует учебную схему и в этом редакционном цикле не исполнялся. Он не является подтверждением проверки PostgreSQL или промышленной реализации. Алгоритмы контроля обогащения и допуска сумм приведены только как псевдокод.
Расчёт движений#
В лаборатории есть таблица journal_lines, где на каждое событие приходятся дебетовая и кредитовая строка. Сумма в минимальных единицах хранится как целое число; debit трактуется положительно.
SELECT account_code,
currency,
SUM(CASE WHEN side = 'debit'
THEN amount_minor
ELSE -amount_minor END) AS net_minor
FROM journal_lines
WHERE business_date >= '2026-01-01'
AND business_date < '2026-02-01'
AND observed_at <= '2026-01-31T23:59:59Z'
GROUP BY account_code, currency
ORDER BY account_code, currency;
Такой запрос применим к учебной схеме. Для производственной модели необходимо дополнить его юридическим лицом, книгой учёта, версиями учётных правил и другими обязательными измерениями.
Контроль двойной записи#
Суммирование по событию позволяет найти явный дисбаланс:
SELECT event_id, currency,
SUM(CASE WHEN side = 'debit'
THEN amount_minor
ELSE -amount_minor END) AS balance_diff
FROM journal_lines
GROUP BY event_id, currency
HAVING SUM(CASE WHEN side = 'debit'
THEN amount_minor
ELSE -amount_minor END) <> 0;
Если запрос вернул ноль строк, это означает только отсутствие обнаруженных дисбалансов среди уже присутствующих строк. Проверка не доказывает, что все исходные события загружены, что суммы верны или что документы правильно сопоставлены.
Гранулярность соединений#
Опасная ошибка:
-- Плохо: сумма операции размножается по строкам проводок
SELECT SUM(e.amount_minor)
FROM source_events e
JOIN journal_lines l USING (event_id);
В стенде каждая операция соответствует двум строкам журнала, поэтому её исходная сумма будет посчитана дважды. В реальной системе коэффициент искажения может быть переменным.
Сначала определите, что представляет одна строка в итоговой таблице. Например, для оборота по счёту за месяц зерно результата - счёт, месяц и валюта. Перед соединением проверьте детализацию таблиц: если одна операция связана с несколькими проводками, сумма операции при прямом JOIN повторится для каждой проводки. Сведите данные к нужному зерну и только потом соединяйте.
Контракт обогащения до агрегации#
Рассмотрим добавление атрибутов подразделения к уже допущенным строкам проводок. Выходное зерно должно остаться прежним: одна строка на выбранную версию строки проводки. Это проектный контроль качества, а не требование IFRS или универсальное правило SQL. Распределение суммы между несколькими получателями меняет зерно и требует отдельного контракта распределения.
Входной ключ включает источник, юридическое лицо, регистр, документ, номер строки и выбранную версию. Сумма сохраняет валюту, масштаб и знак: в синтетическом примере используются целые копейки RUB, дебет положительный, кредит отрицательный. Для поиска подразделения закрепляют полный контекст ключа, версию справочника, дату действия business_date и cutoff известности observed_at. Дата действия атрибута и время получения его версии различаются. До JOIN проверяют уникальность входных ключей; иначе нельзя отличить дубликат входа от размножения при обогащении.
Для каждой входной строки считают реальные совпадения по полному предикату, до агрегации и независимо от суммы строки:
| Вид обогащения | Допустимые совпадения | Решение при нарушении |
|---|---|---|
| Обязательное | Ровно одно | Ноль или несколько блокируют допуск результата |
| Необязательное | Ноль или одно | Ноль сохраняет факт с явным признаком отсутствия атрибута; несколько блокируют допуск |
Необязательность означает разрешённое отсутствие атрибута, а не разрешённую потерю факта. Две одинаковые записи справочника остаются двумя совпадениями. DISTINCT, произвольный выбор первой записи и подмена отсутствия нулём скрывают проблему. При допустимых приоритетах сначала формально выбирают кандидата, затем проверяют единственность победителя. Детали классификации счетов остаются в версии соответствий, состояния отклонённых строк - в контракте карантина.
Проверка кандидатов и результата JOIN#
Псевдокод описывает критерии допуска, без реализации загрузчика:
Закрепить входные версии, контекст, времена и версию справочника.
Для каждого входного ключа сохранить число реальных кандидатов и их ID.
Проверить допустимое число кандидатов по обязательности атрибута.
Если есть нарушение, не допускать результат к окончательной агрегации.
Выполнить обогащение, сохранив факт при разрешённом отсутствии атрибута.
Сравнить входной и выходной наборы ключей и кратность каждого ключа.
Проверить неизменность суммы, валюты, масштаба и знака каждой строки.
Сверить контрольные суммы внутри юридического лица, регистра и валюты.
Сохранить результаты контроля с версиями входа и справочника.
В PostgreSQL 18 INNER JOIN выдаёт строку для каждого совпадения; LEFT JOIN добавляет строку с NULL справа при отсутствии совпадений. Поэтому число строк результата LEFT JOIN не равно числу реальных кандидатов: для отсутствующего соответствия оно равно одному. Кандидатов считают по гарантированно ненулевому ID правой строки, а не через COUNT(*) над внешним соединением. Фильтр правого атрибута в WHERE может удалить сохранённые строки с NULL; условие выбора кандидатов и фильтр итогового результата имеют разные задачи.
Ограничения PostgreSQL 18 помогают обеспечить уникальность полного ключа и ссылочную целостность. Но уникальность ID записи справочника не доказывает единственность совпадения по иному предикату, например по ключу и интервалу действия. EXPLAIN оценивает план, а не доказывает бизнес-кардинальность. Эти свойства конкретной СУБД отделены от предложенного критерия допуска; совместимость с другими СУБД здесь не проверяется.
Ошибки, которые скрывает общий итог#
Синтетический вход содержит строки A: +10000, B: -10000, C: 0 копеек RUB. Если A и B получают по два совпадения, общий итог остаётся нулевым, хотя число строк и валовые движения удвоились. Если C исчезает, сумма тоже не меняется. Поэтому равенство нетто-суммы недостаточно: проверяют набор ключей, кратность, сохранность полей, суммы положительных и отрицательных движений отдельно и число нулевых строк.
При цепочке обогащений контроль выполняют после каждого шага: потеря одной строки и дублирование другой могут сохранить даже общее число строк. Предварительная агрегация допустима только при явно выбранном новом зерне и сохранённой связи с исходными строками; она не исправляет неоднозначный справочник. Если нужны только строки, имеющие связь, это отдельный контракт отбора с объяснимым набором исключённых ключей, а не обогащение с сохранением всех фактов.
Неизвестная сумма, ноль и отсутствие фактов#
Сохраняем зерно одной выбранной версии строки проводки с полным ключом. Итог относится к юридическому лицу, регистру, счёту, валюте и периоду business_date при закреплённом cutoff observed_at и версии входа. Суммы в примерах синтетические, в целых копейках RUB; дебет положительный, кредит отрицательный. Масштаб и валюту нельзя определять по наличию значения.
Известный ноль означает измеренную сумму 0. NULL в обязательной мере означает, что сумму нельзя установить из принятого входа; это не нулевая операция. Отсутствие фактов означает, что для выбранной группы нет входных строк. Оно может быть нормальным отсутствием движения или следствием неполной доставки, неверного фильтра либо недоступного периметра. Семантику отсутствия определяют контракт источника и контроль полноты, а не результат SUM. Неприменимую меру также следует обозначать отдельным состоянием, а не смешивать с неизвестной обязательной суммой.
В PostgreSQL 18 SUM учитывает все значения, кроме NULL: числовой ноль входит в сумму, а NULL пропускается. COUNT(*) считает входные строки, COUNT(amount) - строки с известной суммой. SUM возвращает NULL, если нет значений для сложения, включая пустой вход и вход только из NULL. Агрегация без GROUP BY над пустым входом возвращает одну строку результата; обычный GROUP BY по ключам без группирующих наборов не создаёт строк для отсутствующих групп. Это поведение конкретной СУБД, а не доказательство полноты финансового итога; соответствие других СУБД или полного стандарта SQL здесь не проверяется.
Критерии допуска суммы#
Следующая таблица относится к одной ожидаемой группе до добавления искусственных строк внешним соединением. n_rows - число реальных фактов, n_known - число известных сумм; raw_sum сохраняет результат SUM без подстановки. Значения таблицы объясняют документированную семантику, а не результат исполнения кода.
| Входные суммы, копейки RUB | n_rows | n_known | raw_sum | Решение по проектному контракту |
|---|---|---|---|---|
0 |
1 | 1 | 0 | Известный ноль, если доставка полна |
10000, -10000 |
2 | 2 | 0 | Нулевое нетто, но есть движения |
10000, NULL |
2 | 1 | 10000 | Частичная сумма; окончательный итог запрещён |
NULL |
1 | 0 | NULL | Неизвестная обязательная мера; итог запрещён |
| Нет фактов | 0 | 0 | NULL | Проверить полноту и разрешённость пустой группы |
Для обязательной меры критерий n_known = n_rows необходим, но недостаточен: все ожидаемые факты должны быть доставлены, ключи и временной периметр согласованы. При n_rows > 0 неизвестная сумма блокирует окончательный итог группы независимо от нетто и от размера известной части. Частичный предварительный результат можно хранить только с явной неполнотой, числом неизвестных значений и ссылкой на контроль, исключая его из утверждённого выпуска.
При n_rows = 0 равенство счётчиков выполняется автоматически и ничего не доказывает. Ноль для пустой группы допустим только при подтверждённой полной поставке, ожидаемом периметре и явной политике «нет движения означает нулевой оборот». Эта политика не превращает отсутствие снимка остатка в нулевой остаток. Для обязательного снимка отсутствие строки остаётся ошибкой. Допуск результата является проектным инженерным решением, не требованием IFRS.
Границы COALESCE и контроль пустых групп#
COALESCE в PostgreSQL 18 выбирает первый аргумент, отличный от NULL. Подстановка нуля внутри SUM превращает неизвестную сумму в известную; подстановка после SUM смешивает пустой вход и вход только из NULL. Более того, ненулевой частичный итог для 10000, NULL вообще не требует подстановки и всё равно неполон. COALESCE не заменяет контроль счётчиков и доставки.
Ниже только псевдокод, без новой исполняемой SQL/Python-реализации:
Закрепить ожидаемые группы, входные версии, валюту, масштаб и времена.
Подтвердить полноту доставки и сохранение зерна после обогащения.
Для каждой группы реальных фактов сохранить n_rows, n_known и raw_sum.
Сопоставить агрегаты с ожидаемыми группами, сохранив отсутствующие группы.
Если n_known < n_rows, заблокировать окончательный итог группы.
Если фактов нет, применить явную политику пустой группы или блокировать итог.
Для разрешённой пустой группы сохранить ноль вместе с причиной подстановки.
Сохранить состояния, счётчики и контроль с идентификатором выпуска.
Если ожидаемые группы получают LEFT JOIN к фактам, COUNT(*) включает искусственную строку для группы без совпадений. Число фактов тогда считают по гарантированно ненулевому ключу факта либо присоединяют уже рассчитанные агрегаты. Фильтр amount IS NOT NULL до контроля скрывает неизвестные строки. Контроль выполняют по каждой группе и обязательной мере: общий счётчик по витрине не объясняет, какой итог неполон. Здесь не разрабатываются загрузчик, карантин или новый алгоритм JOIN; их границы описаны выше и в связанных материалах.
Контракт относительного показателя#
Задача - допустить финансовую долю только после проверки смысла обеих мер. Здесь рассматривается отношение денежных потоков за период, например доля выбранной категории расходов в общей сумме расходов. Это проектный контракт витрины, а не требование IFRS и не универсальное определение финансового коэффициента. Отношения остатка к потоку, годовая нормализация и нормативные показатели требуют отдельных определений.
Зерно результата - один показатель и версия его определения на юридическое лицо, книгу, отчётный периметр, период и release_id. Числитель N и знаменатель D рассчитывают отдельно на этом зерне из закреплённых входов. Для доли категории её набор фактов должен быть подмножеством общей базы; JOIN к отдельным строкам категории не должен повторять общий знаменатель. Обе меры используют одну валюту, масштаб, версию валютного basis и правил классификации, один интервал business_date и cutoff observed_at. Совпадение кода RUB само по себе не доказывает сопоставимость курсов или выпусков.
В синтетическом примере N и D - целые копейки RUB, масштаб 2. Для этого показателя дебетовые расходы положительны, кредитовые уменьшения расходов отрицательны; произвольная смена знака или ABS запрещена. Результат N / D безразмерен, а процент равен 100 * N / D. Сохраняют N, D, состояние допуска и версию определения, а не только отображаемый процент. Правила денежной точности и валютного basis задаются до вычисления.
Нулевая, неизвестная и отрицательная база#
Для выбранной доли расходов проект допускает D > 0 после подтверждения полноты обеих мер. Другой показатель может разрешать отрицательную базу, но только с явной интерпретацией и отдельной версией определения.
| Состояние входа | Решение для выбранной доли |
|---|---|
| N = 0, D > 0, обе меры полны | Известная нулевая доля |
| D = 0, включая N = 0 | Отношение не определено; не подставлять 0% |
| N или D неизвестны либо неполны | Результат не допущен, даже если известная часть ненулевая |
| D < 0 | Не допускать по этому определению; сохранить причину |
| D > 0, N < 0 или N > D | Арифметически возможно; проверить возвраты, знаки и периметр |
Нулевая база может возникнуть из взаимного погашения положительных и отрицательных движений, а не только из отсутствия расходов. Отрицательная база может отражать преобладание возвратов. Эти причины сохраняют для диагностики; модуль знаменателя изменил бы определение показателя. Диапазон 0..100% гарантирован лишь при неотрицательной базе и её неотрицательном подмножестве. Выход за него нельзя автоматически обрезать. Даже малая положительная база может дать большой процент: порог существенности допустим только как заранее утверждённое правило, без универсального численного порога.
Отношение сумм и критерии допуска#
При укрупнении непересекающихся сопоставимых групп пересчитывают отношение сумм N / D, а не среднее сохранённых долей. Синтетические группы имеют N1 = 1000, D1 = 10000 и N2 = 9000, D2 = 30000 копеек RUB. Доли равны 10% и 30%; общий результат (1000 + 9000) / (10000 + 30000) = 25%, тогда как простое среднее даёт 20%. При положительных D это среднее, взвешенное знаменателями. Перекрывающиеся группы нельзя суммировать; при недопущенной группе нельзя молча удалить её и представить результат как полный. Среднее индивидуальных коэффициентов допустимо для отдельной задачи с собственным определением, но не заменяет долю общей суммы. Для остатков нужны правила агрегирования по времени.
Ниже только псевдокод; новой исполняемой реализации нет:
Закрепить определение показателя, выпуск, зерно, период и валютный basis.
Подтвердить полноту N и D и допустимость наборов фактов для обеих мер.
Сверить сопоставимость измерений и отсутствие повторного учёта базы.
Если мера неизвестна или неполна, сохранить отказ без числовой доли.
Если D = 0, сохранить состояние неопределённого отношения.
Если знак D не разрешён определением, сохранить отказ с причиной.
Иначе рассчитать N / D в согласованной десятичной арифметике.
Сохранить N, D, состояние и версии; форматировать процент отдельно.
На новом зерне повторить контроль и расчёт по суммам, не по долям.
В PostgreSQL 18 деление целых типов отбрасывает дробную часть к нулю: типы операндов следует выбрать до деления. NULLIF может заменить нулевой знаменатель на NULL, но не объясняет причину и не проверяет отрицательную или неполную базу. Это свойства конкретной СУБД, а не контракт допуска. SQL/Python для нового расчёта не исполнялись; поведение других СУБД не подтверждается.
План-факт: версия и сопоставимость#
Задача - сравнить расходы утверждённого бюджета с фактическими расходами месяца. Это предлагаемый контракт управленческой витрины, не требование IFRS и не описание бюджетного движка. Он сравнивает план с фактом; мост изменений объясняет разницу двух фактических выпусков и решает другую задачу.
Зерно сравнения - юридическое лицо, книга, центр затрат, статья расходов, финансовый месяц и валюта в закреплённом периметре. Ключ результата дополнительно содержит scenario_id, budget_version, actual_release_id и версию определения сравнения. Выбирают одну утверждённую версию бюджета, не сумму всех его редакций и не автоматически последнюю доступную запись. Сохраняют дату утверждения и cutoff доступности плана: бюджет, пересмотренный после месяца, отвечает на другой вопрос, чем исходный бюджет. Для факта закрепляют интервал business_date и cutoff observed_at; поздняя проводка меняет новый фактический выпуск, не прежнее сравнение.
Сопоставимость требует одинаковых календаря периода, состава юридических лиц, классификации статей, валютного basis, масштаба и правил знака. Если план составлен по статьям, факт сначала переводят в эти статьи по закреплённым правилам соответствий. План без детализации по центру затрат можно сравнить с фактом только на более общем зерне либо после отдельного утверждённого распределения; нельзя скопировать весь план в каждый центр. Факт в текущих курсах и план в бюджетных курсах дают смешанное отклонение: это разрешено лишь при явном определении и маркировке сравнения. Для сравнения на постоянных курсах обе стороны пересчитывают в согласованный basis и сохраняют исходные версии. Совпадение валютного кода не доказывает сопоставимость.
Отклонение и отсутствующий план#
Для выбранного определения расходы положительны, возвраты отрицательны. A - фактические расходы, P - плановые; обе суммы выражены целыми копейками RUB. Абсолютное отклонение в денежных единицах A - P не является модулем разности: положительное значение означает расходы выше плана, отрицательное - ниже. Это арифметическое направление, а не автоматическая оценка эффективности. Для выручки или другого показателя трактовка требует своего определения.
| Состояние | Допуск сравнения |
|---|---|
| Известные полные A и P, включая явный P = 0 | Рассчитать A - P; нулевой план сохранить как утверждённое значение |
| Нет строки плана для ожидаемого ключа | Сохранить «план отсутствует»; не подменять нулём |
| Строка плана есть, сумма неизвестна | Сохранить отдельное состояние неизвестной суммы, блокировать отклонение |
| План есть, фактов нет | A = 0 только при доказанной полноте поставки и разрешённой политике пустой группы |
| Несопоставимые basis или незавершённая поставка | Не допускать окончательное сравнение |
Отсутствующий план можно считать нулём только по явно утверждённой политике для конкретного периметра с сохранением причины подстановки. Группы без плана и группы без факта сохраняют для контроля покрытия; INNER JOIN скрыл бы их. Процент отклонения требует отдельного определения знаменателя и допуска базы, описанного выше. При укрупнении отклонения суммируют только по непересекающимся сопоставимым группам с допущенными обеими мерами; итог по одним сопоставленным строкам нельзя выдавать за полный.
Месячный бюджет и критерии допуска#
Синтетический бюджет P = 100000 копеек RUB имеет одну строку за месяц. Две проводки расходов дают 40000 и 70000 копеек. Правильный A = 110000, отклонение = 10000 копеек. Прямое присоединение бюджета к двум проводкам и суммирование обеих сторон даёт ложный P = 200000 и отклонение = -90000. Это иллюстрация, не финансовые данные автора.
Факт агрегируют отдельно до месячного зерна плана, а план проверяют на уникальность выбранного ключа и версии. Затем соединяют агрегаты, сохраняя ключи обеих сторон, либо используют заранее определённый полный набор ожидаемых групп. Месячный бюджет не становится дневным от соединения с календарём: дневное сравнение требует отдельного распределения с сохранением месячной суммы. Ниже только псевдокод, без исполняемого SQL/Python:
Закрепить сценарий, утверждённую версию бюджета и выпуск факта.
Проверить сопоставимость периметра, календаря, статей, basis и знаков.
Свести факт к согласованному зерну; проверить уникальность плана на нём.
Сохранить объединение ключей плана и факта и ожидаемые группы.
Для каждой группы сохранить A, P, наличие строк и состояния полноты.
При отсутствии или неизвестности меры применить явную политику допуска.
Для допущенной группы рассчитать A - P, сохранив версии и состояние.
Сверить суммы A и P до и после соединения по периметру и валюте.
Проверить сумму отклонений = сумма A - сумма P на допущенном периметре.
Отдельно показать исключённые группы, не скрывая их из общего покрытия.
PostgreSQL 18 поддерживает FULL OUTER JOIN для сохранения несопоставленных строк обеих сторон. Он не подтверждает полноту бюджета или его утверждение; это предметные проверки. Алгоритм не исполнялся, совместимость СУБД не проверялась. Вопрос для подготовки: почему подтверждённый нулевой бюджет и отсутствующая бюджетная строка должны иметь разные состояния, даже если интерфейс показывает одинаковую пустую ячейку?
Для сравнения внутри незавершённого месяца используйте накопительный план-факт: он определяет начало финансового года, промежуточный cutoff и допустимую часть месячного плана.
Денежная точность#
В PostgreSQL numeric пригоден для точных десятичных вычислений; real и double precision являются неточными двоичными типами. В данной лаборатории использованы целые минимальные единицы валюты, поэтому операции суммирования не требуют float.
Для процентов, курсов и распределений нужны отдельные правила округления, масштаба, точности и обработки остаточной погрешности.
Проверка плана#
Для нагруженных финансовых запросов полезно изучать EXPLAIN и, на тестовом окружении, EXPLAIN ANALYZE, оценивать планы соединений, сортировки, агрегации, партиционирование и статистику. ANALYZE действительно исполняет запрос; для команд изменения данных нужна осторожность и транзакционные ограничения.
Связанные темы и вопросы#
Начните с зерна финансовой модели и сверки финансовых данных. Здесь проверяется сохранение уже допущенных фактов при преобразовании; полнота доставки источником требует отдельной сверки.
- Почему одна выходная строка после LEFT JOIN не доказывает наличие кандидата?
- Какие проверки обнаружат удвоение взаимно погашающихся проводок?
- Почему среднее долей отличается от отношения сумм и какие состояния базы запрещают окончательный процент?
Для отношения расходов к натуральной нагрузке требуется отдельный контракт знаменателя, его единицы и покрытия. См. удельные расходы на единицу операционной нагрузки.