sqlpostgresqlstringsunicode

SQL ascii and chr: Code Points and Characters

ascii() returns the code point of the first character and chr() builds a character from a code; in PostgreSQL both handle full Unicode, while MySQL and ClickHouse differ.

4 min lugemistReferencesql · postgresql · strings · unicode · mysql · clickhouse
See artikkel on praegu venekeelne — ingliskeelne tõlge on töös.

Функция ascii в PostgreSQL возвращает числовой код первого символа строки, а chr делает обратное — строит символ по его коду. Эта пара нужна, когда вы генерируете буквенные метки вроде A, B, C для нумерации, вставляете в строку невидимые управляющие символы (перенос строки, табуляцию) или сравниваете символы по кодовым точкам, а не по виду. Разберём обе функции на PostgreSQL и отметим, где MySQL и ClickHouse ведут себя иначе.

Главное, что стоит держать в голове с самого начала: в базе с кодировкой UTF-8 ascii отдаёт не значение байта, а полную кодовую точку Unicode, и потому она симметрична с chr. Это отличает PostgreSQL от MySQL, где одноимённая ASCII() смотрит лишь на первый байт. Работа с кодами символов особенно полезна в диагностике невидимых данных: табуляций, переводов строк, управляющих знаков и подозрительных импортов, где глаз не отличает пробел от неразрывного пробела. Запрос с ascii(col) показывает фактический код символа прямо в выражении, а не то, как он отрисован в терминале. Если приём с ascii/chr повторяется в отчётах, проверках качества или миграциях, вынесите его в согласованный слой: view, generated column, CHECK либо staging-преобразование, чтобы правило можно было протестировать и переиспользовать.

ascii: код первого символа

ascii(text) возвращает кодовую точку первого символа строки и игнорирует всё остальное. Для пустой строки результат равен нулю. В базе UTF-8 это настоящая кодовая точка Unicode, а не номер байта.

SELECT ascii('A');        -- 65
SELECT ascii('a');        -- 97
SELECT ascii('Apple');    -- 65, only the first character matters
SELECT ascii('');         -- 0

То, что учитывается только первый символ, удобно для классификации по первой букве: например, разложить имена по алфавитным корзинам. Сгруппируем сотрудников по заглавному коду первого символа имени:

SELECT ascii(upper(left(name, 1))) AS first_code,
       count(*)                    AS people
FROM employees
GROUP BY first_code
ORDER BY first_code;

Главный практический приём — сравнивать коды, а не сами буквы: диапазон по ascii записывается одним BETWEEN. Например, отобрать пользователей, чьё имя начинается с латинской буквы A–M:

SELECT id, name
FROM users
WHERE ascii(upper(left(name, 1))) BETWEEN ascii('A') AND ascii('M');

chr: символ по коду

chr(integer) делает обратное — возвращает символ для заданной кодовой точки. В PostgreSQL это полноценная точка Unicode, а не только ASCII: chr(233) даст букву e с акцентом, chr(8364) — знак евро, а любые коды выше 127 кодируются в UTF-8. Аргумент 0 запрещён, и слишком большое значение вызовет ошибку, а не вернёт мусорный символ.

SELECT chr(65);     -- A
SELECT chr(97);     -- a
SELECT chr(8364);   -- the euro sign

Связка chr(ascii(x) + n) сдвигает букву по алфавиту, прибавляя смещение к коду. Сгенерируем метки A, B, C, ... для нумерации строк в отчёте:

SELECT chr(ascii('A') + (n - 1)) AS label
FROM generate_series(1, 5) AS g(n);
-- A, B, C, D, E

Тот же подход помогает строить буквенные коды отделов или короткие идентификаторы без отдельной таблицы-справочника. Главное — держать арифметику в пределах нужного диапазона: за ascii('Z') идут не буквы, а служебные символы [, \, ], так что счётчик A–Z придётся сбрасывать или переходить на двухбуквенные метки вручную.

