Зачем разбираться в числовых типах
Многие приходят из языков, где есть «просто число»: в JavaScript это number, в Python — int или float, который сам решает, когда вырасти. В SQL такого нет. Тип фиксируется при CREATE TABLE, занимает строго определённое число байт на диске, и от выбора зависит и сколько влезет в строку, и что произойдёт при переполнении, и точны ли деньги до копейки.
Разбираться придётся. К счастью, в PostgreSQL числовых типов всего шесть штук, и они укладываются в две группы: целые и с плавающей/фиксированной точкой.
Целые: SMALLINT, INTEGER, BIGINT
Целые типы отличаются только шириной — то есть диапазоном значений и расходом байт.
Размер растёт — диапазон растёт экспоненциально. Цена — байты на диске и в памяти.
В реальном проде
BIGINT, иначе пятилетний долг прилетит ровно тогда, когда мигрировать дороже всего.
Посмотрим на размеры и переполнение целых типов:
Переполнение — это ошибка, а не «обнуление»
В отличие от C/Java, в PostgreSQL целые не оборачиваются при переполнении. Если попытаться вписать 40000 в SMALLINT, СУБД бросит ошибку integer out of range и откатит транзакцию. Это сознательное решение: лучше падать громко, чем тихо подменять данные.
Поймай переполнение SMALLINT — ошибка должна вылететь:
Тот же запрос с ::integer пройдёт без шума. Урок: тип — это договор о диапазоне, не пожелание.
NUMERIC(p, s) — точные дроби
NUMERIC (синоним — DECIMAL) хранит произвольную
p(precision) — общее число значащих цифр.s(scale) — сколько из них после запятой.
NUMERIC(10, 2) значит «до 10 цифр всего, из них 2 после точки» — то есть диапазон ±99 999 999.99. NUMERIC без параметров — это неограниченная точность (но и неограниченная цена в байтах).
Главное свойство: NUMERIC считается точно, без округления при сложении и вычитании. 0.1 + 0.2 в NUMERIC равно ровно 0.3, а не 0.30000000000000004.
Сравни точность NUMERIC и DOUBLE PRECISION:
float_eq_03 будет false. Это не баг PostgreSQL — это IEEE 754, тот же стандарт floating-point, что в любом языке. Двоичная дробь не может представить 0.1 точно, потому что 0.1 в двоичной системе — это бесконечная периодическая дробь, как 1/3 в десятичной.
REAL и DOUBLE PRECISION — быстрая неточность
REAL (4 байта, ~6 значащих цифр) и DOUBLE PRECISION (8 байт, ~15 значащих цифр) — это IEEE 754 single и double. Хороши там, где нужна скорость и не нужна точность до последней копейки: координаты GPS, веса для ML-моделей, физика, статистика.
Плохи везде, где числа сравниваются на равенство, складываются миллион раз подряд, или представляют деньги.
Один — точный и медленный. Другой — быстрый и приблизительный. Выбор не идеологический, а доменный.
Почему деньги хранят в INT центах
Самая частая ошибка в финансовых системах — price NUMERIC(10,2) (хорошо) или price DOUBLE PRECISION (катастрофа). Индустриальный стандарт — хранить деньги в наименьшей неделимой единице (центы, копейки, паисы) как целое, обычно BIGINT.
Три причины:
- Скорость. Сложение
BIGINT— это одна CPU-инструкция.NUMERICидёт через библиотеку произвольной точности, в десятки раз медленнее. - Никакого скрытого округления.
12.34 + 0.01в центах — это1234 + 1 = 1235. Никаких представлений, никаких эпсилонов. - Простое сравнение и хеширование. Индексы по
BIGINTкомпактнее и быстрее, чем поNUMERIC.
Цена — нужно делить на 100 при выводе и помнить о масштабе. Но эта цена платится один раз на слое представления, а выигрыш получаем на каждой операции.
Именно поэтому в нашей вселенной курса products.price_cents и order_items.unit_price_cents — это INT в центах (на самом деле, в нашей seed-таблице мы умножаем ещё на 100, чтобы влезть в типичный диапазон — но идея та же).
Деньги как INT: суммируем заказы, делим на 10000 для рублей с копейками:
CAST и оператор ::
В PostgreSQL приведение типов записывается двумя способами:
- Стандартный SQL:
CAST(value AS target_type). - PostgreSQL-shortcut:
value::target_type.
Они эквивалентны. Второй короче и чаще встречается в проде.
Два эквивалентных способа приведения, плюс деление целых vs дробное:
Заметь третью колонку: 7 / 2 = 3, а не 3.5. В SQL деление двух целых даёт целое — остаток отбрасывается. Чтобы получить дробный результат, хотя бы один из операндов должен быть NUMERIC или DOUBLE PRECISION. Это типичный источник багов в финансовых отчётах.
Округление: ROUND, FLOOR, CEIL, TRUNC
PostgreSQL даёт четыре функции округления:
ROUND(x, n)— банковское округление до n знаков (дляNUMERIC) или просто до целого (для остальных).FLOOR(x)— вниз до целого.CEIL(x)(он жеCEILING) — вверх до целого.TRUNC(x, n)— отбрасывание знаков после позиции n.
Четыре способа избавиться от дроби:
Заметил, что ROUND(2.5) = 3, а ROUND(3.5) = 4? Это «банковское» округление к ближайшему чётному (banker’s rounding) — для статистических задач оно даёт меньший
Подводный камень: NUMERIC без точности
NUMERIC без параметров — это «как угодно длинно». Удобно для расчётов, но очень дорого для хранения. Если в колонке лежит «3.14159265358979323846…», то каждая строка тащит десятки байт там, где DOUBLE PRECISION обошёлся бы 8-ю. Правило: в продакшен-таблицах всегда указывай (p, s).
Второе: NUMERIC не имеет встроенной защиты от переполнения по (p, s). Если попытаешься вставить 12345.67 в NUMERIC(5,2) — получишь ошибку numeric field overflow. Это поведение CHECK-подобное и срабатывает на INSERT/UPDATE.
Чек-лист
- Целые типы —
SMALLINT(2 байта),INTEGER(4 байта),BIGINT(8 байт). Для новых ID —BIGINT/BIGSERIAL. - Переполнение целого — это ошибка, не silent wrap. PostgreSQL откатит транзакцию.
NUMERIC(p, s)— точные десятичные, медленные. Для денег это безопасный выбор, если не хочешь возиться с центами.REAL/DOUBLE PRECISION— IEEE 754 floating point. Быстрые, но0.1 + 0.2 != 0.3.- Индустриальный стандарт для денег —
BIGINTв минимальной неделимой единице (центы, копейки). - Приведение типов —
CAST(x AS T)илиx::T. Деление двух целых даёт целое. ROUNDв PostgreSQL — банковское округление к ближайшему чётному.