Урок 03

Типы данных

Зачем типы

Правильный тип:

  • Экономит место — int вместо text.
  • Ускоряет запросы — сравнения и индексы.
  • Ловит ошибки — нельзя записать строку в число.
  • Документирует — структура видна из схемы.

PostgreSQL строже MySQL. Не даст вставить 'abc' в integer.

Числовые типы

Целые:

ТипРазмерДиапазон
smallint2 байта-32 768 … 32 767
integer (int, int4)4 байта-2.1 млрд … 2.1 млрд
bigint (int8)8 байт±9.2 квинтиллиона

Правило: id — bigint. Счётчики — integer. Флаги — smallint или boolean.

Автоинкремент:

-- Старый способ
id SERIAL PRIMARY KEY           -- integer
id BIGSERIAL PRIMARY KEY        -- bigint

-- Современный (PostgreSQL 10+)
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY

Дробные:

ТипТочностьДля чего
real (float4)6 знаковнаучные расчёты
double precision (float8)15 знаковкоординаты
numeric(p, s)точноденьги

Деньги — только numeric:

price NUMERIC(10, 2) NOT NULL
-- 12345678.90

⚠️ float для денег — потеря точности. 0.1 + 0.2 != 0.3.

Строковые типы

ТипОсобенности
char(n)фиксированная длина, добивает пробелами
varchar(n)переменная, макс n
varcharпеременная, без лимита
textпеременная, без лимита

Правило:

  • text — по умолчанию (в PostgreSQL он не медленнее).
  • varchar(n) — если нужен бизнес-лимит (email ≤ 255).
  • char(n) — почти никогда.
email VARCHAR(255) NOT NULL,
name TEXT NOT NULL,
code CHAR(2)   -- страна: US, RU

Дата и время

ТипОсобенности
dateтолько дата
timeтолько время
timestampбез таймзоны
timestamptzс таймзоной
intervalпромежуток

Правило: всегда timestamptz. Хранит в UTC, отдаёт в таймзоне клиента.

created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()

⚠️ timestamp без зоны — источник багов между серверами.

Интервалы:

SELECT NOW() + INTERVAL '1 day 2 hours';
SELECT '2026-01-01'::date - '2025-12-25'::date;  -- 7

Логический

is_active BOOLEAN NOT NULL DEFAULT TRUE

Значения: TRUE, FALSE, NULL. Не 0/1.

UUID

Встроенный с 13 версии:

id UUID PRIMARY KEY DEFAULT gen_random_uuid()

Плюсы:

  • Не пересекается при merge.
  • Не угадывается в URL.
  • Удобно в распределённых системах.

Минусы:

  • 16 байт vs 8.
  • Хуже для индекса (случайный порядок).

Правило: для публичных id (в URL) — UUID. Для внутренних FK — bigint.

JSON / JSONB

ТипОсобенности
jsonхранит как текст, не индексируется
jsonbбинарный, индексируется, быстрее

Правило: всегда jsonb.

metadata JSONB NOT NULL DEFAULT '{}'::jsonb

Запросы:

-- Достать поле
SELECT metadata->>'theme' FROM users;

-- Проверить наличие ключа
SELECT * FROM users WHERE metadata ? 'theme';

-- Поиск по вложенному
SELECT * FROM users WHERE metadata->'prefs'->>'lang' = 'ru';

-- Индекс
CREATE INDEX idx_users_metadata_theme ON users ((metadata->>'theme'));

Когда использовать:

  • Динамические поля (настройки, метаданные).
  • Внешние API (сырые ответы).
  • Не для бизнес-логики (там — колонки).

Массивы

tags TEXT[] NOT NULL DEFAULT '{}',
scores INTEGER[]

Операции:

INSERT INTO posts (tags) VALUES (ARRAY['php', 'laravel']);

SELECT * FROM posts WHERE 'php' = ANY(tags);

SELECT tags[1] FROM posts;   -- первый элемент (индексы с 1)

