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.