Learning Platform
Урок 04.01 · 18 мин
Начальный
Numeric typesINTBIGINTNUMERICREALFloating pointCAST

Зачем разбираться в числовых типах

Многие приходят из языков, где есть «просто число»: в JavaScript это number, в Python — int или float, который сам решает, когда вырасти. В SQL такого нет. Тип фиксируется при CREATE TABLE, занимает строго определённое число байт на диске, и от выбора зависит и сколько влезет в строку, и что произойдёт при переполнении, и точны ли деньги до копейки.

Разбираться придётся. К счастью, в PostgreSQL числовых типов всего шесть штук, и они укладываются в две группы: целые и с плавающей/фиксированной точкой.

Целые: SMALLINT, INTEGER, BIGINT

Целые типы отличаются только шириной — то есть диапазоном значений и расходом байт.

Целые типы PostgreSQL

Размер растёт — диапазон растёт экспоненциально. Цена — байты на диске и в памяти.

SMALLINT2 байта
диапазон−32 768 … 32 767
зачемгода, % скидки, мелкие счётчики
INTEGER4 байта
диапазон±2.1 млрд
зачемдефолтный выбор для ID и счётчиков
BIGINT8 байт
диапазон±9.2 · 10¹⁸
зачемID больших таблиц, суммы в центах

В реальном проде

переход с INT на BIGINT для первичных ключей — типовой кризисный жанр
. Если ожидается рост сильно за миллиард строк или счётчиков — сразу пиши BIGINT, иначе пятилетний долг прилетит ровно тогда, когда мигрировать дороже всего.

Посмотрим на размеры и переполнение целых типов:

PostgreSQL

Переполнение — это ошибка, а не «обнуление»

В отличие от C/Java, в PostgreSQL целые не оборачиваются при переполнении. Если попытаться вписать 40000 в SMALLINT, СУБД бросит ошибку integer out of range и откатит транзакцию. Это сознательное решение: лучше падать громко, чем тихо подменять данные.

Поймай переполнение SMALLINT — ошибка должна вылететь:

PostgreSQL

Тот же запрос с ::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:

PostgreSQL

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-моделей, физика, статистика.

Плохи везде, где числа сравниваются на равенство, складываются миллион раз подряд, или представляют деньги.

NUMERIC vs DOUBLE PRECISION

Один — точный и медленный. Другой — быстрый и приблизительный. Выбор не идеологический, а доменный.

NUMERIC(p, s)точные десятичные
скоростьмедленнее в 5–50 раз
размерпеременный, ~8–24 байта
когдаденьги, проценты, бухгалтерия
DOUBLE PRECISIONIEEE 754 binary64
скоростьнативная (FPU CPU)
размерфиксированно 8 байт
когдакоординаты, физика, ML, статистика

Почему деньги хранят в INT центах

Самая частая ошибка в финансовых системах — price NUMERIC(10,2) (хорошо) или price DOUBLE PRECISION (катастрофа). Индустриальный стандарт — хранить деньги в наименьшей неделимой единице (центы, копейки, паисы) как целое, обычно BIGINT.

Три причины:

  1. Скорость. Сложение BIGINT — это одна CPU-инструкция. NUMERIC идёт через библиотеку произвольной точности, в десятки раз медленнее.
  2. Никакого скрытого округления. 12.34 + 0.01 в центах — это 1234 + 1 = 1235. Никаких представлений, никаких эпсилонов.
  3. Простое сравнение и хеширование. Индексы по BIGINT компактнее и быстрее, чем по NUMERIC.

Цена — нужно делить на 100 при выводе и помнить о масштабе. Но эта цена платится один раз на слое представления, а выигрыш получаем на каждой операции.

Именно поэтому в нашей вселенной курса products.price_cents и order_items.unit_price_cents — это INT в центах (на самом деле, в нашей seed-таблице мы умножаем ещё на 100, чтобы влезть в типичный диапазон — но идея та же).

Деньги как INT: суммируем заказы, делим на 10000 для рублей с копейками:

PostgreSQL

CAST и оператор ::

В PostgreSQL приведение типов записывается двумя способами:

  • Стандартный SQL: CAST(value AS target_type).
  • PostgreSQL-shortcut: value::target_type.

Они эквивалентны. Второй короче и чаще встречается в проде.

Два эквивалентных способа приведения, плюс деление целых vs дробное:

PostgreSQL

Заметь третью колонку: 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.

Четыре способа избавиться от дроби:

PostgreSQL

Заметил, что ROUND(2.5) = 3, а ROUND(3.5) = 4? Это «банковское» округление к ближайшему чётному (banker’s rounding) — для статистических задач оно даёт меньший

накопленный сдвиг
, чем привычное «0.5 всегда вверх».

Подводный камень: NUMERIC без точности

NUMERIC без параметров — это «как угодно длинно». Удобно для расчётов, но очень дорого для хранения. Если в колонке лежит «3.14159265358979323846…», то каждая строка тащит десятки байт там, где DOUBLE PRECISION обошёлся бы 8-ю. Правило: в продакшен-таблицах всегда указывай (p, s).

Второе: NUMERIC не имеет встроенной защиты от переполнения по (p, s). Если попытаешься вставить 12345.67 в NUMERIC(5,2) — получишь ошибку numeric field overflow. Это поведение CHECK-подобное и срабатывает на INSERT/UPDATE.

Проверка знанийKnowledge check
У тебя в проде колонка price DOUBLE PRECISION. На отчёте за месяц SUM(price) расходится с ручной выверкой на 0.07 рублей. Что произошло и как чинить?
ОтветAnswer
Это накопленная ошибка floating-point. IEEE 754 не может точно представить большинство десятичных дробей с двумя знаками (0.1, 0.2, 12.34 — все они в двоичной системе бесконечные периодические). При суммировании миллионов значений ошибки округления накапливаются — обычно в седьмом-восьмом знаке. Починка: миграция колонки в NUMERIC(15,2) или (правильнее) в BIGINT с хранением в копейках. До миграции — пересчёт отчёта через CAST(price AS numeric(15,2)) перед SUM. Системное решение — баг-репорт «деньги хранятся в DOUBLE PRECISION», это архитектурный долг.
Анатомия tuple на странице: как типы влияют на размер строки Числовые типы в DuckDB: DECIMAL, HUGEINT, FLOAT Оптимальный выбор типов данных в ClickHouse

Чек-лист

  • Целые типы — 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 — банковское округление к ближайшему чётному.

Проверьте понимание

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. Команда вставляет в SMALLINT-колонку значение 50000. Что произойдёт в PostgreSQL?

Закончили урок?

Отметьте его как пройденный, чтобы отслеживать свой прогресс

Войдите чтобы оценить урок

Прогресс модуля
0 из 6