Управляющие символы: перенос строки и табуляция

chr — стандартный способ получить невидимые символы, которые неудобно или невозможно вписать в строковый литерал прямо в коде SQL. Самые частые — перенос строки chr(10) и табуляция chr(9); склеив их с данными через ||, вы соберёте многострочное значение прямо в запросе.

SELECT u.name || chr(10) || u.email AS contact_card
FROM users AS u
WHERE u.id = 1;

Соберём CSV-подобную выгрузку заказов, где chr(10) разделяет строки, а chr(9) — поля внутри строки:

SELECT string_agg(
         o.id || chr(9) || o.amount || chr(9) || o.status,
         chr(10) ORDER BY o.id
       ) AS report
FROM orders AS o
WHERE o.status = 'paid';

Полезные коды, которые стоит помнить:

  • chr(9) — горизонтальная табуляция;
  • chr(10) — перевод строки (LF);
  • chr(13) — возврат каретки (CR);
  • chr(13) || chr(10) — пара CRLF для совместимости с Windows.

Ловушка: в PostgreSQL пара симметрична, в MySQL — нет

Вопреки распространённому мифу, в PostgreSQL ascii не усекает символ до байта. На базе с кодировкой UTF-8 обе функции работают с полными кодовыми точками Unicode и взаимно обратны: для буквы e с акцентом ascii вернёт 233, а chr(233) восстановит ту же букву. Round trip chr(ascii(x)) корректен не только для ASCII, но и для любого символа Unicode, пока кодировка базы — UTF-8. Подвох появляется при переносе кода в другую СУБД, где функция с тем же именем означает совсем другое.

-- round trip works for ASCII
SELECT chr(ascii('A'));   -- A

-- chr handles full Unicode code points directly
SELECT chr(233);          -- e with acute accent

Именно здесь, в различиях между движками, и прячется настоящая ловушка:

  • В MySQL функция называется ASCII() и возвращает значение первого байта, а не кодовую точку — то самое усечение, которого в PostgreSQL нет. Для полной точки Unicode в MySQL есть ORD(). Обратная функция — CHAR(N USING utf8mb4), причём без указания кодировки CHAR() возвращает бинарную строку.
  • В ClickHouse есть char() (принимает несколько кодов сразу) и для кодовой точки — функции вроде ord поверх UTF-8; имена и поведение отличаются от Postgres.

Подвох с ascii и chr почти всегда не в самой функции PostgreSQL, а в крайних значениях и в переносе на другой движок: первый символ против всей строки, многобайтный Unicode и разные ожидания от кодировки. Перед миграцией между PostgreSQL, MySQL и ClickHouse прогоните маленькую таблицу с NULL, пустой строкой и несколькими символами вне ASCII — например, кириллицей и эмодзи. Движки часто совпадают на латинице и расходятся ровно там, где тестовые данные слишком чистые: код вроде ascii(name) молча даст разные числа в PostgreSQL и MySQL для одной и той же буквы é.

Ещё один практический момент — индексы. Выражение ascii(upper(left(name, 1))) в WHERE считается по каждой строке, и обычный B-tree по name тут не поможет. Если такой фильтр стоит на горячем пути, сделайте под него выражательный индекс или вынесите первый код в generated column. На маленькой выборке разница незаметна, но на больших таблицах последовательное сканирование по ascii(...) обходится дорого.

Практический вывод: внутри PostgreSQL ascii и chr симметричны и работают с полным Unicode, поэтому для генерации латинских меток и управляющих символов они надёжны. Опасность не в самой паре, а в имени: ASCII в MySQL означает «первый байт», а в PostgreSQL — «кодовая точка Unicode», и при переносе запросов эту разницу нужно держать в голове.

Harjuta päris ülesannetel

Lahenda ülesandeid SQL-treeneris kohese hindamise ja vihjetega.

Ava treener