Выбор типа данных — одно из ключевых решений при проектировании схемы. Неправильный выбор может привести к переполнению, потере точности, некорректной работе с временными зонами или неоправданному расходу памяти. Разберём основные типы PostgreSQL и принципы выбора.
Числовые типы
| Тип | Размер | Диапазон |
|---|---|---|
SMALLINT | 2 байта | −32 768 … 32 767 |
INTEGER | 4 байта | −2.1 млрд … 2.1 млрд |
BIGINT | 8 байт | −9.2 × 10¹⁸ … 9.2 × 10¹⁸ |
NUMERIC(p,s) | переменный | произвольная точность |
REAL | 4 байта | ~6 знаков точности |
DOUBLE PRECISION | 8 байт | ~15 знаков точности |
Когда что использовать:
INTEGER— счётчики, ID (если таблица не вырастет до 2 млрд строк), возраст, количество.BIGINT— ID в высоконагруженных системах, временны́е метки в миллисекундах, финансовые идентификаторы.NUMERIC— деньги, физические измерения, где нужна точность. Никогда не храните деньги вFLOAT/REAL.DOUBLE PRECISION— научные расчёты, координаты (с пониманием погрешности).
-- Правильно: деньги в NUMERIC
CREATE TABLE invoices (
invoice_id BIGSERIAL PRIMARY KEY,
amount NUMERIC(15, 2) NOT NULL, -- до 999 млрд с копейками
tax_rate NUMERIC(5, 4) -- например, 0.2000
);
-- Неправильно: деньги в FLOAT (потеря точности при вычислениях)
-- amount FLOAT -- никогда так не делайте
Почему FLOAT опасен для денег:
SELECT 0.1::FLOAT + 0.2::FLOAT;
-- Результат: 0.30000000000000004
SELECT 0.1::NUMERIC + 0.2::NUMERIC;
-- Результат: 0.3
Текстовые типы: просто используйте TEXT
В PostgreSQL есть три варианта:
CHAR(n)— строка фиксированной длины, дополняется пробелами. Почти никогда не нужен.VARCHAR(n)— строка с максимальной длиной.TEXT— строка без ограничений.
Производительность TEXT и VARCHAR в PostgreSQL одинакова — оба хранятся одинаково внутри. Ограничение VARCHAR(n) — это просто CHECK-констрейнт на длину.
-- Нет смысла писать VARCHAR(255) вместо TEXT
-- Используйте TEXT везде, добавляйте ограничение только если оно осмысленно
CREATE TABLE users (
username TEXT NOT NULL CHECK (LENGTH(username) BETWEEN 3 AND 50),
email TEXT NOT NULL,
country_code CHAR(2) -- вот тут CHAR(2) оправдан: строго 2 символа
);
Типы дат и времени
| Тип | Описание |
|---|---|
DATE | Только дата |
TIME | Только время (без даты, без TZ) |
TIMESTAMP | Дата + время, без временно́й зоны |
TIMESTAMPTZ | Дата + время, с временно́й зоной (хранит UTC) |
INTERVAL | Промежуток времени |
Главное правило: всегда используйте TIMESTAMPTZ.
TIMESTAMP хранит «наивное» время без информации о зоне. Когда пользователи работают из разных часовых поясов, или сервер переезжает в другой регион, данные становятся неоднозначными.
TIMESTAMPTZ хранит UTC внутри и автоматически конвертирует в текущую временну́ю зону при отображении.
-- Правильно
CREATE TABLE events (
event_id BIGSERIAL PRIMARY KEY,
title TEXT NOT NULL,
starts_at TIMESTAMPTZ NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Операции с TIMESTAMPTZ работают корректно при переводе часов
SELECT *
FROM events
WHERE starts_at BETWEEN NOW() AND NOW() + INTERVAL '7 days';
DATE оправдан, когда время действительно не нужно: дата рождения, дата окончания подписки, рабочий день.
BOOLEAN
Занимает 1 байт. Принимает TRUE/FALSE/NULL. Не нужно хранить 0/1 в INTEGER или строки 'yes'/'no'.
ALTER TABLE users ADD COLUMN is_active BOOLEAN NOT NULL DEFAULT TRUE;
ALTER TABLE users ADD COLUMN is_premium BOOLEAN NOT NULL DEFAULT FALSE;
-- Индекс на булевом поле полезен, только если значения неравномерны
-- Например, индекс на is_active оправдан, если 95% пользователей активны
CREATE INDEX idx_users_inactive ON users(user_id) WHERE is_active = FALSE;
UUID
UUID — 128-битный идентификатор, гарантированно уникальный без координации между серверами.
-- Включить расширение для генерации UUID v4
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE sessions (
session_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id INT REFERENCES users(user_id),
expires_at TIMESTAMPTZ NOT NULL
);
Подробнее о выборе между UUID и SERIAL — в статье о первичных ключах.
ENUM vs справочная таблица
PostgreSQL поддерживает пользовательские ENUM-типы:
CREATE TYPE order_status AS ENUM ('pending', 'paid', 'shipped', 'delivered', 'cancelled');
CREATE TABLE orders (
order_id BIGSERIAL PRIMARY KEY,
status order_status NOT NULL DEFAULT 'pending'
);
Минус ENUM: добавить новое значение — ALTER TYPE, требующий блокировки. Удалить значение нельзя без пересоздания типа.
Альтернатива — справочная таблица:
CREATE TABLE order_statuses (
code TEXT PRIMARY KEY,
label TEXT NOT NULL,
sort_order INT
);
CREATE TABLE orders (
order_id BIGSERIAL PRIMARY KEY,
status TEXT NOT NULL REFERENCES order_statuses(code) DEFAULT 'pending'
);
Справочная таблица гибче: добавление/удаление значений — обычный INSERT/DELETE без блокировок. Используйте ENUM только для действительно стабильных наборов (дни недели, стороны света).
JSONB: когда схема неизвестна заранее
JSONB хранит JSON в бинарном формате с поддержкой индексирования:
CREATE TABLE products (
product_id BIGSERIAL PRIMARY KEY,
name TEXT NOT NULL,
attributes JSONB -- гибкие атрибуты: цвет, размер, материал и т.д.
);
-- GIN-индекс для быстрого поиска внутри JSONB
CREATE INDEX idx_products_attributes ON products USING GIN (attributes);
-- Поиск по атрибуту
SELECT * FROM products
WHERE attributes @> '{"color": "red", "size": "XL"}';
JSON (без B) хранит исходный текст и медленнее. Используйте JSONB — всегда.
Итоговая шпаргалка
| Задача | Тип |
|---|---|
| Счётчик, возраст | INTEGER |
| Большой ID, метрики | BIGINT |
| Деньги | NUMERIC(15,2) |
| Любой текст | TEXT |
| Фиксированный код (RU, USD) | CHAR(2-3) |
| Дата и время | TIMESTAMPTZ |
| Только дата | DATE |
| Флаг да/нет | BOOLEAN |
| Глобальный ID | UUID |
| Гибкие атрибуты | JSONB |
Правильный выбор типов — это инвестиция, которая окупается при масштабировании. Потренируйтесь проектировать схемы и писать запросы в нашем тренажёре SQL.