Выбор типа данных для хранения денежных сумм — фундаментальное решение, от которого зависит корректность всех расчётов. Рассмотрим три подхода.
FLOAT / DOUBLE PRECISION — категорически нет.
Числа с плавающей запятой используют двоичное представление по стандарту IEEE 754. Десятичная дробь 0.1 не имеет точного двоичного представления — в памяти хранится 0.1000000000000000055511151231257827021181583404541015625. Это не баг, а особенность формата.
-- Демонстрация проблемы FLOAT
SELECT 0.1::DOUBLE PRECISION + 0.2::DOUBLE PRECISION;
-- Результат: 0.30000000000000004
SELECT (0.1::DOUBLE PRECISION + 0.2::DOUBLE PRECISION) = 0.3::DOUBLE PRECISION;
-- Результат: false
-- Накопление ошибки при суммировании
SELECT SUM(amount) FROM generate_series(1, 1000000)
CROSS JOIN LATERAL (SELECT 0.01::DOUBLE PRECISION AS amount) t;
-- Ожидаем: 10000.00
-- Получаем: 9999.999999999831
INTEGER (хранение в копейках) — надёжно, но с оговорками.
Идея: хранить 10.95 рублей как 1095 копеек. Целочисленная арифметика точна. Этот подход используют Stripe, MasterCard, Adyen.
-- Сумма в копейках
CREATE TABLE payments_int (
id BIGSERIAL PRIMARY KEY,
amount_cents BIGINT NOT NULL CHECK (amount_cents >= 0),
currency CHAR(3) NOT NULL
);
-- Отображение: 1095 → 10.95
SELECT amount_cents / 100.0 AS amount_display
FROM payments_int;
Проблемы возникают при работе с валютами, имеющими разное количество знаков: японская иена — 0, иорданский динар — 3, криптовалюты — до 18. Нужна таблица «множителей» для каждой валюты.
NUMERIC / DECIMAL — оптимальный баланс.
PostgreSQL тип NUMERIC хранит числа в десятичном формате с произвольной точностью. Арифметика точна, поддерживается любое количество знаков после запятой.
-- Точное хранение и арифметика
SELECT 0.1::NUMERIC + 0.2::NUMERIC = 0.3::NUMERIC;
-- Результат: true
-- Рекомендуемое определение для финансовых столбцов
-- NUMERIC(19, 4) — до 15 целых + 4 дробных знака
-- Достаточно для сумм до 999 триллионов с точностью до 0.01 копейки
CREATE TABLE payments (
id BIGSERIAL PRIMARY KEY,
amount NUMERIC(19, 4) NOT NULL,
currency CHAR(3) NOT NULL DEFAULT 'RUB'
);
Мы выбрали NUMERIC(19, 4) для «ПлатёжКа» — четыре знака после запятой обеспечивают запас для промежуточных вычислений (курсы валют, процентные ставки), а финальный результат округляется до двух знаков при отображении.
Оставить комментарий