Деньги в PostgreSQL: почему float портит расчёты
Типы real и double precision хранят числа приблизительно: уже сумма 0.1 и 0.2 может дать не ровно 0.3. Разбираем, чем заменить float в PostgreSQL, когда подойдёт numeric, а когда — целое число в минимальных единицах валюты.

В PostgreSQL выражение 0.1 + 0.2 с типом double precision может вернуть 0.30000000000000004, а не 0.3. Для графика это обычно безобидная погрешность. Для платежа, баланса или сверки такой способ хранения превращает мелкую неточность в спор о том, кто кому должен. Ниже — выбор для PostgreSQL 16 и 17: numeric или целые минимальные единицы, с примерами и ограничениями каждого подхода.
Откуда берётся лишняя дробь
Типы real и double precision используют двоичное представление с плавающей точкой. Многие десятичные дроби — в том числе 0.1 и 0.2 — нельзя записать в нём точно. База хранит ближайшее представимое значение, а при сложении и умножении погрешность может переходить в результат.
Проверьте разницу прямо в PostgreSQL:
SELECT 0.1::double precision + 0.2::double precision AS approximate_sum;
SELECT 0.1::numeric + 0.2::numeric AS exact_decimal_sum;
В первой строке результат может отображаться как 0.30000000000000004, во второй — как 0.3. Приведение типа здесь показано явно: оно исключает сомнения, какой тип участвует в вычислении. В приложении тип значения тоже важен — если сумма сначала попала в float, последующее сохранение в numeric не восстановит потерянные десятичные цифры.
Точность хранения и правило округления — разные вещи. Тип может точно сохранить число, но бизнес-правило всё равно должно решить, когда и как округлять итог.
Термин: минимальная единица валюты. Это наименьшая единица, в которой система учитывает сумму. Для валюты с двумя десятичными знаками ею обычно служит одна сотая основной единицы. У разных валют и финансовых операций правила точности могут отличаться.
Три варианта хранения
| Вариант | Что выигрываете | Чем платите | Типичный выбор |
|---|---|---|---|
numeric(p, s) | Десятичные значения хранятся точно; можно задать точность и масштаб. | Вычисления обычно обходятся дороже, чем с целыми числами; нужно выбрать масштаб и политику округления. | Цены, суммы платежей, налоги и другие десятичные финансовые значения. |
| Целое число минимальных единиц | Точные сложение и сравнение целых значений; часто удобно для остатка пакета и платежных событий. | Нужно переводить сумму для показа и учитывать валюту, её точность и промежуточные дробные результаты. | Платёжная система, где сумма операции выражается в целых минимальных единицах. |
real или double precision | Подходят для приближённых численных расчётов и измерений. | Десятичная сумма может представляться неточно, а сравнение на равенство — давать неожиданный результат. | Научные вычисления, измерения, графики; не источник истины для денег. |
Когда выбрать numeric
Это прямой вариант, если предметная модель оперирует десятичными суммами и вам важно не терять их представление при хранении и арифметике. В PostgreSQL тип называется numeric; decimal — его синоним. Ограничение вида numeric(p, s) задаёт общую точность и число цифр после десятичного разделителя.
Например, столбец numeric(12, 2) рассчитан на значения с двумя знаками после разделителя. Но это не просто проверка «разрешены только два знака»: PostgreSQL округляет значение до заданного масштаба при его приведении к такому столбцу. Если лишние цифры нужно не округлять, а отклонять, проверьте это правило отдельно до записи.
Не выбирайте масштаб по привычке. Если приложение хранит только конечные суммы с двумя знаками, одного решения может быть достаточно. Если оно учитывает курсы, доли, ставки или промежуточные вычисления, им может потребоваться больше знаков, чем итоговому платёжному столбцу. Важно определить отдельно точность хранения исходных данных и момент округления результата.
Когда выбрать целые минимальные единицы
Этот вариант удобен, когда сумма платежа всегда целая в выбранной минимальной единице. Например, приложение хранит число таких единиц, а валюту — в отдельном поле. Тогда прибавление и сравнение платежей выполняются без двоичной погрешности.
Цена — дополнительная ответственность в коде и схеме данных. Нельзя считать, что у всех валют одинаковое число десятичных знаков, а умножение суммы на процент или деление на доли не всегда даст целое число. Для таких вычислений заранее задайте правило округления, а не отбрасывайте остаток молча. И не смешивайте суммы разных валют в одной арифметике без явного пересчёта.
Почему float не становится точным от округления на экране
Форматирование до двух знаков может показать 0.30, но это меняет только отображение, а не сохранённое значение. Аналогично, условие «сравнить значения после округления» иногда маскирует дефект: разные части системы могут округлить в разных местах или по разным правилам. Для денег надёжнее хранить исходную сумму в подходящем точном типе, а округлять там, где этого требует предметное правило.
Что ломается на практике
- Проверка равенства. Два вычисленных float-значения, которые должны представлять одну сумму, могут различаться в последних разрядах. Не используйте такое сравнение как проверку равенства денежных сумм.
- Суммирование операций. Погрешности отдельных значений могут накапливаться. Итог зависит от данных и последовательности вычислений, поэтому универсальную «безопасную поправку» нельзя назначить без анализа системы.
- Перенос уже накопленных данных. Преобразование float в numeric не может достоверно определить, какое десятичное число пользователь вводил изначально. До миграции выясните, как данные создавались, и сравните старые суммы с исходными документами или журналами операций.
- Округление в нескольких слоях. Если базу, сервер приложения и интерфейс заставить округлять по отдельности, суммы на экране, в отчёте и в платеже могут разойтись. Зафиксируйте одно правило и точку, где оно применяется.
- Общее поле для разных валют. Одно число без кода валюты не сообщает, что именно вы храните. Точность и допустимые операции должны учитывать валюту и смысл суммы.
Как выбрать и проверить решение
- Опишите операции. Выпишите, что система делает с суммой: сохраняет платёж, складывает возвраты, умножает цену на количество, делит комиссию. Результат: понятно, где возникают промежуточные дроби.
- Зафиксируйте правило округления. Укажите, на каком этапе операция превращается в платёжную сумму и что происходит с остатком. Результат: правило одинаково для базы, приложения и отчётов.
- Выберите тип для источника истины. Для десятичных финансовых значений рассмотрите numeric; для целых минимальных единиц — целочисленный тип с отдельной валютой. Результат: выбор соответствует данным, а не только удобству вывода.
- Проверьте границы и миграцию. Испытайте максимальную сумму, допустимое число знаков, частичные возвраты и повторный импорт. Для старых float-данных сопоставьте значения с независимым источником. Результат: переполнение и округление не обнаруживаются впервые в проде.
- Измерьте нагрузку на реальных запросах. Сравните время запросов и потребление ресурсов на копии характерных данных, при той же версии PostgreSQL и сопоставимой нагрузке. Результат: вы видите цену numeric именно в своём приложении, а не опираетесь на выдуманный универсальный коэффициент.
У numeric есть цена в производительности по сравнению с типами с фиксированной разрядностью, но конкретная разница зависит от запросов и объёма данных. Если узкое место критично, измеряйте профиль нагрузки до смены типа. Для корректности финансовых записей экономия на точности обычно не стоит риска расхождения.
Где подход ломается
numeric решает проблему двоичного представления, но не задаёт за вас бухгалтерское правило. Деление может потребовать округления, ограниченный масштаб округляет входные значения, а чрезмерно узкая точность может привести к ошибке при записи. Целочисленный подход тоже не универсален: дробный результат нельзя без правила превратить в целое число минимальных единиц.
Отдельно храните валюту и смысл суммы, если таблица обслуживает несколько валют или типов операций. Тип money в PostgreSQL существует, но его текстовый вывод зависит от локали; он также не заменяет поле валюты для системы с разными валютами. Перед выбором проверьте ограничения и формат обмена с приложением.
Не переносите существующую колонку с float в numeric одним преобразованием и не считайте, что так восстановили точные исходные цены. Сначала выясните, откуда пришли значения и как их округляли раньше; затем подготовьте проверку миграции на копии данных.
Что делать дальше
Возьмите одну денежную операцию из приложения — например, оплату с возвратом — и проследите сумму от ввода до отчёта. Зафиксируйте тип каждого значения, валюту, место округления и поведение при делении. Затем добавьте тесты на десятичные дроби, граничные значения и повторную загрузку платежа. В проде следите не только за скоростью запросов, но и за расхождениями между суммой операции, возвратом и итогом сверки.
Инвалидация отчётов и выбор схемы для бухгалтерского учёта — отдельные темы; здесь речь только о типе числового значения и точности его хранения.
Тест и проверочная: Быстрые ответы с объяснением, почему остальные варианты неверны
Спросить за 10 ₽Источники: PostgreSQL 17: Numeric Types, PostgreSQL 17: Monetary Types
Частые вопросы
Можно ли хранить деньги в double precision, если округлять до двух знаков?
Что лучше для цены — numeric или целое число копеек?
Какой масштаб numeric(p, s) поставить для денег?
Как безопасно перевести старые значения float в numeric?
Изменилось ли поведение numeric в PostgreSQL 16 и 17?
Что делать, если сумма из numeric всё равно отличается от ожидаемой?
Есть ли у numeric подводные камни в проде?
Материал носит информационный характер и подготовлен редакцией «Агентуры». Он не является офертой, рекламой или индивидуальной консультацией. Упомянутые продукты, компании и торговые знаки принадлежат их правообладателям. Перед принятием решений, влекущих юридические или финансовые последствия, обратитесь к профильному специалисту.