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 λεπτά ανάγνωσηςReferencesql · postgresql · strings · utf-8 · mysql · clickhouse
Αυτό το άρθρο είναι προς το παρόν στα ρωσικά — η αγγλική μετάφραση βρίσκεται σε εξέλιξη.

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

Εξασκηθείτε σε πραγματικές ασκήσεις

Λύστε ασκήσεις στην εξάσκηση SQL με άμεση βαθμολόγηση και υποδείξεις.

Άνοιγμα εξάσκησης