Learning Platform
Урок 11.06 · 17 мин
Средний
UUIDCITEXTINETCIDRMONEYdata types

PostgreSQL — одна из тех СУБД, у которых под капотом лежит не пара десятков типов, а целая зоопарк специализированных. Этот урок — про четыре утилитарных типа, которые ты будешь встречать каждый день, и про когда они оправданы, а когда нет.

UUID — глобально уникальные идентификаторы

UUID — 128-битный идентификатор, который статистически уникален без координации между серверами. Это альтернатива SERIAL/BIGSERIAL для систем, где id нужно генерировать до записи в БД — например, на клиенте мобильного приложения или в распределённой системе.

В Postgres 13+ есть встроенная функция gen_random_uuid() — она возвращает UUID v4 (полностью случайный). До 13 нужно было расширение pgcrypto или uuid-ossp.

Генерация UUID и базовая структура:

PostgreSQL

UUID хранится как 16 байт. Текстовая запись — 36 символов с дефисами (123e4567-e89b-12d3-a456-426614174000). Никогда не храни UUID как TEXT — это в два раза больше места и в разы медленнее на сравнении.

Когда UUID лучше SERIAL

  • Распределённая генерация: клиент или внешний сервис создают id без обращения к мастеру.
  • Безопасность через нечитаемость: SERIAL делает url-ы вида /orders/1234 — легко угадать соседние. UUID непредсказуем.
  • Слияние данных из нескольких источников: при миграции из двух БД с SERIAL начинаются конфликты id.

Когда UUID хуже SERIAL

  • Производительность вставки: случайный UUID v4 ломает локальность B-tree индекса. Каждая вставка приземляется в случайное место, страницы кэша грязнят чаще. На массовой вставке разница может быть в разы.
    UUID v7
    решает эту проблему, но он ещё не везде встроен.
  • Место на диске: 16 байт против 4 (INT) или 8 (BIGINT). На сотнях миллионов строк это терабайты разницы.
  • Читаемость в логах: /orders/1234 понятнее, чем /orders/123e4567-....

Правило: если нет конкретной причины — используй BIGINT SERIAL/IDENTITY. Если есть распределённость или security-через-нечитаемость — UUID.

CITEXT — case-insensitive строки

В нашем customers колонка email TEXT NOT NULL UNIQUE. Это значит, что [email protected] и [email protected] — две разных строки, и UNIQUE их не отловит. На практике это создаёт дубликаты пользователей.

Стандартное решение — нормализовать email до lowercase на уровне приложения. Это работает, но требует дисциплины.

Лучшее решение — тип CITEXT из расширения citext:

CITEXT — сравнения и UNIQUE работают как case-insensitive:

PostgreSQL

CITEXT — это специальный TEXT, у которого оператор = и B-tree сравнения работают через lower(). Cтоит чуть больше CPU на каждое сравнение, но избавляет от целого класса багов.

Альтернатива без CITEXT — expression unique index CREATE UNIQUE INDEX ON users (lower(email)). Это тоже работает, но требует, чтобы любой запрос писал WHERE lower(email) = lower('...'). CITEXT делает всё прозрачно.

INET и CIDR — IP-адреса и сети

Многие команды хранят IP как VARCHAR(45) — потому что «строка же». Это неверно.

INET хранит IP-адрес (v4 или v6) с опциональной маской. CIDR — то же самое, но представляет сеть (не хост). Они занимают ровно 7 байт для v4 и 19 для v6 (в сравнении с 7-15 байтами у текста для v4) — но главное не место, а операции.

INET даёт настоящие сетевые операции:

PostgreSQL

Операторы:

  • << — IP принадлежит сети.
  • >> — сеть содержит IP.
  • && — две сети пересекаются.

Это разительно отличается от VARCHAR, где «принадлежит ли 192.168.1.10 к /24» — пишется отдельной функцией.

Типичный кейс — банлисты, гео-фильтры, аудит-логи. Везде, где IP — это не просто строка, а сущность, по которой нужно делать операции на сетевом уровне.

MONEY — и почему её не любят

MONEY существует. Использовать её — почти всегда плохая идея.

MONEY выглядит безобидно — но дальше начинаются проблемы:

PostgreSQL

Что не так:

  • Зависимость от lc_monetary. Один и тот же литерал '$1,234.56' парсится по-разному в зависимости от настройки сервера. На production это иногда выводит из себя.
  • Только одна валюта на колонку. MONEY ничего не знает про USD vs EUR. Хранить разные валюты в одной колонке нельзя.
  • Один масштаб дробной части. По умолчанию — 2 знака. Биткоин или ставка процента «0.05%» сюда не помещаются.
  • Конвертация в NUMERIC теряет валютную метку (потому что её и не было).

