А точнее о том, как их лучше хранить в БД.
Начинали мы как-то разработку нового проекта, и нужно было решить, как хранить деньги. Говорят, лучше в банке, но нам надо было в БД.
Складывать, вычитать, умножать и делить планировали в коде приложения.
База — PostgreSQL.
Я предложила вариант, с которым уже работала раньше:
✅ DECIMAL (он же NUMERIC) — тип данных, который рекомендуется для хранения денежных сумм и других величин, где важна точность.
NUMERIC(precision, scale)
precision — общее количество значимых цифр в числе, т. е. количество цифр по обе стороны десятичной точки.scale — количество цифр в дробной части, справа от десятичной точки.Например, число
23,5141 имеет precision = 6 и scale = 4.Плюсы хранения в NUMERIC (DECIMAL):
➕ Высокая точность при расчетах (хотя расчеты планировалось делать в коде, а не в БД, полезно сразу иметь устойчивый тип данных, который хорошо мапится на аналогичный тип данных в коде)
➕ Интуитивно понятное представление значений:
видишь 575,34 — значит это 575 рублей 34 копейки (у нас были только рубли, без валют)
➕ Гибкость: если бы бизнес-требования изменились, и возникла необходимость хранить 3 или 4 знака после запятой, то можно просто поменять тип столбца через
ALTER TABLE на numeric(20, 4), а значения пересчитывать не нужно — БД просто расширит точность и добавит лишние нули после запятой, ничего не ломая:было 575,34 -> стало 575,3400
Минусы хранения в NUMERIC (DECIMAL):
➖ Операции со значениями numeric выполняются гораздо медленнее, чем с целыми числами
Коллега сказал:
— На прошлом месте мы использовали подход с хранением сумм как целых чисел в копейках. Может так сделаем?
✅ Хранение в виде целого числа minor units
Например, 57534 — это 575 рублей 34 копейки.
Хранить не обязательно в копейках, можно и в сотых долях копейки (4 знака после запятой) или других юнитах.
С точки зрения типа данных в БД можно использовать либо тип
bigint (8 байт) либо если предполагаются очень большие суммы и маленькие единицы хранения - NUMERIC со scale = 0. Во втором случае в коде тоже нужно маппить на неограниченный по размеру тип целого числа, чтобы не произошло переполнения.Плюсы хранения в виде целого числа minor units:
➕ Точные вычисления (о делении ниже)
➕ Высокая скорость вычислений, если используется bigint
Минусы хранения в виде целого числа minor units:
➖ Менее человеко-читаемое представление, выше вероятность ошибки при передаче данных в другие системы
➖ Меньше гибкости: в случае изменения требований (4 знака после запятой вместо 2) придется перезаписывать все значения:
было 57534 -> стало 5753400
➖ Дополнительные сложности с округлением при делении:
57534 копеек / 9 = 6392,666(6)
По правилам округления обычно 6392,666(6) -> 6393
Но при целочисленном делении дробная часть отбрасывается: 6392 (потеряли копейку)
Поэтому для деления в коде целое число приводится к decimal, который позволяет задавать масштаб и способ округления.
— А ведь в PostgreSQL есть тип данных MONEY, может его?
❌ Тип MONEY
Это плохая идея, так как MONEY форматирован уже с кодом валюты, которая берется из локали (
lc_monetary). Поменялась локаль, и показали пользователю вместо 575,34 ₽ -> $575.34 😅Также внутри он устроен как целочисленный тип со свойственными ему проблемами с округлением. И хотя мы не собирались выполнять деление в БД, лучше не оставлять себе таких ограничений на будущее.