Опирается на правила:
PG-T-020…PG-T-025из PostgreSQL Style Guide → раздел Строковые типы.
Важно знать
- По умолчанию —
text. Неvarchar(255).text,varchar(n),char(n)в PostgreSQL хранятся одинаково, отличаются только семантикой.- Длину ставим, когда это доменное правило (E.164, ISO 3166-1), не «техническое».
char(n)почти никогда — обязательный паддинг пробелами создаёт сюрпризы.- Case-insensitive —
citextили functional unique index наlower().- Кластер должен быть
UTF8—SHOW server_encoding.- TOAST автоматически выносит длинные значения (>2KB) — не разделяй преждевременно.
PostgreSQL — не Oracle и не MySQL. В нём строковые типы внутри сервера хранятся одинаково (TOAST для длинных значений), работают одинаково быстро. Длина — это CHECK-проверка перед записью, и больше ничего. UCP формулирует: дефолт — text, длина — только при доменном правиле.
По умолчанию — text
PG-T-020:
CREATE TABLE customer (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
full_name text NOT NULL,
email text NOT NULL,
bio text
);
Аргументы:
- Хранение одинаковое.
textиvarchar(n)используют тот же TOAST для длинных значений, та же скорость. varchar(255)пришёл из MySQL/Oracle, где это действительно отличалось отtext. В PostgreSQLvarchar(255)=text+CHECK (length(...) <= 255). Если завтра окажется нужно 256 — миграция.- Менять
textнаvarchar(n)дешевле, чем расширятьvarchar(n)(последнее в старых версиях PG переписывало таблицу).
Когда длина оправдана
PG-T-021: длина — доменное правило, не «техническое».
-- ✓ — доменные стандарты
phone_e164 varchar(15) NOT NULL, -- E.164 ограничивает до 15 цифр
country_code char(2) NOT NULL, -- ISO 3166-1 alpha-2: ровно 2 буквы
currency char(3) NOT NULL, -- ISO 4217: ровно 3 буквы
inn varchar(12) NOT NULL, -- ИНН: ФЛ 12, ЮЛ 10
-- ✗ — почему 255? почему 1000?
full_name varchar(255),
description varchar(1000)
Сценарий поломки: русское имя «Анна-Виктория Александрова Петрова-Иванова» — 40+ символов с дефисами. varchar(255) нормально, но как только клиент напишет описание длиннее лимита — 500 Internal Server Error на ровном месте.
Для full_name, description — берём text. Валидация длины — на уровне приложения (Jakarta Validation @Size).
char(n) — почти никогда
PG-T-022: только для строго фиксированной длины из стандарта.
CREATE TABLE foo (code char(5));
INSERT INTO foo VALUES ('AB');
SELECT '[' || code || ']' FROM foo; -- '[AB ]'
char(5) для строки 'AB' хранит её как 'AB ' (с паддингом пробелов). Это создаёт сюрпризы:
LIKE 'AB%'— нужноLIKE 'AB%'(работает), но=—'AB'vs'AB '(тонкость).length()возвращает 5, не 2.- JSON serialization —
"AB "с пробелами.
Допустимо для:
char(2)— ISO 3166-1 страна.char(3)— ISO 4217 валюта.
Для всего остального — text или varchar(n).
Case-insensitive
PG-T-023: два подхода.
Подход 1: citext
CREATE EXTENSION IF NOT EXISTS citext;
CREATE TABLE account (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email citext NOT NULL UNIQUE
);
INSERT INTO account (email) VALUES ('ivan@example.com');
SELECT * FROM account WHERE email = 'IVAN@EXAMPLE.COM'; -- найдёт
Плюсы: код короче. Минусы:
- Не работает с типизированными JDBC-драйверами «из коробки» — приходит как
text, проверь, что jOOQ генерирует. - Усложняет миграцию на другой движок.
Подход 2: text + functional unique index
CREATE TABLE account (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email text NOT NULL
);
CREATE UNIQUE INDEX account_email_lower_uk ON account (lower(email));
-- запросы должны явно использовать lower():
SELECT * FROM account WHERE lower(email) = lower('IVAN@EXAMPLE.COM');
Плюсы: портируемый, работает везде.
Минусы: каждый запрос обязан использовать lower(email) — забыли → seq scan.
Что нельзя
-- ✗ — LOWER() без индекса
SELECT * FROM account WHERE LOWER(email) = LOWER('...'); -- seq scan
PostgreSQL не использует обычный b-tree индекс на email для LOWER(email) = .... Нужен functional index (lower(email)) или citext.
UTF8
PG-T-024: кластер всегда UTF8.
SHOW server_encoding; -- ожидаем UTF8
Локально-зависимые кодировки (SQL_ASCII, LATIN1, WIN1251) — наследие. На UTF8 эмодзи, multilingual content работают без ошибок.
Это про инфраструктуру, не про DDL — но проверять стоит. Иногда платформенные базы получают SQL_ASCII от старых deployment-скриптов, и эмодзи в комментариях покупателей ломают всё.
TOAST для длинных строк
PG-T-025: автоматическое разделение.
PostgreSQL автоматически выносит длинные значения колонок в отдельное хранилище (TOAST). Порог — около 2 KB на запись.
Что это даёт:
textс большими статьями/описаниями физически хранится отдельно от основной строки.- Частое чтение коротких полей таблицы не задевает большие тексты.
EXPLAIN ANALYZEпоказывает меньшую скорость, если запрос требует чтения toast-сегментов.
Не разделяйте таблицу на основную + «details» преждевременно — TOAST уже это делает. Делите, только если измерения показывают узкое место.
Что запрещено
| Антипаттерн | Правило | Что взамен |
|---|---|---|
varchar(255) без причины | PG-T-020 | text |
varchar(1000) для description | PG-T-021 | text |
varchar(36) для UUID | PG-T-022 | тип uuid (см. UUID) |
char(50) для phone, email | PG-T-022 | text или varchar(n) с E.164 |
LOWER(email) без functional index | PG-T-023 | functional index или citext |
SQL_ASCII encoding | PG-T-024 | UTF8 |
Разделение длинного text в отдельную таблицу преждевременно | PG-T-025 | trust TOAST |
| Длина «потому что DBA сказал» | PG-T-021 | доменное обоснование |
Куда дальше
- PG → Строковые типы — нормативные формулировки.
- Числа и точность — bigint, numeric.
- Время и таймзоны — timestamptz.
- UUID и идентификаторы — тип
uuid, неchar(36). - JSONB — когда оправдан, когда нет — для structured данных.
- Полнотекстовый поиск (FTS) —
tsvectorдля search. - Антипаттерны типов — сводка.