Как правильно хранить денежные суммы (цены) в реляционной базе данных?

«Как правильно хранить денежные суммы (цены) в реляционной базе данных?» — вопрос из категории Базы данных, который задают на 24% собеседований PHP Разработчик. Ниже — развёрнутый ответ с разбором ключевых моментов.

Ответ

Для хранения денежных сумм необходимо использовать тип данных, который гарантирует точность при арифметических операциях и отсутствие ошибок округления, характерных для чисел с плавающей запятой.

Правильный выбор: DECIMAL/NUMERIC (фиксированная точность).

CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    -- DECIMAL(total_digits, decimal_places)
    -- total_digits - общее количество знаков (до 38 в PostgreSQL)
    -- decimal_places - количество знаков после запятой
    price DECIMAL(10, 2) NOT NULL, -- например, 9999999.99
    currency CHAR(3) NOT NULL DEFAULT 'USD'
);

-- Все операции будут точными
INSERT INTO products (name, price) VALUES ('Laptop', 1299.99);
UPDATE products SET price = price * 0.9; -- 10% скидка

Почему не FLOAT/REAL/DOUBLE? Эти типы используют двоичное представление и могут давать ошибки округления при десятичных операциях, что критично для финансов.

-- Опасный пример с FLOAT
SELECT 0.1::FLOAT + 0.2::FLOAT; -- Может вернуть 0.30000000000000004
-- Правильный пример с DECIMAL
SELECT 0.1::DECIMAL + 0.2::DECIMAL; -- Всегда вернет 0.3

Дополнительные практики:

  • Храните сумму в минимальных единицах (центы, копейки) как целое число (INTEGER или BIGINT). Это полностью исключает проблемы с округлением и упрощает арифметику. Отображение с разделителем — задача приложения.
    price_amount INTEGER NOT NULL, -- 1299.99 USD хранится как 129999 (центов)
    price_currency CHAR(3) NOT NULL
  • Для сложных финансовых систем рассмотрите библиотеки с точной десятичной арифметикой на уровне приложения (например, BigDecimal в Java, decimal в Python).
  • Всегда указывайте валюту отдельным полем. Агрегация сумм в разных валютах без конвертации — ошибка.