Есть такой старый SQL-рефлекс: создаёшь таблицу, доходишь до поля name, и рука почти сама пишет:
name varchar(255)
Не потому что кто-то в продукте сказал: “имя пользователя не длиннее 255 символов”. Не потому что это ограничение пришло из бизнес-логики, и даже не потому что кто-то посмотрел на реальные данные. Просто так часто делают.
А если хочется почувствовать себя чуть более аккуратным, то появляется вариант посерьёзнее:
name varchar(50)
Выглядит логично: varchar(50) меньше, чем varchar(255), а varchar(255) вроде бы экономнее, чем text. Значит, наверное, база будет хранить данные компактнее.
Но нет. В PostgreSQL varchar(50) не резервирует 50 символов, varchar(255) не резервирует 255 символов, а text не превращает каждую строку в бездонную дыру. База хранит значение, а не фантазию о том, каким это значение когда-нибудь станет.
VARCHAR(N) — это не размер ячейки
Главная путаница начинается с буквы N.
Многие воспринимают её так, будто она говорит базе: “под каждое значение заранее выдели место на N символов”. Но varchar(n) работает совсем не так.
В PostgreSQL это строка переменной длины с ограничением сверху. Если вы объявили:
title varchar(100)
это значит не “каждый title занимает место под 100 символов”, а всего лишь: “в эту колонку нельзя положить строку длиннее 100 символов”.
Если внутри лежит hello, то база хранит hello. Она не добивает строку пустотой до 50, 100 или 255 символов, не открывает отдельный склад под возможные будущие буквы и не ведёт себя как человек, который купил шкаф на четыре метра ради пары футболок.
В документации PostgreSQL это описано прямо: character varying(n) хранит строки длиной до n символов, text хранит строки произвольной длины, а varchar без ограничения принимает строки без заданного лимита. При этом короткие строки имеют небольшой служебный заголовок, а большие значения уже могут сжиматься или выноситься отдельно через TOAST.
И вот это “до n символов” — ключевое. До, а не ровно.
Есть небольшая тонкость: в PostgreSQL n — это символы, а не байты. Для UTF-8 это важно, потому что один символ не всегда равен одному байту.
Ещё есть стандартные нюансы SQL с явным приведением к varchar(n) и лишними пробелами, но они не меняют главный тезис: N — это верхняя граница, а не заранее выделенное место.
Маленький эксперимент, чтобы не спорить на ощущениях
Берём PostgreSQL и создаём три таблицы:
create table s_v50 ( value varchar(50));create table s_v1000 ( value varchar(1000));create table s_text ( value text);
Теперь кладём туда одинаковые данные.
Я выбрал положить 100.000 раз одну и ту же строку xxxxxxxxxxxxxxxxxxxx:
insert into s_v50(value)select repeat('x', 20)from generate_series(1, 100000);insert into s_v1000(value)select repeat('x', 20)from generate_series(1, 100000);insert into s_text(value)select repeat('x', 20)from generate_series(1, 100000);
Теперь проверим средний размер строки в каждой из таблиц:
select 'varchar(50)' as type, avg(pg_column_size(value)) as avg_column_sizefrom s_v50union allselect 'varchar(1000)' as type, avg(pg_column_size(value)) as avg_column_sizefrom s_v1000union allselect 'text' as type, avg(pg_column_size(value)) as avg_column_sizefrom s_text;
pg_column_size показывает размер конкретного значения в байтах. Не размер объявления колонки, не потенциальный максимум, а именно то, сколько занимает значение.
Результат будет таким:
type | avg_column_size---------------+----------------varchar(50) | 21varchar(1000) | 21text | 21
Конкретная цифра может немного отличаться в зависимости от данных и деталей хранения, но здесь важна не сама цифра, а то, что она одинаковая. Во всех трёх случаях внутри лежит одна и та же фактическая строка.
varchar(1000) не раздул её до тысячи символов, varchar(50) не сжал её магически сильнее, а text не вызвал великий и ужасный TOAST. Просто строка. Просто хранение.
А если смотреть на таблицу целиком?
Окей, размер одного значения — это понятно. Но можно посмотреть и на таблицу:
select relname, pg_size_pretty(pg_table_size(oid)) as table_size, pg_size_pretty(pg_total_relation_size(oid)) as total_sizefrom pg_classwhere relname in ('s_v50', 's_v1000', 's_text')order by relname;
Здесь pg_table_size показывает размер таблицы без индексов, но с TOAST, free space map и visibility map, а pg_total_relation_size — полный размер вместе с индексами. Для этого маленького эксперимента это скорее контрольная проверка, чем великая диагностика.
Результат:
relname | table_size | total_size --------+------------+------------s_text | 5128 kB | 5128 kBs_v1000 | 5128 kB | 5128 kBs_v50 | 5120 kB | 5120 kB
Как и ожидалось — размер таблиц никак не отличается.
То есть сам по себе переход с:
value varchar(1000)
на:
value varchar(50)
не уменьшит размер таблицы, если фактические значения остались теми же. Вы не сэкономили память, вы просто поставили новое правило на входе. Да, иногда это правило нужно, но это уже разговор про архитектуру и бизнес-требования, а не про оптимизацию хранения.
Индексы тоже не хранят ваши намерения
Следующий распространённый аргумент звучит примерно так: “ну ладно, в таблице не раздувается, но индекс на varchar(1000) точно будет больше, чем на varchar(50)”.
Тоже нет, по крайней мере если значения одинаковые.
Создадим индексы и сразу посмотрим их размер:
create index s_v50_value_idx on s_v50(value);create index s_v1000_value_idx on s_v1000(value);create index s_text_value_idx on s_text(value);select indexrelid::regclass as index_name, pg_size_pretty(pg_relation_size(indexrelid)) as index_sizefrom pg_indexwhere indrelid in ( 's_v50'::regclass, 's_v1000'::regclass, 's_text'::regclass);
Результат следующий:
index_name | index_size ------------------+------------s_v50_value_idx | 712 kBs_v1000_value_idx | 712 kBs_text_value_idx | 712 kB
Индекс работает с фактическими значениями. Он не думает: “о, тут колонка varchar(1000), надо на всякий случай хранить тысячу символов в каждом ключе”. У него нет такой странной тревожности.
Более того, у B-tree индексов в PostgreSQL есть отдельное ограничение: один index entry не может быть слишком большим — примерно больше трети страницы, уже после TOAST-сжатия, если оно применимо. Это описано в документации по B-tree индексам. Поэтому индексировать огромные строки “как есть” — отдельная взрослая тема. Иногда нужен другой индекс, иногда хеш выражения, иногда полнотекстовый поиск, иногда нормализация данных.
Тогда зачем вообще нужен VARCHAR(N)?
Вот тут начинается нормальный разговор.varchar(n) — это не оптимизация памяти, а ограничение.
username varchar(32)
означает, что в вашей системе username не может быть длиннее 32 символов. И это вполне нормально, если такое правило действительно есть.
external_id varchar(64)
нормально, если внешняя система гарантирует максимум 64 символа.
Проблема начинается вот здесь:
description varchar(255)
Потому что в реальных проектах это часто означает не “описание по бизнес-логике не может быть длиннее 255 символов”, а “я не знаю, какая тут должна быть длина, но 255 выглядит привычно, так все пишут”.
И вот это уже не архитектура. Это число, которое пережило старые базы, старые драйверы, старые ORM, старые туториалы и теперь гуляет по новым проектам как легаси-амулет.
255 — число, которое притворяется архитектурой.А вот CHAR(N) — другая история
С char(n) всё чуть опаснее. varchar(n) — переменная длина, а char(n), он же character(n), — фиксированная длина с дополнением пробелами.
То есть если вы пишете:
status char(50)
и кладёте туда:
newpaidfailed
то вы, скорее всего, не оптимизируете хранение. Вы просто заводите склад пробелов.
Документация PostgreSQL отдельно предупреждает, что у character(n) нет преимущества в производительности, а из-за padding он может занимать больше места. Иногда char(n) имеет смысл, например если у вас действительно фиксированный формат и вы понимаете, зачем это делаете. Но использовать его потому что “фиксированная длина звучит быстрее” — сомнительная идея.
На практике в PostgreSQL это обычно не быстрее, а просто более жёстко и менее удобно. Выглядит низкоуровнего, а пользы от этого часто никакой.
TEXT — не страшная бездна
Ещё один страх: “если поставить text, туда же можно засунуть что угодно”.
Да, можно. Но это не уникальное свойство text, ведь можно написать и так:
value varchar(1000000)
и тоже разрешить очень длинные значения.
Смысл не в том, что text опасен. Смысл в том, что у поля либо есть реальный лимит, либо его нет. Если поле — тело статьи, комментарий, описание, лог ошибки, markdown, ответ внешней системы или кусок импортированных данных, text часто честнее, чем случайный varchar(n).
Потому что n в таких местах обычно не защищает систему, а просто создаёт будущий баг. Пользователь написал нормальный комментарий — не влез. Внешний API вернул длинное сообщение об ошибке — обрезалось. Адрес оказался длиннее, чем хотелось разработчику в момент создания таблицы.
Если лимит нужен — пусть он будет честным
Я не за то, чтобы везде бездумно писать text, это лениво. Если у поля есть реальный лимит, его надо поставить, но важно и нужно понимать, откуда он взялся.
Иногда мне больше нравится вариант с text и явным CHECK, особенно когда ограничение — часть бизнес-логики:
create table users ( username text not null, constraint username_length_check check (char_length(username) <= 32));
CHECK constraints позволяют назвать правило и сделать его явным. Да, это длиннее, зато теперь ограничение видно как отдельный контракт. Его проще обсуждать, искать, менять и объяснять. Это уже не просто “тип такой”.
А если нужно проверять не символы, а байты, это тоже отдельная история. В PostgreSQL есть char_length, который считает символы, и octet_length, который считает байты. Для обычного пользовательского имени обычно нужны символы. Для сетевого протокола или внешнего бинарного лимита — возможно, байты.
TOAST приходит не к типу, а к размеру
Когда строка становится большой, PostgreSQL не пытается любой ценой запихнуть её прямо в обычную строку таблицы. Для этого есть TOAST.
Если совсем грубо: большие значения могут сжиматься и/или выноситься отдельно, а в основной строке остаётся ссылка на них. Это связано с тем, что обычная страница PostgreSQL имеет ограниченный размер, и огромные значения надо как-то хранить.
Но тут легко сделать неправильный вывод. TOAST — это не “режим для text” и не “то, что никогда не случается с varchar”. Он смотрит не на название типа, а на фактический размер значения. Подробности есть в документации PostgreSQL про TOAST.
Маленький text будет маленьким. Большой varchar(100000) может стать большим и уехать в TOAST. Тип сам по себе не отменяет физику хранения.
text. TOAST приходит к большим значениям.Но в MySQL же иначе?
Да. И вот тут стоит не тащить PostgreSQL-логику во все базы подряд.
В MySQL у VARCHAR есть свои детали хранения. Он хранит фактические данные плюс 1 или 2 байта под длину, в зависимости от максимального размера. Ещё есть charset, row size, индексы, старые ограничения, исторические нюансы. Это описано в документации MySQL по storage requirements и в разделе про CHAR и VARCHAR.
То есть объявленная длина там может влиять на большее количество вещей, чем в PostgreSQL. Но даже в MySQL varchar(1000) не означает, что строка abc внезапно занимает тысячу символов. Если лежит короткая строка, база хранит короткую строку плюс служебные данные, а не воображаемый максимум.
Поэтому нормальный подход такой: сначала смотрим конкретную СУБД, потом делаем выводы. Иначе получается классика: человек однажды услышал что-то про MySQL пятнадцать лет назад, а потом уверенно применяет это к PostgreSQL, SQL Server и всему, что понимает SQL.
А в SQL Server?
Там тоже свои нюансы. В SQL Server varchar(n) задаёт размер в байтах, а не “просто количество символов в человеческом смысле”. С Unicode, nvarchar, UTF-8 collations и varchar(max) разговор становится ещё веселее. У Microsoft это отдельно разобрано в документации по char и varchar.
Поэтому я бы не формулировал универсальное правило вида “всегда используйте text” или “всегда используйте varchar(n)”. Так обычно и рождаются плохие схемы: берётся нормальная мысль, вырывается из контекста и превращается в религию.
Более честная формула
VARCHAR(N) не экономит память. Он ограничивает данные. Да, иногда это полезно, но иногда это лишь случайный забор посреди дороги.
В PostgreSQL между text, varchar и varchar(n) нет той магической разницы в производительности, которую им часто приписывают.
То есть вопрос не в том, какой varchar быстрее. Вопрос в другом: какое ограничение здесь правда нужно?
Если ограничение настоящее — ставьте его. Если ограничения нет, то text честнее. Если хочется явно показать бизнес-правило, можно использовать text + CHECK. А если вы пишете varchar(255) просто потому, что “так принято”, лучше на секунду остановиться, потому что это не аргумент, а археология и технический долг.
Михаил Миронов, Табрика co-founder.
ссылка на оригинал статьи https://habr.com/ru/articles/1044476/