Если вы переходите на PostgreSQL с MySQL или Oracle, первое, что удивляет — здесь почти всегда пишут просто text, без цифры в скобках. Разбираемся почему.
Откуда взялся varchar(255)
В MySQL и старых версиях Oracle строковые типы действительно различались по устройству хранения. varchar(255) физически занимал другой объём на диске, чем text. Поэтому разработчики привыкли ставить длину «на всякий случай».
В PostgreSQL так не работает. Здесь text, varchar(n) и char(n) хранятся одинаково — через один и тот же внутренний механизм. varchar(255) — это просто text с дополнительной проверкой «не длиннее 255 символов» перед каждой записью. Скорость одинакова, место на диске одинаково.
Итог: varchar(255) в PostgreSQL — это устаревшая привычка, которая добавляет ограничение без какого-либо технического смысла.
По умолчанию — text
Для большинства строковых полей правильный тип — 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(255):
- Если завтра понадобится хранить строку длиннее 255 символов — не нужна миграция таблицы.
- В старых версиях PostgreSQL расширение
varchar(n)переписывало всю таблицу. Сtextтакой проблемы нет. - Явно показывает: длина не регламентирована доменным правилом.
Когда длину указывать всё-таки нужно
Длину в колонке имеет смысл ставить, когда она продиктована реальным стандартом, а не ощущением «тут не должно быть больше тысячи символов».
Примеры обоснованных ограничений:
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, -- ИНН: 10 цифр (юрлицо) или 12 (физлицо)
Антипример — когда длина взята «из головы»:
full_name varchar(255), -- почему 255? какой стандарт?
description varchar(1000) -- почему 1000?
Для full_name и description правильно взять text, а ограничение по длине вынести в код приложения, где его легко изменить и протестировать:
// Jakarta Validation
public record CreateCustomerCommand(
@NotBlank @Size(max = 200) String fullName,
@Size(max = 5000) String description
) {}
Так бизнес-ограничение остаётся там, где ему место — в логике приложения. Если бизнес решит, что теперь можно 300 символов, меняется одна строчка в коде, а не схема базы данных.
char(n) — для фиксированных стандартов
char(n) — тип с фиксированной длиной. Если строка короче n, PostgreSQL дополняет её пробелами справа.
CREATE TABLE foo (code char(5));
INSERT INTO foo VALUES ('AB');
SELECT '[' || code || ']' FROM foo; -- '[AB ]'
Это создаёт неожиданные проблемы:
length(code)вернёт5, а не2.- При сериализации в JSON получите
"AB "с пробелами. - Сравнения могут вести себя не так, как ожидается.
Когда char(n) уместен:
char(2)— код страны по ISO 3166-1 (всегда ровно 2 буквы).char(3)— код валюты по ISO 4217 (всегда ровно 3 буквы).
Во всех остальных случаях — text или varchar(n).
Поиск без учёта регистра
Допустим, нужно хранить email и искать по нему без учёта регистра — IVAN@EXAMPLE.COM и ivan@example.com должны находить одну запись.
Обычный индекс по email тут не поможет: запрос WHERE LOWER(email) = LOWER(...) не использует индекс по исходному полю и будет сканировать всю таблицу.
Есть два рабочих подхода.
Подход 1: расширение citext
citext — это специальный тип PostgreSQL, который при сравнении автоматически игнорирует регистр:
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'; -- найдёт
Плюс: код чище, индекс работает автоматически.
Минус: некоторые драйверы не умеют маппить citext и возвращают его как text — нужно проверять поведение конкретного драйвера.
Подход 2: text + функциональный индекс
Поле остаётся text, но индекс строится по результату lower():
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));
Тогда запрос нужно писать явно:
SELECT * FROM account WHERE lower(email) = lower('IVAN@EXAMPLE.COM');
Плюс: работает без расширений, портируемо.
Минус: каждый запрос обязан использовать lower() — если забыть, индекс не задействуется.
Кодировка UTF8
Кластер PostgreSQL имеет кодировку, которая задаётся при создании базы данных. Проверить текущую:
SHOW server_encoding; -- ожидаем UTF8
Если кодировка не UTF8 (например, SQL_ASCII или WIN1251), возникают проблемы с многоязычным контентом и эмодзи. Такое встречается на старых инсталляциях, созданных по устаревшим инструкциям.
UTF8 — единственная правильная кодировка для нового кластера.
TOAST: длинные строки хранятся автоматически
Если в таблице есть поле с длинным текстом — статья, описание товара, биография — не нужно выносить его в отдельную таблицу руками.
PostgreSQL автоматически делает это сам через механизм TOAST: значения длиннее ~2 КБ физически хранятся отдельно от основной строки таблицы. При запросе, который не обращается к большому полю, PostgreSQL его не читает — и это ускоряет работу.
CREATE TABLE article (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
slug text NOT NULL UNIQUE,
title text NOT NULL,
body text NOT NULL -- длинный текст — TOAST справится сам
);
Разделять на article + article_body имеет смысл, только если измерения показывают реальное узкое место. Без измерений — доверьтесь TOAST.
Коротко
- В PostgreSQL
text,varchar(n)иchar(n)хранятся одинаково — только семантика разная. varchar(255)в PostgreSQL — устаревшая привычка из MySQL/Oracle; по умолчанию пишитеtext.- Длину (
varchar(n)) ставьте, когда есть доменный стандарт: E.164, ISO 3166-1 и подобные. char(n)дополняет строку пробелами — используйте только для строго фиксированных кодов из стандартов.- Поиск без учёта регистра:
citextили функциональный индекс(lower(field)). - Простой
LOWER(field)вWHEREбез функционального индекса — сканирование всей таблицы. - Кодировка кластера должна быть UTF8.
- Длинные тексты TOAST выносит в отдельное хранилище автоматически — не делайте это вручную без измерений.
Что почитать дальше
- Числа и точность — bigint, numeric, float.
- Время и таймзоны — timestamptz.
- UUID и идентификаторы — тип
uuid, неchar(36). - JSONB — когда оправдан, когда нет — для структурированных данных.