Сервис развивается: тестируем формат, собираем идеи, улучшаем сервис. Есть идеи? Написать
Войти
Дайджесты новостей
Иллюстрация к анализу типов данных numeric и bigint для финансовых вычислений в базах данных PostgreSQL

Хранение денежных значений в PostgreSQL: почему numeric незаменим и когда bigint спасает производительность

Выбор типа данных для денежных расчетов в реляционных СУБД определяет стабильность бизнеса. Произвольная точность numeric в PostgreSQL требует программной эмуляции арифметики в обход регистров CPU. Базовые правила округления, структура хранения и критерии перехода на копейки в bigint для нагруженных систем.

Хранение денежных значений в PostgreSQL: почему numeric незаменим и когда bigint спасает производительность

Выбор типа данных для финансовых транзакций в реляционной базе данных — это фундаментальное архитектурное решение. Ошибка на этапе проектирования оборачивается расхождением балансов, блокировками транзакций и падением скорости аналитических запросов. База данных PostgreSQL предлагает два основных инструмента для работы с денежными величинами: произвольно точный тип numeric и фиксированный 64-битный bigint. За каждым из них стоят принципиально разные компромиссы между математической строгостью и аппаратной скоростью процессора.

Двоичный обман: почему double precision разрушает баланс

Главная ошибка при проектировании финансовых таблиц — использование типов real или double precision. Эти типы данных соответствуют стандарту IEEE 754, где числа хранятся в двоичной экспоненциальной форме. В оборудовании двоичная дробь формируется суммой степеней двойки (1/2, 1/4, 1/8 и далее). Подобно тому как дробь 1/3 невозможно точно записать в виде конечной десятичной дроби, число 0.1 превращается в двоичной системе в бесконечную периодическую дробь.

На уровне единичной операции погрешность кажется микроскопической: сложение 0.1 и 0.2 дает результат 0.30000000000000004. Интерфейс часто скрывает проблему округлением при выводе. Однако в учетном реестре (ledger) тысячи транзакций суммируются, перемножаются на процентные ставки и коэффициенты конвертации. Погрешность накапливается, порождая эффект "исчезающих копеек". В итоге регулярная сверка дебета и кредита выдает ошибку, а закрытие расчетного периода сбоит из-за несовпадения сумм. Финансовая логика требует строгого совпадения каждого десятичного знака, что исключает приблизительные типы из учетных систем.

Анатомия numeric: программная арифметика по основанию 10000

Для гарантированной точности в PostgreSQL реализован тип numeric (синоним — decimal). В документации СУБД он классифицируется как тип с произвольной точностью (arbitrary precision): он вмещает до 131 072 цифр до запятой и до 16 383 знаков после нее. Значение 12.30, сохраненное в numeric, остается точным без каких-либо искажений.

Однако за точность приходится платить ресурсами. Процессор аппаратно складывает и умножает числа только в регистрах фиксированного размера (например, 64-битных). Тип numeric аппаратно не поддерживается. Исходный код PostgreSQL организует хранение числа как динамическую структуру переменной длины:

  • Заголовок с числом цифровых блоков, положением десятичной точки, знака и масштаба (scale).
  • Массив двухбайтовых целых чисел, представляющих значение по основанию 10 000 (base-10000), где каждая ячейка хранит четыре десятичные цифры (от 0000 до 9999).

Когда запрос вычисляет сумму или произведение колонок numeric, ядро СУБД выполняет программную эмуляцию длинной арифметики «столбиком», пошагово перебирая блоки в памяти и учитывая переносы разрядов. Процессор тратит десятки машинных тактов там, где для целого числа хватило бы одной инструкции ADD. Кроме того, заголовок numeric увеличивает расход памяти, создавая дополнительную нагрузку на диск и кэш shared_buffers.

Целочисленные копейки: архитектура и границы bigint

Альтернативный подход в платежных шлюзах — отказ от дробей в пользу типа bigint. Идея заключается в выборе минимальной неделимой единицы (minor unit): вместо рублей в базу записываются копейки, вместо долларов — центы. Сумма 12 рублей 30 копеек сохраняется в базу как целое число 1230.

