Dan l-artiklu bħalissa huwa bir-Russu — it-traduzzjoni bl-Ingliż għaddejja.
char_length (синоним character_length) возвращает число символов в строке, и именно эту функцию нужно брать, когда вы валидируете имя пользователя, режете заголовок под лимит или сравниваете длину с ограничением VARCHAR(n). В UTF-8 один символ занимает от одного до четырёх байт, поэтому подсчёт через байтовые функции расходится с числом символов на любой строке вне ASCII: акцент над буквой, кириллица, иероглиф, эмодзи. Ниже разберём, чем char_length отличается от octet_length и почему LENGTH нельзя использовать для подсчёта символов в переносимом коде.
Разница между символами и байтами в char_length — не теория, а контракт данных. Лимит на имя в форме, длина email под индекс, ширина колонки VARCHAR и байтовый бюджет ключа во внешней системе измеряются разными единицами, и если перепутать их в одном правиле, ошибка проявится только на акцентах и иероглифах. Поэтому в запросе стоит явно показывать, что именно мы считаем: char_length для человеческого лимита или octet_length для машинного. Если одна и та же проверка длины повторяется в отчётах, тестах качества и миграциях, вынесите char_length в CHECK-ограничение, generated-колонку или view, чтобы правило было одно и его можно было протестировать.
char_length считает символы
В Postgres char_length (синоним character_length) возвращает число символов в строке независимо от того, сколько байт они занимают в кодировке. На чистой латинице результат очевиден и совпадает с тем, что вы видите глазами:
SELECT char_length('acai');
SELECT char_length('cafe');
SELECT char_length('Moscow');
Расхождение появляется, как только в строке есть символ вне ASCII. Возьмём букву со знаком ударения, записанную как a плюс комбинируемый акцент U&'a\0301'. Глаз видит один знак, но в UTF-8 он кодируется двумя байтами, и char_length с octet_length дают разные числа:
SELECT char_length(U&'a\0301') AS chars,
octet_length(U&'a\0301') AS bytes;
Здесь char_length отвечает на вопрос «сколько символов увидит человек» и возвращает 1, а octet_length — «сколько места это займёт в памяти или на диске» и возвращает 2. Та же логика работает с substring и left: они оперируют символами, поэтому обрезка строки по символам никогда не разрежет многобайтовый символ пополам, тогда как ручная резка по байтам это сделает и породит битый текст.
octet_length и почему байты не равны символам
octet_length возвращает размер строки в байтах. Для чистого ASCII он совпадает с char_length, но для акцентированных и иероглифических символов расходится. Запрос ниже кладёт обе меры рядом, чтобы расхождение было видно прямо в выдаче:
SELECT name,
char_length(name) AS chars,
octet_length(name) AS bytes
FROM users
WHERE country IN ('BR', 'JP', 'RU');
Ориентир по UTF-8:
- латиница и цифры: 1 байт на символ;
- акценты и кириллица: обычно 2 байта;
- большинство CJK-иероглифов: 3 байта;
- эмодзи и редкие знаки: 4 байта.
Поэтому строка из четырёх иероглифов даст char_length = 4, но octet_length = 12. Эта разница напрямую бьёт по ограничениям колонок: если колонка объявлена как VARCHAR(10), в Postgres лимит считается по символам, и такая строка пройдёт, а вот вручную выставленный байтовый лимит в 10 байт она превысит и будет отклонена.
Валидация длины
Для бизнес-правил почти всегда нужны символы, поэтому в фильтрах и проверках берём char_length. Найдём пользователей со слишком коротким именем и подозрительно длинным email:
SELECT id, name, email
FROM users
WHERE char_length(name) < 2
OR char_length(email) > 254;
То же правило длины в CHECK-ограничении на char_length, чтобы слишком короткое или длинное имя вообще не попало в таблицу:
ALTER TABLE employees
ADD CONSTRAINT name_len_chk
CHECK (char_length(name) BETWEEN 2 AND 100);
А octet_length пригодится, когда есть жёсткий технический предел в байтах — например, размер ключа во внешней системе или префикса индекса:
SELECT id, email
FROM users
WHERE octet_length(email) > 191;
На практике держите в голове простое правило: лимиты для людей (имя, заголовок, комментарий) считайте символами через char_length, а лимиты для машин (ключ, индекс, поле фиксированного размера во внешнем формате) — байтами через octet_length. Смешивать эти две меры в одном правиле — верный способ получить ошибку именно на акцентах и иероглифах, которую не видно на тестовых данных из латиницы, где char_length и octet_length совпадают.
Ловушка: LENGTH ведёт себя по-разному в разных СУБД
Главный подвох — функция LENGTH: она есть везде, но считает в каждом движке своё.
- В PostgreSQL
length(text) считает символы (как char_length), но length(bytea) считает байты.
- В MySQL
LENGTH() считает байты, а символы считает CHAR_LENGTH().
- В ClickHouse
length() для String считает байты, а символы возвращает lengthUTF8().
В примерах ниже строка 'cafe' записана с акцентом над последней буквой (café), поэтому в UTF-8 она занимает 5 байт при 4 символах. Байтовая функция возвращает 5, а символьная — 4; на чистом ASCII-слове cafe обе вернули бы 4, и подвох остался бы незаметным.
SELECT LENGTH('cafe'),
CHAR_LENGTH('cafe');
SELECT length('cafe'),
lengthUTF8('cafe');
Из-за этого расхождения проверка длины, написанная в Postgres через length(), при переносе в MySQL или ClickHouse молча превращается из символьной в байтовую: на слове café с акцентом length() даст 4 в Postgres и 5 в MySQL и ClickHouse. На латинице тесты пройдут, а первая же строка с акцентом или кириллицей перешагнёт лимит, который раньше держался. Поэтому при миграции прогоняйте проверки длины на строках с NULL, пустым значением, акцентом, кириллицей и эмодзи, а не только на ASCII-данных, где length, char_length и octet_length совпадают.
Тот же риск касается производительности: char_length(col) в WHERE или CHECK — это функция над колонкой, и она может закрыть планировщику путь к обычному индексу по этой колонке. Если правило длины применяется к большой таблице, посмотрите план выполнения и при необходимости заведите индекс по выражению или храните длину в generated-колонке, а не пересчитывайте её на каждом запросе.
Вывод простой: не полагайтесь на LENGTH для подсчёта символов в переносимом коде. Пишите char_length явно в Postgres, CHAR_LENGTH в MySQL и lengthUTF8 в ClickHouse — тогда правила валидации не поплывут при миграции данных с акцентами и иероглифами. И помните про ещё одну тонкость: char_length считает кодовые точки Unicode, а не «графемы», как их видит человек. Эмодзи с модификатором цвета кожи или флаг из двух символов могут дать char_length больше единицы. Для большинства задач валидации это допустимо, но если вы режете отображаемые позиции, считать «видимые» символы придётся отдельной логикой на стороне приложения.
char_length(синонимcharacter_length) возвращает число символов в строке, и именно эту функцию нужно брать, когда вы валидируете имя пользователя, режете заголовок под лимит или сравниваете длину с ограничениемVARCHAR(n). В UTF-8 один символ занимает от одного до четырёх байт, поэтому подсчёт через байтовые функции расходится с числом символов на любой строке вне ASCII: акцент над буквой, кириллица, иероглиф, эмодзи. Ниже разберём, чемchar_lengthотличается отoctet_lengthи почемуLENGTHнельзя использовать для подсчёта символов в переносимом коде.Разница между символами и байтами в
char_length— не теория, а контракт данных. Лимит на имя в форме, длина email под индекс, ширина колонкиVARCHARи байтовый бюджет ключа во внешней системе измеряются разными единицами, и если перепутать их в одном правиле, ошибка проявится только на акцентах и иероглифах. Поэтому в запросе стоит явно показывать, что именно мы считаем:char_lengthдля человеческого лимита илиoctet_lengthдля машинного. Если одна и та же проверка длины повторяется в отчётах, тестах качества и миграциях, вынеситеchar_lengthвCHECK-ограничение, generated-колонку или view, чтобы правило было одно и его можно было протестировать.char_length считает символы
В Postgres
char_length(синонимcharacter_length) возвращает число символов в строке независимо от того, сколько байт они занимают в кодировке. На чистой латинице результат очевиден и совпадает с тем, что вы видите глазами:SELECT char_length('acai'); -- 4 SELECT char_length('cafe'); -- 4 SELECT char_length('Moscow'); -- 6Расхождение появляется, как только в строке есть символ вне ASCII. Возьмём букву со знаком ударения, записанную как
aплюс комбинируемый акцентU&'a\0301'. Глаз видит один знак, но в UTF-8 он кодируется двумя байтами, иchar_lengthсoctet_lengthдают разные числа:-- one visible character, two bytes in UTF-8 SELECT char_length(U&'a\0301') AS chars, -- 1 octet_length(U&'a\0301') AS bytes; -- 2Здесь
char_lengthотвечает на вопрос «сколько символов увидит человек» и возвращает1, аoctet_length— «сколько места это займёт в памяти или на диске» и возвращает2. Та же логика работает сsubstringиleft: они оперируют символами, поэтому обрезка строки по символам никогда не разрежет многобайтовый символ пополам, тогда как ручная резка по байтам это сделает и породит битый текст.octet_length и почему байты не равны символам
octet_lengthвозвращает размер строки в байтах. Для чистого ASCII он совпадает сchar_length, но для акцентированных и иероглифических символов расходится. Запрос ниже кладёт обе меры рядом, чтобы расхождение было видно прямо в выдаче:SELECT name, char_length(name) AS chars, octet_length(name) AS bytes FROM users WHERE country IN ('BR', 'JP', 'RU');Ориентир по UTF-8:
Поэтому строка из четырёх иероглифов даст
char_length = 4, ноoctet_length = 12. Эта разница напрямую бьёт по ограничениям колонок: если колонка объявлена какVARCHAR(10), в Postgres лимит считается по символам, и такая строка пройдёт, а вот вручную выставленный байтовый лимит в 10 байт она превысит и будет отклонена.Валидация длины
Для бизнес-правил почти всегда нужны символы, поэтому в фильтрах и проверках берём
char_length. Найдём пользователей со слишком коротким именем и подозрительно длинным email:SELECT id, name, email FROM users WHERE char_length(name) < 2 OR char_length(email) > 254;То же правило длины в
CHECK-ограничении наchar_length, чтобы слишком короткое или длинное имя вообще не попало в таблицу:ALTER TABLE employees ADD CONSTRAINT name_len_chk CHECK (char_length(name) BETWEEN 2 AND 100);А
octet_lengthпригодится, когда есть жёсткий технический предел в байтах — например, размер ключа во внешней системе или префикса индекса:SELECT id, email FROM users WHERE octet_length(email) > 191; -- byte budget for an index prefixНа практике держите в голове простое правило: лимиты для людей (имя, заголовок, комментарий) считайте символами через
char_length, а лимиты для машин (ключ, индекс, поле фиксированного размера во внешнем формате) — байтами черезoctet_length. Смешивать эти две меры в одном правиле — верный способ получить ошибку именно на акцентах и иероглифах, которую не видно на тестовых данных из латиницы, гдеchar_lengthиoctet_lengthсовпадают.Ловушка: LENGTH ведёт себя по-разному в разных СУБД
Главный подвох — функция
LENGTH: она есть везде, но считает в каждом движке своё.length(text)считает символы (какchar_length), ноlength(bytea)считает байты.LENGTH()считает байты, а символы считаетCHAR_LENGTH().length()дляStringсчитает байты, а символы возвращаетlengthUTF8().В примерах ниже строка
'cafe'записана с акцентом над последней буквой (café), поэтому в UTF-8 она занимает 5 байт при 4 символах. Байтовая функция возвращает5, а символьная —4; на чистом ASCII-словеcafeобе вернули бы4, и подвох остался бы незаметным.-- MySQL: byte count vs character count SELECT LENGTH('cafe'), -- 5 (e with accent = 2 bytes) CHAR_LENGTH('cafe'); -- 4-- ClickHouse: byte count vs character count SELECT length('cafe'), -- 5 lengthUTF8('cafe'); -- 4Из-за этого расхождения проверка длины, написанная в Postgres через
length(), при переносе в MySQL или ClickHouse молча превращается из символьной в байтовую: на слове café с акцентомlength()даст4в Postgres и5в MySQL и ClickHouse. На латинице тесты пройдут, а первая же строка с акцентом или кириллицей перешагнёт лимит, который раньше держался. Поэтому при миграции прогоняйте проверки длины на строках с NULL, пустым значением, акцентом, кириллицей и эмодзи, а не только на ASCII-данных, гдеlength,char_lengthиoctet_lengthсовпадают.Тот же риск касается производительности:
char_length(col)вWHEREилиCHECK— это функция над колонкой, и она может закрыть планировщику путь к обычному индексу по этой колонке. Если правило длины применяется к большой таблице, посмотрите план выполнения и при необходимости заведите индекс по выражению или храните длину в generated-колонке, а не пересчитывайте её на каждом запросе.Вывод простой: не полагайтесь на
LENGTHдля подсчёта символов в переносимом коде. Пишитеchar_lengthявно в Postgres,CHAR_LENGTHв MySQL иlengthUTF8в ClickHouse — тогда правила валидации не поплывут при миграции данных с акцентами и иероглифами. И помните про ещё одну тонкость:char_lengthсчитает кодовые точки Unicode, а не «графемы», как их видит человек. Эмодзи с модификатором цвета кожи или флаг из двух символов могут датьchar_lengthбольше единицы. Для большинства задач валидации это допустимо, но если вы режете отображаемые позиции, считать «видимые» символы придётся отдельной логикой на стороне приложения.