Este artículo está actualmente en ruso — la traducción está en curso.
Функция 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');
SELECT ascii('a');
SELECT ascii('Apple');
SELECT ascii('');
То, что учитывается только первый символ, удобно для классификации по первой букве: например, разложить имена по алфавитным корзинам. Сгруппируем сотрудников по заглавному коду первого символа имени:
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);
SELECT chr(97);
SELECT chr(8364);
Связка chr(ascii(x) + n) сдвигает букву по алфавиту, прибавляя смещение к коду. Сгенерируем метки A, B, C, ... для нумерации строк в отчёте:
SELECT chr(ascii('A') + (n - 1)) AS label
FROM generate_series(1, 5) AS g(n);
Тот же подход помогает строить буквенные коды отделов или короткие идентификаторы без отдельной таблицы-справочника. Главное — держать арифметику в пределах нужного диапазона: за 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. Подвох появляется при переносе кода в другую СУБД, где функция с тем же именем означает совсем другое.
SELECT chr(ascii('A'));
SELECT chr(233);
Именно здесь, в различиях между движками, и прячется настоящая ловушка:
- В 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», и при переносе запросов эту разницу нужно держать в голове.
Функция
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 tripchr(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Именно здесь, в различиях между движками, и прячется настоящая ловушка:
ASCII()и возвращает значение первого байта, а не кодовую точку — то самое усечение, которого в PostgreSQL нет. Для полной точки Unicode в MySQL естьORD(). Обратная функция —CHAR(N USING utf8mb4), причём без указания кодировкиCHAR()возвращает бинарную строку.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», и при переносе запросов эту разницу нужно держать в голове.