Почему в PostgreSQL для денег используют numeric и стоит ли менять его на bigint
Тип numeric в PostgreSQL рекомендуют для денежных расчётов, но предупреждают о медленной работе. Разбираемся, можно ли заменить его на bigint с копейками или double precision, и чем это грозит.

Разработчики PostgreSQL прямо рекомендуют тип numeric для хранения денежных сумм и других величин, где важна точность. Однако тут же признают, что вычисления с numeric выполняются очень медленно по сравнению с целочисленными и плавающими типами. Это противоречие заставляет задуматься: а так ли необходим numeric, или можно обойтись более быстрыми типами, например bigint с хранением копеек? Давайте разберёмся, какие риски и преимущества скрываются за этим выбором.
Почему numeric считается золотым стандартом для денег
Тип numeric в PostgreSQL — это десятичный тип с произвольной точностью. Он позволяет хранить числа с точным десятичным представлением, без ошибок округления, свойственных двоичной арифметике с плавающей запятой. Именно поэтому документация рекомендует его для денежных сумм: при расчётах с деньгами недопустимы ситуации, когда 0.1 + 0.2 даёт 0.30000000000000004, как это происходит в double precision. Для финансовых операций, бухгалтерского учёта и любых расчётов, где каждый цент на счету, точность критична.
Однако цена этой точности — производительность. PostgreSQL выполняет операции с numeric через собственную библиотеку произвольной точности, что значительно медленнее, чем арифметика на процессоре для целых чисел или чисел с плавающей запятой. В документации прямо сказано: «calculations on numeric values are very slow compared to the integer types, or to the floating-point types». Для высоконагруженных систем, где выполняются миллионы операций в секунду, это может стать узким местом.
Можно ли заменить numeric на bigint с копейками
Идея хранить денежные суммы в виде целого числа копеек (или центов, если валюта — доллар) кажется привлекательной: bigint — это 64-битное целое, операции с ним выполняются на аппаратном уровне, что даёт огромный прирост скорости. Но здесь есть подводные камни. Во-первых, нужно договориться о единице хранения: если валюта — рубль, то логично хранить копейки, но тогда максимальная сумма ограничена примерно 92 миллиардами рублей (92233720368547758 копеек). Для большинства компаний этого достаточно, но для крупных корпораций или государственных бюджетов может не хватить. Во-вторых, при делении и умножении на дробные коэффициенты (например, налог 13%) придётся выполнять дополнительные операции округления, и здесь легко ошибиться. PostgreSQL не предоставляет встроенных функций для безопасного округления денежных сумм, поэтому вся логика ложится на разработчика.
Кроме того, при использовании bigint теряется наглядность: в базе данные хранятся в копейках, и при отладке или написании SQL-запросов приходится постоянно помнить о делении на 100. Это повышает риск ошибок. Тем не менее, многие крупные компании, включая некоторые банки, используют именно такой подход для оптимизации производительности, особенно в системах с огромным потоком транзакций.
А что насчёт double precision
Тип double precision (число с плавающей запятой двойной точности) — ещё один кандидат на замену. Он работает быстро, но имеет фундаментальный недостаток: двоичное представление не может точно выразить большинство десятичных дробей. Например, число 0.1 в двоичной системе — это бесконечная периодическая дробь, и при хранении оно округляется. При накоплении таких ошибок в тысячах операций итоговая сумма может отличаться от ожидаемой на несколько копеек или даже рублей. Для финансовых расчётов это неприемлемо: бухгалтерский баланс не сойдётся, а аудиторы зададут неудобные вопросы.
Некоторые разработчики пытаются использовать double precision и округлять результат на каждом шаге, но это не спасает: ошибки накапливаются внутри самих операций, и округление лишь маскирует проблему. Классический пример: SELECT 0.1::double precision + 0.2::double precision возвращает 0.30000000000000004, и никакое округление не сделает это значение точным 0.3. Поэтому для денег double precision — табу в серьёзных проектах.
Сравнение производительности и точности
Чтобы понять масштаб проблемы, рассмотрим типичный тест: сумма 10 миллионов операций сложения. Для bigint это займёт миллисекунды, для double precision — чуть дольше, а для numeric — в десятки раз дольше. В реальных системах, где ежедневно обрабатываются миллионы платежей, разница в скорости может означать часы дополнительной работы сервера. Однако современные процессоры и оптимизации PostgreSQL (например, JIT-компиляция) частично нивелируют этот разрыв, но не полностью.
С другой стороны, точность numeric не ограничена: можно хранить числа с любой разрядностью, вплоть до 131072 цифр до запятой и 16383 после. Это делает его универсальным для любых финансовых операций, включая расчёты с высокой точностью, например, в криптовалютах или научных вычислениях.
Что выбрать практикующему разработчику
Если ваш проект — типичный интернет-магазин или банковская система с умеренной нагрузкой, то numeric — безопасный выбор: он гарантирует точность и избавляет от головной боли с округлением. Если же вы строите высоконагруженную систему, где каждая миллисекунда на счету, и вы готовы взять на себя заботу о корректном округлении, то можно рассмотреть bigint с хранением в минимальных денежных единицах. Но помните: это потребует тщательного тестирования и написания дополнительных функций для арифметики.
Что касается double precision — его стоит использовать только для величин, где точность не критична: например, для рейтингов, процентов (не денежных), физических измерений. Для денег — никогда.
Что будет дальше
В сообществе PostgreSQL периодически обсуждается возможность добавления специализированного типа для денег, который сочетал бы скорость bigint и точность numeric. Например, в некоторых СУБД есть тип MONEY, но в PostgreSQL он имеет ограничения и не рекомендуется к использованию. Возможно, в будущих версиях появится улучшенная реализация, но пока разработчикам приходится выбирать между производительностью и точностью самостоятельно.
Итог
Тип numeric остаётся стандартом для денежных расчётов в PostgreSQL, несмотря на его медлительность, потому что точность важнее скорости для финансовых операций. Замена на bigint возможна, но требует высокой квалификации команды и тщательного контроля округлений. Double precision для денег неприемлема. Взвесьте требования вашего проекта: если нагрузка не экстремальная, не изобретайте велосипед — используйте numeric.
Следите за развитием PostgreSQL: возможно, в будущем появятся новые типы, которые снимут эту дилемму.