В PostgreSQL три строковых типа — text, varchar(n), char(n). Внутри сервера они хранятся одинаково и работают одинаково быстро. Разница только в семантике ограничения длины.
1. По умолчанию — text
PG-T-020 Если у поля нет бизнес-причины ограничивать длину — берём text.
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 для длинных значений), скорость одинаковая. Длина — этоCHECK-проверка перед записью, и больше ничего.varchar(255)пришёл из MySQL/Oracle, гдеvarchar(255)действительно отличался отtext. В PostgreSQL это простоtextсCHECK (length <= 255)и неудобный путь миграции, если завтра окажется 256.- Менять
textнаvarchar(n)дешевле, чем расширятьvarchar(n)(последнее в старых версиях переписывало таблицу).
2. Когда длина оправдана
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
Плохие случаи:
full_name varchar(255) -- почему 255?
description varchar(1000) -- почему 1000?
Если завтра приходит русское имя в 4 слова с дефисами или клиент пишет описание длиннее лимита — будет 500-я ошибка на ровном месте. Берите text, валидация длины — на уровне приложения, если она нужна.
3. char(n) — почти никогда
char(n) — фиксированная длина с обязательным паддингом пробелами:
CREATE TABLE foo (code char(5));
INSERT INTO foo VALUES ('AB');
SELECT '[' || code || ']' FROM foo; -- '[AB ]'
PG-T-022 char(n) оправдан только для строго фиксированной длины из стандарта — char(2) для ISO-страны, char(3) для валюты, char(36) под формат UUID-строки (хотя и это неправильно — см. UUID, нужен тип uuid).
Для всего остального — паддинг пробелами создаёт сюрпризы при LIKE, =, length() и при сериализации в JSON.
4. Case-insensitive: citext или функциональный индекс
Логины, email, теги часто сравниваются без учёта регистра. Два рабочих подхода:
Подход 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'; -- найдёт
Подход 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');
PG-T-023 Для case-insensitive — citext либо lower() + functional index. Не LOWER() без индекса в каждом запросе.
citext короче в коде, но:
- не работает с типизированными драйверами JDBC «из коробки» (приедет как
text, проверьте, что jOOQ генерирует); - усложняет миграцию, если решите переехать на другой движок.
text + functional index — более портируемый вариант, работает везде.
5. Энкодинг
PG-T-024 Кластер должен быть UTF8. Локально-зависимые сборки баз — наследие.
SHOW server_encoding; -- ожидаем UTF8
Это про инфраструктуру, не про DDL, но проверять стоит — иногда платформенные базы получают SQL_ASCII от старых deployment-скриптов, и потом эмодзи в комментариях покупателей ломают всё.
6. Длинные строки (TOAST)
PostgreSQL автоматически выносит длинные значения колонок в отдельное хранилище (TOAST). Порог — около 2 KB на запись. Это работает прозрачно: вы не управляете этим явно, но имеет смысл знать:
textс большими статьями/описаниями физически хранится отдельно от основной строки;- частое чтение «маленьких» колонок таблицы не задевает большие тексты;
EXPLAIN ANALYZEпокажет ниже скорость, если запрос требует чтения toast-сегментов.
PG-T-025 Если в таблице есть и часто читаемый набор коротких полей, и редко-читаемый длинный текст (например, журнал событий с полем body), не разделяйте их на две таблицы преждевременно — TOAST уже это делает. Делите, только если измерения показывают узкое место.
Чек-лист на ревью
- [ ] Нет
varchar(255)и подобных «магических» длин без бизнес-обоснования. - [ ] Длина
varchar(n)/char(n)соответствует доменному правилу (стандарт, формат, регуляторное ограничение). - [ ]
char(n)использован только для строго фиксированной длины. - [ ] Case-insensitive поля защищены
citextили functional unique index — не остались только в коде приложения. - [ ] Кластер в
UTF8.
Связанные
- Числа и точность.
- UUID и идентификаторы — почему UUID не
varchar(36). - Антипаттерны.