open atlas
↑ К треку
SQL и PostgreSQL вглубь SQL · 06 · 01

Базовые типы: деньги, текст, время, идентичность

У Postgres точная система типов. Выбирай numeric (не float) для денег, timestamptz (никогда timestamp) для времени и правильную версию uuid — дефолты кусаются в проде.

SQL Middle ◷ 15 min
Уровень
ОсновыJuniorMiddleSenior
Уже знаешь этот юнит? Пройди быструю проверку за минуту →

Биллинг хранил цены в колонке double precision, потому что «это же число». Два года никто не замечал. Потом аудитор просуммировал 4,2 млн позиций — и итог разошёлся на $1847. Не украли, а округлили по доле цента за раз. 0.1 + 0.2 в плавающей точке не равно 0.3, и база — худшее место узнавать это. Фикс — одна строка смены типа и болезненный backfill. Тип, выбранный в первый день, — это решение, с которым живёшь годами.

Семейства типов: выбирай по смыслу

Postgres группирует встроенные типы в несколько семейств. Хорошее моделирование начинается с того, что каждую колонку сопоставляют семейству, отвечающему тому, что значение означает, а не тому, что проще набрать. Прежде чем создать следующую колонку, спроси себя: что это значение представляет — счётчик, момент времени, идентификатор — и делает ли выбранный тип этот смысл обязательным или просто подразумевает его?

Числа: никогда не храни деньги во float

integer — 4-байтовое целое со знаком (до ~2,1 млрд); bigint — 8 байт (до ~9,2 квинтиллиона). Используй bigint для всего, что считает события, строки или деньги-в-центах на масштабе — переполнение integer на первичном ключе — это реальный инцидент.

Жёсткое правило: деньги — это numeric или целые центы, никогда real/double precision. numeric — точная десятичная дробь: хранит заданные цифры и считает в десятичной системе, так что 0.10 + 0.20 ровно 0.30. Плавающая точка хранит двоичное приближение, а у 0.1 нет точного двоичного представления. Посмотри сам:

SELECT 0.1::float8 + 0.2::float8 AS float_sum,   -- 0.30000000000000004
       0.1::numeric + 0.2::numeric AS exact_sum;  -- 0.3

-- денежная колонка как надо:
CREATE TABLE orders (
  id          bigint PRIMARY KEY,
  total_cents bigint NOT NULL,        -- целые центы: быстро, точно, без аргумента scale
  tax         numeric(12, 2) NOT NULL -- или numeric с фиксированной точностью
);

numeric(12, 2) значит до 12 значащих цифр, из них 2 после запятой. Компромисс: арифметика numeric эмулируется программно и примерно в 10–100× медленнее нативной float-математики CPU, а каждое значение переменной ширины. Для денег это не проблема — правит корректность, — но не тяни numeric в горячую научную агрегацию, где крошечная погрешность округления допустима.

Текст: text и varchar(n) одинаковы по скорости

Стойкий миф: varchar(50) быстрее или меньше, чем text. В Postgres это не так. Все три — text, varchar(n) и varchar — делят одинаковое хранение и одни и те же операторы. Единственное, что varchar(n) добавляет, — это check-ограничение на длину: Postgres проверяет, что каждая запись не длиннее n символов, и отвергает более длинные с ошибкой.

Значит, выбор только в том, нужно ли тебе это ограничение. Лимит длины, отражающий реальное бизнес-правило (2-символьный код страны), — хорошее моделирование; произвольный varchar(255), скопированный из MySQL-туториала, — шум. Postgres-нативная привычка — text плюс явный CHECK, когда граница действительно нужна: яснее, и можно менять границу без неуклюжего alter типа.

CREATE TABLE users (
  id        bigint PRIMARY KEY,
  email     text NOT NULL,
  country   text NOT NULL CHECK (char_length(country) = 2)  -- реальное правило, явно
);

Время: timestamptz, всегда

Это самая частая ошибка моделирования, и тихая. Есть два типа timestamp:

  • timestamp (он же timestamp without time zone) хранит значение по настенным часам и выбрасывает зону. Это наивное «2026-06-02 14:00» без привязки к реальному моменту.
  • timestamptz (timestamp with time zone) хранит абсолютный момент. На входе Postgres конвертирует ввод в UTC и хранит его; на выходе рендерит в зоне сессии TimeZone. Он не хранит исходную зону — он хранит момент.

timestamptz — правильный дефолт практически для любого события, строки лога, created_at и дедлайна. С простым timestamp два сервера в разных зонах, вставляя «сейчас», пишут разные абсолютные моменты, которые сравниваются неверно, и истину потом не восстановить. Храни момент; пусть приложение решает, как его показать.

