PostgreSQLJuniorTechnical

Что такое numeric vs float8/real? Почему в финансах берут numeric?

numeric хранит точные десятичные цифры и используется для денег, потому что float8/real — двоичное приближение, при котором 0.1+0.2 ≠ 0.3 и ошибки накапливаются в финансовых операциях.

Как хранятся числа: двоичная vs десятичная арифметика

real (float4) и double precision / float8 используют IEEE 754 — двоичное представление с плавающей точкой. Большинство десятичных дробей (0.1, 0.15, 1.005) не представимы точно в двоичной системе и хранятся приближённо.

numeric(precision, scale) хранит число как строку десятичных цифр с явной точностью. Вычисления точные, но медленнее из-за программной арифметики.

Демонстрация проблемы

-- Проблема float в финансах
SELECT 0.1::float8 + 0.2::float8 = 0.3::float8;  -- false!
SELECT 0.1::float8 + 0.2::float8;  -- 0.30000000000000004

-- numeric работает точно
SELECT 0.1::numeric + 0.2::numeric = 0.3::numeric;  -- true
SELECT 0.1::numeric + 0.2::numeric;  -- 0.3

-- Накопительная ошибка в финансах
SELECT SUM(amount::float8)
FROM (VALUES (0.1), (0.1), (0.1), (0.1), (0.1),
             (0.1), (0.1), (0.1), (0.1), (0.1)) AS t(amount);
-- Результат: 0.9999999999999999, а не 1.0

Правильная схема для финансовых данных

CREATE TABLE payments (
    payment_id   bigint          PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
    user_id      bigint          NOT NULL REFERENCES users(user_id),
    amount       numeric(19, 4)  NOT NULL CHECK (amount > 0),
    fee          numeric(19, 4)  NOT NULL DEFAULT 0,
    currency     char(3)         NOT NULL DEFAULT 'RUB',
    created_at   timestamptz     NOT NULL DEFAULT now()
);

-- Для аналитики и ML — float подходит
CREATE TABLE product_recommendations (
    product_id   bigint          PRIMARY KEY,
    score        double precision NOT NULL,  -- приближённая метрика, точность не критична
    click_rate   real            NOT NULL
);

Когда что использовать

  • numeric(p, s) — деньги, налоги, ставки, лимиты, баланс, любые значения где точность и воспроизводимость обязательны.
  • double precision / float8 — координаты GPS, научные измерения, ML-скоры, статистика, метрики производительности.
  • real / float4 — то же, что float8, но с меньшей точностью (7 значащих цифр); экономит место в таблицах с миллиардами строк.
  • money — избегать: зависит от lc_monetary, неудобен при обмене данными, не поддерживает параметризацию precision.

Хранение в приложении

В Python используйте Decimal для работы с numeric-колонками:

from decimal import Decimal
import asyncpg

# asyncpg автоматически конвертирует numeric в Decimal
async def get_balance(conn: asyncpg.Connection, user_id: int) -> Decimal:
    row = await conn.fetchrow(
        "SELECT balance FROM wallets WHERE user_id = $1", user_id
    )
    return row["balance"]  # Decimal, не float

# Никогда не делайте:
balance = float(row["balance"])  # теряем точность!

Подводные камни

  • Незаметные копеечные расхождения: float-ошибки в 0.000001 накапливаются при тысячах операций и ломают финансовые сверки.
  • ORM-конвертации: SQLAlchemy по умолчанию может смапить Python float в double precision; явно указывайте Numeric(19, 4) в Column.
  • numeric без precision: numeric без (p, s) хранит до 131072 цифр до запятой — огромный overhead; всегда задавайте precision и scale.
  • Производительность: numeric в 3–10 раз медленнее float в агрегатах на больших таблицах; на тяжёлой OLAP-аналитике это ощутимо.
  • Сравнение с литералами: WHERE price = 1.99 для float-колонки непредсказуемо; для numeric работает корректно.
  • Тип money и локаль: SELECT '1,234.56'::money зависит от lc_monetary сервера; при смене локали данные ломаются.
  • Индексы на numeric: работают нормально, но размер индекса больше, чем для bigint; для колонок с целыми суммами лучше хранить в копейках как bigint.

Common mistakes

  • Сравнивать float на точное равенство.
  • Брать numeric без нужной scale.
  • Использовать money как универсальный тип денег.

What the interviewer is testing

  • Просит пример ошибки округления.
  • Проверяет выбор для amount и score.
  • Уточняет precision/scale.

Sources

Related topics