Преимущества bigint:

  1. Фиксированный размер: колонка занимает ровно 8 байт, оптимизируя упаковку строк на страницах базы данных и ускоряя сканирование индексов.
  2. Аппаратная скорость: процессор выполняет операции над 64-битными целыми числами на предельной частоте регистров без накладных расходов СУБД.
  3. Огромный диапазон: 64-битное целое со знаком охватывает значения от −9.22 × 10¹⁸ до +9.22 × 10¹⁸. При хранении в копейках максимальная сумма превышает 92 квадриллиона рублей.

Однако переход на bigint переносит сложность в прикладной код. Число 1230 само по себе не несет информации о масштабе: это могут быть 12 рублей 30 копеек, 1230 рублей или 0.01230 токена. Если один сервис запишет сумму в рублях, а другой прочитает в копейках, произойдет сбой списания. Кроме того, не все валюты делятся на 100: японская иена не имеет копеек, а бахрейнский динар делится на 1000 филсов. Без проверок CHECK и слоя бизнес-валидации целочисленный подход провоцирует системные ошибки при интеграциях.

Ловушка дробных расчетов: налоги, проценты и курсы валют

Даже если балансы счетов хранятся в копейках, расчеты неизбежно требуют дробных величин: начисление процентов, расчет НДС (20%), распределение скидок и валютная конвертация генерируют дробные результаты.

Попытка выполнять эти операции исключительно в целых числах ведет к потерям. Если товар стоимостью 19 рублей 99 копеек продается со скидкой 3.5%, расчетная цена составляет 19.29035 рубля. Если округлить копейки сразу в момент деления, а затем умножить на партию из 10 000 штук, погрешность выльется в реальные финансовые убытки.

В финансовых системах действует правило: промежуточные вычисления производятся с повышенной точностью, а округление до минимальной единицы выполняется один раз на границе проводки. Для расчетов подходит numeric с расширенным масштабом, например numeric(20, 6).

Также необходимо зафиксировать единый алгоритм округления:

  • Банковское округление к ближайшему четному числу, принятое по умолчанию в бухучете для устранения статистического смещения.
  • Арифметическое округление в пользу продавца или покупателя, регламентированное налоговым законодательством.
  • Метод усечения (truncate) без округления копеек.

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

Практический сценарий: гибридная модель биллинга

Оптимальным решением становится разделение схемы базы данных на учетный и расчетный контуры:

  1. Транзакционный учет (Transaction Ledger): Таблицы проводок и балансов работают в условиях частых записей. Здесь оправдано использование bigint с фиксированной единицей учета (копейка или цент). Колонки сопровождаются кодом валюты по ISO 4217 (например, RUB, USD) и ограничениями CHECK (amount >= 0). Это обеспечивает плотность индексов и скорость вычисления сумм через SUM().
  2. Расчетный контур тарифов и скидок: Таблицы прейскурантов, процентных ставок, налогов и курсов валют проектируются на типе numeric(precision, scale). Например, курс валют задается как numeric(18, 6), а скидка — numeric(5, 4). В этом слое выполняются все калькуляции стоимости корзины и комиссий.
  3. Шлюз фиксации проводок: Преобразование из расчетного слоя в учетный регистр оформляется единой функцией. Она выполняет финальное округление дробного numeric по регламенту и создает атомарную запись в bigint.

Чек-лист проектирования и валидации финансовых схем

Перед утверждением схемы базы данных инженерам стоит свериться со следующими правилами:

  • Исключить типы real и double precision из колонок денежных балансов и расчетов.
  • При выборе numeric явно задавать параметры precision и scale, например numeric(14, 2) для рублей.
  • При выборе bigint указывать единицу измерения в названии колонки (например, amount_cents или balance_kopecks) и хранить код валюты рядом.
  • Контролировать сериализацию в API: в JavaScript тип Number представляет собой IEEE 754 float, искажающий целые числа больше 2⁵³ − 1 и десятичные дроби. Денежные значения должны передаваться в виде строк (string) либо обрабатываться специализированными библиотеками.
  • Разработать план миграции: если исторические данные хранились в double precision, операция ALTER TABLE ... TYPE numeric не восстановит потерянную точность; потребуется сверка с первичными выписками.
  • Протестировать граничные условия: нулевые и отрицательные суммы, лимиты переполнения, взаимные возвраты, конвертацию валют и распределение некратного остатка при делении платежа.

Осознанный выбор между точностью numeric и скоростью bigint позволяет заложить надежный фундамент биллинга, способный выдерживать рост нагрузки без риска финансовых расхождений.