Что использовать вместо:

  • NUMERIC(precision, scale) для суммы. Для центов часто пишут BIGINT (хранить целые центы) — это избегает плавающей точки.
  • TEXT или CHAR(3) для кода валюты (‘USD’, ‘EUR’).
  • Если нужно «много валют» — отдельная таблица exchange_rates или модель с парой (amount_cents, currency).

MONEY оставляют для тех систем, у которых одна валюта и где совместимость со старым кодом важнее чистоты. В новых проектах — обычно избегают.

Маленькая шпаргалка по выбору типа

Когда какой тип использовать вместо очевидного-но-неподходящего.

Нужен глобальный idUUID (gen_random_uuid)
Локальный idBIGINT IDENTITY
Email (UNIQUE без регистра)CITEXT
Обычная строкаTEXT
IP-адрес или сетьINET / CIDR
Сумма денегNUMERIC + код валюты

ENUM или TEXT + CHECK?

В нашей orders есть status TEXT NOT NULL CHECK (status IN ('pending', 'paid', ...)). Альтернатива — встроенный тип ENUM:

CREATE TYPE order_status AS ENUM ('pending', 'paid', 'shipped', 'delivered', 'cancelled', 'refunded');

CREATE TABLE orders (
  ...
  status order_status NOT NULL
);

Преимущества ENUM:

  • Занимает 4 байта (как INT), а не длина строки.
  • Гарантирует валидный набор на уровне типа, не CHECK.
  • Сортируется в порядке объявления, не алфавитно ('pending' < 'paid' < 'shipped').

Минусы:

  • Невозможно удалить значение (ALTER TYPE ... DROP VALUE не существует). Только пересоздать тип.
  • Добавить значение можно (ALTER TYPE ... ADD VALUE), но в начало списка только в Postgres 12+.
  • Сравнение ENUM-значений с TEXT требует явного cast’а.

Правило: ENUM хорош для стабильных, редко меняющихся наборов (статус заказа, тип события). Для часто-эволюционирующих (категории контента, теги) лучше TEXT + CHECK или отдельная справочная таблица.

ENUM в деле — обрати внимание, как сортируется по порядку объявления:

PostgreSQL

ORDER BY priority DESC отсортировал по «важности» (critical → low) автоматически, потому что внутри ENUM хранится порядковый номер.

Ещё пара полезных мелочей

Кроме того, что мы разобрали, в Postgres стоит знать:

  • BYTEA — бинарные данные. Не для больших файлов (лучше внешнее хранилище), но для хэшей, маленьких изображений, ключей — нормально.
  • ENUM — фиксированный набор значений. Эффективно, но переименовать значение позже сложно — Postgres хранит порядок. Многие предпочитают TEXT + CHECK constraint, что гибче.
  • INTERVAL — продолжительность времени. '2 hours'::interval, '1 month'::interval. Складывается с TIMESTAMPTZ и DATE.
  • BIT(n) и BIT VARYING — битовые поля. Эффективно для флагов и масок, но используется редко: обычно проще BOOLEAN-колонки или JSONB.
Проверка знанийKnowledge check
У вас в новом проекте: пользователи регистрируются по email. Email уникальный. Команда планирует хранить email как TEXT и нормализовать через приложение. В чём риск, и что предложить?
ОтветAnswer
Риск: дисциплину легко нарушить. Любой пропущенный INSERT/UPDATE без lower() создаст «[email protected]» и «[email protected]» как два разных пользователя — UNIQUE TEXT этого не отловит. Дальше — двойные учётки, пропавшие пароли, путаница в логах. Два правильных варианта: (1) CITEXT — прозрачно для всех запросов, специальный тип решает задачу на уровне СУБД; (2) Expression unique index: CREATE UNIQUE INDEX ON users (lower(email)) + дисциплина «всегда искать через lower(email) = lower($1)». Первое надёжнее, второе портативнее (CITEXT — постгрес-специфика). Просто TEXT + надежда — плохой план.
UUID vs serial vs ULID — выбор суррогатного ключа

Чек-лист

  • UUID — 128 бит, статистически уникален без координации. Генерируй gen_random_uuid() (Postgres 13+).
  • UUID хорош для распределённых систем и нечитаемости URL. Плох для производительности B-tree (но v7 решает).
  • CITEXT (расширение citext) — case-insensitive строка. Идеально для email, имён пользователей.
  • INET/CIDR для IP-адресов и сетей. Дают операторы <<, >>, &&. Не храни IP как VARCHAR.
  • MONEY лучше не использовать в новых проектах. Замена: NUMERIC(p,s) + отдельная колонка кода валюты.
  • Для центов часто хранят BIGINT напрямую — никаких float-сюрпризов.

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

Результат: 0 из 0
Прикладной
Вопрос 1 из 4. В каком из сценариев UUID — лучший выбор, чем обычный BIGINT IDENTITY?

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

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

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

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