Есть такой старый 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_size from s_v50 union all select 'varchar(1000)' as type, avg(pg_column_size(value)) as avg_column_size from s_v1000 union all select 'text' as type, avg(pg_column_size(value)) as avg_column_size from s_text;
pg_column_size показывает размер конкретного значения в байтах. Не размер объявления колонки, не потенциальный максимум, а именно то, сколько занимает значение.
Результат будет таким:
type | avg_column_size ---------------+---------------- varchar(50) | 21 varchar(1000) | 21 text | 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_size from pg_class where 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 kB s_v1000 | 5128 kB | 5128 kB s_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_size from pg_index where indrelid in ( 's_v50'::regclass, 's_v1000'::regclass, 's_text'::regclass );
Результат следующий:
index_name | index_size ------------------+------------ s_v50_value_idx | 712 kB s_v1000_value_idx | 712 kB s_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)
и кладёте туда:
new paid failed
то вы, скорее всего, не оптимизируете хранение. Вы просто заводите склад пробелов.
Документация 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.
Комментарии (13)

ShashkovS
07.06.2026 02:42Жесть, LLM-стиль сквозит из каждого третьего предложения. Ну, ещё объяснения как для тупых: не влияет. Смотри, проверим, что не влияет. Вот видишь, не влияет. Формула: не влияет. Так что не влияет.

shurutov
07.06.2026 02:42Разница в поведении между
varchar(N)иtextдавно уже расписана: https://ru-postgres.livejournal.com/65930.html
alwaysdeterminated Автор
07.06.2026 02:42Да, я ведь тут и не претендую на открытие. Скорее просто захотелось разобрать миф, о котором я все ещё иногда слышу — так статья и появилась.

LaserPro
07.06.2026 02:42Для MySQL, немного сложнее: да, varchar не резервирует место для хранения данных, но есть лимит на общую длину строки в БД (т.е. всех колонок вместе, не созраненной строки в одной ячейке). Что-то около ~64K bytes. вот тут уже учитывается сколько у вас "больших" varchar колонок - при добавлении очередной varchar(255) база скажет row size limit exceed (даже на пустой таблице, без данных)

alwaysdeterminated Автор
07.06.2026 02:42Да, отличный нюанс, спасибо.
Еще один хороший пример того, почему казалось бы схожую функциональность не стоит воспринимать одинаково в разных СУБД.

Akina
07.06.2026 02:42вот тут уже учитывается сколько у вас "больших" varchar колонок
Угу. А заодно - какой у них CHARSET (ведь нужно указанный макс. размер, который в символах, пересчитать в макс. размер хранения, который в байтах).
А ещё в MySQL, в отличие от PostgreSQL, нельзя указать просто VARCHAR, без указания максимального размера помещаемых данных.
база скажет row size limit exceed (даже на пустой таблице, без данных)
Именно поэтому использование xTEXT(size) или xTEXT+CHECK(size) в MySQL предпочтительнее VARCHAR(size) - и оно работает ровно так, как описано в статье.

ptr128
07.06.2026 02:42Разница между VARCHAR и TEXT начинается с того, что исторически это разные oid в pg_type, что порой приводит к странным последствиям. Особенно в некоторых расширениях.
Поэтому, рекомендую вместо
some_string textписать просто
some_string varcharбез скобок и максимальной длины.
kemsky
Даже если это никак не отражается на хранении, то для оптимизатора эта информация важна - для оценки ресурсов необходимых для выполнения запроса.
"Да, иногда это полезно, но иногда это лишь случайный забор посреди дороги." - догадаемся кто писал это.
alwaysdeterminated Автор
Да, только вот это уже другой тезис. Статья не говорит, что ограничения бесполезны. Прежде всего речь о хранении данных
Если же говорить про оптимизатор, то он берет реальные оценки ширины из статистики по фактическим данным, а не из N.
В pg_stats для этого есть avg_width.
То есть если в
varchar(1000)лежат строки по 20 байт, планировщик после ANALYZE будет видеть примерно эти 20 байт, а не паниковать из-за огромного N, это было бы довольно тупо со стороны СУБД.wadeg
В mssql - да. В pg это так и не осилили.
kemsky
я про мс и писал, она тут тоже зачем-то упоминается, хотя в ней все по-другому. вобще пересмотрел по диагонали - у статьи нет четкого посыла, надерганы какие-то факты, нюансы хранения не рассмотрены, полно странных сравнений и метафор от ллм (отдельная взрослая тема ага).
ptr128
В PostgreSQL оптимизатор не использует объявленную максимальную длину строки, опираясь в этом вопросе на статистику.