CREATE TABLE events (
  id          bigint PRIMARY KEY,
  occurred_at timestamptz NOT NULL DEFAULT now(),  -- абсолютный момент в UTC
  kind        text NOT NULL
);

-- date — для календарных дат без времени; interval — для интервала:
SELECT occurred_at::date AS day,
       now() - occurred_at AS age      -- age это interval
FROM events;

Используй date для настоящих календарных дат (день рождения, день биллинга) и interval для промежутков («7 дней», «90 минут») — вычитание двух timestamptz даёт interval.

UUID: случайный v4 против упорядоченного по времени v7

uuid — 16-байтовый тип для ключей, генерируемых без центральной последовательности: полезен между шардами или до того, как строка дойдёт до базы. Версия важна для локальности индекса:

  • v4 полностью случаен. Каждая вставка попадает в случайное место в B-tree первичного ключа, так что у индекса нет горячего хвоста — записи разбросаны по страницам, hit rate кеша падает, а индекс фрагментируется и пухнет под тяжёлой нагрузкой вставок.
  • v7 встраивает миллисекундную метку времени в старшие биты, так что значения примерно упорядочены по времени. Вставки приклеиваются к правому краю B-tree, как serial, держа индекс компактным и дружелюбным к кешу.

Для таблицы с высоким темпом вставок v7 (или bigint identity) пишется ощутимо быстрее и куда меньше склонен к bloat индекса, чем v4. Тянись к случайному v4 только когда неугадываемость самого ключа — требование.

Граничные случаи

Postgres поставляет встроенный случайный генератор (gen_random_uuid(), это v4) в ядре. Нативный uuidv7() появился в Postgres 18; на старых версиях v7 генерируют в приложении или расширением. Вывод не «всегда v7», а в том, что физический порядок ключа правит производительностью записи в индекс, так что случайный ключ на write-heavy таблице — осознанный компромисс, а не бесплатный дефолт.

Касты: будь явным на границе

Postgres неявно кастует в одних контекстах и отказывается в других; опора на неявные касты — это как получить неожиданные планы (индекс пропущен, потому что сравнение вынудило каст) или ошибки времени выполнения. Делай касты явными через ::type или CAST(x AS type) на краях запроса, особенно при сравнении колонки с литералом другого типа.

SELECT '42'::int + 1                 AS to_int,      -- 43
       '2026-06-02'::date            AS to_date,
       total_cents::numeric / 100    AS dollars      -- центы → значение для показа
FROM orders;
Викторина

Почему деньги нельзя хранить в колонке double precision (float)?

Викторина

Что на самом деле хранит колонка timestamptz?

Закончи аналогию

Заполни пропуск: text и varchar(n) хранят данные в Postgres идентично; единственное, что varchar(n) добавляет, — это _______ длины, отвергающий строки длиннее n.

Вспомните перед уходом
  1. 01
    Два способа корректно хранить деньги в Postgres и почему float неверен.
  2. 02
    Что хранит timestamptz и почему предпочесть его timestamp?
  3. 03
    varchar(50) быстрее или меньше, чем text в Postgres? В чём реальная разница и что с uuid v4 против v7?
Итог

Моделирование с Postgres начинается с колонки: выбирай тип по тому, что значение означает. Деньги — numeric (точная десятичная) или целые центы, никогда real/double precision, потому что двоичная плавающая точка не представляет 0.1 точно, и погрешность округления накапливается в неверные итоги. Время — timestamptz, хранящий абсолютный момент в UTC и рендерящий в зоне сессии; простой timestamp выбрасывает зону и даёт серверам в разных зонах писать несравнимые значения. text и varchar(n) идентичны по хранению и скорости — varchar(n) лишь добавляет проверку длины, так что предпочитай text плюс явный CHECK для реальных правил. Для ключей предпочитай bigint identity или упорядоченный по времени uuid v7, чтобы держать индекс компактным; случайный v4 фрагментирует B-tree и оправдан лишь когда ключ должен быть неугадываемым. И кастуй явно на границах запроса, чтобы планировщик видел задуманное сравнение. Теперь, когда встретишь в ревью схемы колонку double precision для денег или timestamp без зоны — ты будешь знать, какой тихий сбой там живёт, и к какому однострочному фиксу тянуться.

Практика

Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.

вспомнитьприменитьуглубить0 из 7 завершено
Связанные уроки

Что-то непонятно?

Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.

хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.