sqlpostgresqlstringsutf-8

SQL char_length: Counting Characters, Not Bytes

char_length returns the number of characters in a string, octet_length its size in bytes, and LENGTH counts characters or bytes depending on PostgreSQL, MySQL, or ClickHouse.

4 min lukuaikaReferencesql · postgresql · strings · utf-8 · mysql · clickhouse
Tämä artikkeli on tällä hetkellä venäjäksi — englanninkielinen käännös on työn alla.

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:

  • латиница и цифры: 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;   -- byte budget for an index prefix

На практике держите в голове простое правило: лимиты для людей (имя, заголовок, комментарий) считайте символами через 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, и подвох остался бы незаметным.

-- 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 больше единицы. Для большинства задач валидации это допустимо, но если вы режете отображаемые позиции, считать «видимые» символы придётся отдельной логикой на стороне приложения.

Harjoittele oikeilla tehtävillä

Ratkaise tehtäviä SQL-harjoittelussa välittömällä arvioinnilla ja vihjeillä.

Avaa harjoittelu