SELECT array_length(tags, 1) FROM posts;

Индекс:

CREATE INDEX idx_posts_tags ON posts USING GIN(tags);

Когда:

  • Список тегов.
  • Небольшие массивы.
  • Не для связи многие-ко-многим (там таблица).

ENUM

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

CREATE TABLE orders (
    status order_status NOT NULL DEFAULT 'pending'
);

Плюсы: валидация на уровне БД. Минусы: тяжело менять (нельзя удалить значение).

Альтернатива — lookup-таблица:

CREATE TABLE order_statuses (code TEXT PRIMARY KEY);
INSERT INTO order_statuses VALUES ('pending'), ('paid');

Правило: для стабильных списков — ENUM. Для изменяемых — таблица.

Специальные

Сетевые:

inet, cidr, macaddr

Гео:

point, line, polygon   -- встроенные
geometry               -- PostGIS

Полнотекстовый поиск:

tsvector, tsquery

Диапазоны:

int4range, tstzrange, numrange

Пример:

CREATE TABLE bookings (
    period TSTZRANGE NOT NULL,
    EXCLUDE USING GIST (period WITH &&)   -- запрет пересечений
);

Пример: таблица пользователей

CREATE TABLE users (
    id            BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    uuid          UUID NOT NULL DEFAULT gen_random_uuid() UNIQUE,
    email         VARCHAR(255) NOT NULL UNIQUE,
    name          TEXT NOT NULL,
    password_hash TEXT NOT NULL,
    balance       NUMERIC(12, 2) NOT NULL DEFAULT 0,
    metadata      JSONB NOT NULL DEFAULT '{}'::jsonb,
    tags          TEXT[] NOT NULL DEFAULT '{}',
    is_active     BOOLEAN NOT NULL DEFAULT TRUE,
    created_at    TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    updated_at    TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

Приведение типов

SELECT '123'::integer;         -- 123
SELECT 123::text;              -- '123'
SELECT '2026-01-01'::date;
SELECT CAST('123' AS INTEGER);

-- Проверка типа
SELECT pg_typeof(123);

Правило: явное приведение — всегда лучше неявного.

Частые ошибки

1. varchar(255) по привычке:

В PostgreSQL text не медленнее

Решение: использовать text.

2. float для денег:

0.1 + 0.2 = 0.30000000000000004

Решение: numeric(12,2).

3. timestamp без зоны:

Сервер в UTC, клиент в MSK — разница в 3 часа

Решение: timestamptz.

4. JSON вместо колонок:

Нельзя сделать FK, тяжело индексировать

Решение: бизнес-данные — колонки. JSON — для метаданных.

5. Serial с большим ID:

int переполнится на 2.1 млрд

Решение: bigint + identity.

6. ENUM для изменяемых значений:

Нельзя удалить значение из ENUM

Решение: lookup-таблица.

Практика

1. Создайте таблицу products:

CREATE TABLE products (
    id          BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    sku         VARCHAR(64) NOT NULL UNIQUE,
    name        TEXT NOT NULL,
    price       NUMERIC(10, 2) NOT NULL CHECK (price >= 0),
    stock       INTEGER NOT NULL DEFAULT 0 CHECK (stock >= 0),
    attributes  JSONB NOT NULL DEFAULT '{}'::jsonb,
    tags        TEXT[] NOT NULL DEFAULT '{}',
    is_active   BOOLEAN NOT NULL DEFAULT TRUE,
    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

2. Вставьте товар с JSON-атрибутами и тегами.

3. Запросите по атрибуту attributes->>'color' = 'red'.

4. Запросите по тегу 'sale' = ANY(tags).

Итог

  • Целые: smallint, integer, bigint.
  • Дробные: numeric для денег.
  • Строки: text по умолчанию.
  • Дата: timestamptz всегда.
  • UUID: для публичных id.
  • JSONB: для метаданных.
  • Массивы: для тегов.
  • ENUM: для стабильных списков.

Дальше — создание таблиц и схемы.