Деньги в PostgreSQL: как хранить суммы без ошибок
PostgreSQL умеет хранить денежные значения точно, но тип столбца сам по себе не знает валюту и правила округления. Разбираем, когда выбрать NUMERIC, когда — целые минимальные единицы и почему тип MONEY редко подходит для расчётов.

Ошибка в одном типе данных может испортить расчёт
Если хранить цену в PostgreSQL как real или double precision, десятичная сумма может превратиться в ближайшее двоичное приближение. Для денег это не безобидная погрешность: при сложении заказов и расчёте скидок расхождение может проявиться в итогах, сверках и возвратах. В PostgreSQL 16 практический выбор обычно сводится к двум вариантам — точный numeric или целое число минимальных денежных единиц.
Универсального столбца «деньги» недостаточно. Сумма без валюты неоднозначна, а тип данных не решает за приложение, когда округлять налог или как хранить дробную часть валюты. Ниже — варианты для разных задач и проверки, которые стоит добавить до запуска.
Сначала определите правила суммы
До выбора типа ответьте на три вопроса: какие валюты хранит система, сколько знаков после запятой допускают операции и на каком этапе по бизнес-правилам округляется результат. Цена в каталоге, промежуточный результат расчёта комиссии и списание с платёжного инструмента могут иметь разные требования к точности.
Не сводите эти решения к одному правилу «везде два знака». У валют могут быть разные правила дробной части, а умножение на курс или процент создаёт дополнительные знаки. Определите отдельно точность хранения и точность, которую показываете или списываете.
Два рабочих способа хранения
NUMERIC — когда важны точные десятичные значения
NUMERIC хранит десятичные числа точно, поэтому подходит для сумм, курсов и промежуточных вычислений. В таблице храните валюту отдельным столбцом: PostgreSQL не выводит её из числа и не проверяет, что сумма в рублях не попала в операцию в евро.
Пример для PostgreSQL 16: схема допускает до шести знаков после запятой и отклоняет значения, которые этому правилу не соответствуют. Проверка верхней границы здесь намеренно не задана — выберите её исходя из допустимых сумм вашего сервиса.
CREATE TABLE payments (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
amount numeric NOT NULL,
currency text NOT NULL
CHECK (currency ~ '^[A-Z]{3}$'),
created_at timestamptz NOT NULL DEFAULT now(),
CHECK (amount > 0),
CHECK (amount = round(amount, 6))
);Проверка кода валюты в примере подтверждает только формат из трёх заглавных букв, а не то, что код существует или разрешён продуктом. Список допустимых валют задайте отдельно — например, через справочник и внешний ключ. Условие с round не даёт незаметно принять дробную часть длиннее шести знаков: значение с лишними ненулевыми знаками будет отклонено.
Если вместо такого правила объявить столбец как numeric(20, 6), PostgreSQL приведёт входное значение к заданному масштабу. В частности, лишние дробные знаки могут округлиться при приведении типа, а не вызвать ошибку. Это допустимо, только если такое округление ожидаемо и согласовано с правилами расчёта.
BIGINT — когда у валюты фиксированы минимальные единицы
Для простого платёжного реестра можно хранить целое количество минимальных единиц: например, центов или копеек. Так приложение избегает двоичных дробей, а сумма в конкретной валюте представлена целым числом. Метод подходит, если система чётко знает, сколько таких единиц соответствует одной основной денежной единице.
Например, столбец amount_minor bigint NOT NULL хранит сумму в минимальных единицах, а рядом лежит код валюты. Зафиксируйте правило пересчёта в документации и проверяйте его при вводе и выводе данных. Для валют с другой точностью нельзя механически делить на 100.
У BIGINT ограниченный диапазон. До выбора проверьте максимальную сумму одной операции и возможный итог агрегирования: сумма многих допустимых строк может выйти за пределы целого типа. Если нужны курсы, дробные комиссии или точные промежуточные вычисления, одного целого столбца обычно недостаточно.
Почему не MONEY
В PostgreSQL есть тип money, но он не заменяет явную модель суммы и валюты. Его вывод зависит от локали сервера, а столбец сам по себе не хранит код валюты. Поэтому приложение может получить трудности при переносе данных между окружениями, работе с несколькими валютами и обмене значениями с внешними системами. Для новой схемы обычно прозрачнее numeric или целое число минимальных единиц плюс код валюты.
| Подход | Подходит, когда | Цена выбора |
|---|---|---|
numeric и отдельная валюта | Есть курсы, комиссии или разная точность расчётов | Нужно явно определить масштаб, округление и диапазон |
bigint в минимальных единицах и валюта | Платёжные суммы целые в минимальных единицах и правила фиксированы | Нельзя забывать о масштабе валюты и переполнении при суммировании |
money | Только ограниченный сценарий с учётом локали и особенностей типа | Не хранит отдельный код валюты и хуже подходит для переносимого обмена данными |
Практические проверки до записи
- Храните валюту рядом с суммой. Используйте отдельное поле и ограничьте набор допустимых кодов справочником. Проверка формата из трёх букв не удостоверяет, что валюта поддерживается вашим приложением. Проверка: строка с неизвестным для продукта кодом не должна пройти запись.
- Не используйте двоичные дробные типы для денег. Проверьте типы не только в основной таблице, но и в промежуточных вычислениях приложения. Проверка: сумма нескольких записей в базе и рассчитанный ожидаемый итог совпадают по установленному правилу округления.
- Округляйте в определённой точке. Не округляйте каждый промежуточный результат «для красоты», если бизнес-правило требует округления только при списании или выставлении счёта. Проверка: тесты покрывают положительные и отрицательные значения и граничные случаи округления.
- Проверяйте арифметику на больших значениях. Убедитесь, что умножение суммы на количество, суммирование строк и расчёт комиссии не превышают выбранный диапазон. Проверка: запросы для максимальных ожидаемых данных выполняются без переполнения и дают результат нужной точности.
Тип данных отвечает за представление числа. Валюта, допустимая точность и момент округления остаются правилами вашей системы.
Как выбрать для конкретного проекта
Для каталога товаров с одной валютой и фиксированной точностью bigint в минимальных единицах может быть простым и надёжным решением. Для учёта нескольких валют, курсов и дробных промежуточных расчётов удобнее numeric с отдельным кодом валюты и явными ограничениями. Если данные в основном отображаются, но не участвуют в финансовой арифметике, всё равно проверьте, не появятся ли такие вычисления позже.
Не смешивайте валюты в одном суммировании. Группируйте итог по валюте либо сначала переводите суммы по определённому курсу и правилу округления. Иначе сумма 100 в одной валюте плюс 100 в другой останется арифметическим результатом, но не осмысленной денежной величиной.
Если храните одновременно проводки и текущий баланс, определите, что служит источником истины. Баланс, пересчитанный из проводок, проще сверять; сохранённый отдельно баланс быстрее читать, но требует атомарного обновления вместе с записью проводки. Проверяйте этот сценарий в транзакции и тестируйте конкурентные списания — иначе корректный тип столбца не защитит от двойного расходования.
Где схема может сломаться
Самые частые сбои возникают не из-за одного сложения, а на границах систем. API может передать сумму как число с плавающей точкой; импорт — принести лишние дробные знаки; отчёт — сложить разные валюты; миграция — округлить значения при смене масштаба. Для денежных значений передавайте десятичную сумму строкой или используйте тип данных клиента, который сохраняет десятичную точность, а не преобразует её в двоичное число.
Проверьте отдельно отрицательные значения, возвраты, нулевые суммы, максимальный размер операции и сумму большого числа строк. Если разрешаете отрицательные суммы, явно определите, означают ли они возврат, корректировку или долг: одно ограничение «сумма не меньше нуля» тогда будет неверным. Поведение PostgreSQL 16 и выбранных типов сверяйте с документацией при обновлении сервера или драйвера.
Термин. Масштаб (scale) — число цифр после десятичного разделителя, которое допускает тип или правило хранения. Например, масштаб 2 допускает значения с двумя дробными знаками; это не утверждение о том, что любая валюта использует именно такую точность.
Что делать дальше
Составьте таблицу правил для каждой денежной величины: валюта, допустимая точность, максимальная сумма, этап округления и тип хранения. Затем проверьте миграции, драйвер приложения и формат API на одних и тех же граничных примерах. В рабочей среде следите за расхождениями между проводками и балансами, ошибками переполнения и значениями, отклонёнными ограничениями схемы.
В этой статье речь о хранении сумм и точности. Инвалидация баланса, бухгалтерская модель проводок и правила конвертации валют — отдельные задачи, которые стоит проектировать вместе с требованиями продукта.
Защитник прав потребителей: Досудебные претензии на возврат товара со ссылками на ЗоЗПП.
Спросить за 15 ₽Источники: PostgreSQL 16: Numeric Types, PostgreSQL 16: Monetary Type
Частые вопросы
Можно ли хранить деньги в PostgreSQL в REAL или DOUBLE PRECISION?
Что выбрать: NUMERIC или BIGINT?
Почему не использовать тип MONEY?
Можно ли хранить все суммы с двумя знаками после запятой?
Что делать, если сумма уже записана в FLOAT или DOUBLE PRECISION?
Есть ли отличия в PostgreSQL другой версии?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.