sqlpostgresqlstringsunicode

ASCII и CHR в PostgreSQL: как получить код символа и собрать символ по коду

ascii() возвращает кодовую точку первого символа, chr() собирает символ по коду; в PostgreSQL оба работают с полным Unicode, а в MySQL и ClickHouse поведение иное.

9 мин чтенияСправочникsql · postgresql · strings · unicode · mysql · clickhouse

В PostgreSQL есть удобная пара функций для работы с символами на уровне их числовых кодов:

  • ascii(text) возвращает код первого символа строки;
  • chr(integer) делает обратное — возвращает символ по его коду.

На первый взгляд функции кажутся редкими и «низкоуровневыми». Но на практике они часто помогают там, где обычного взгляда на строку уже не хватает: найти табуляцию, отличить обычный пробел от странного невидимого символа, собрать многострочный текст, сгенерировать буквенные метки вроде A, B, C или проверить, с какой буквы начинается значение.

Главная мысль: в PostgreSQL с кодировкой UTF-8 ascii работает не просто с байтом, а с кодовой точкой Unicode. Поэтому ascii и chr в такой базе хорошо сочетаются друг с другом: одна функция превращает символ в число, другая — число обратно в символ.

Зачем вообще нужны коды символов

Обычно мы работаем со строками как с текстом:

SELECT name
FROM users;

Но иногда важно увидеть не то, как символ выглядит, а что на самом деле лежит внутри строки.

Например, в данных может быть:

  • обычный пробел;
  • табуляция;
  • перенос строки;
  • неразрывный пробел;
  • символ из другой раскладки;
  • невидимый управляющий символ после импорта из Excel, CSV или внешней системы.

Глазом такие вещи часто не поймать. Строка выглядит нормально, а фильтр не срабатывает, сортировка странная, поиск не находит совпадение.

Вот тут и помогает ascii: она показывает числовой код первого символа строки.

Функция ASCII: код первого символа

Функция ascii(text) возвращает числовой код первого символа строки.

Важно: именно первого символа. Всё, что идёт дальше, функция игнорирует.

SELECT ascii('A');
SELECT ascii('a');
SELECT ascii('Apple');
SELECT ascii('');

Результат будет таким:

65
97
65
0

Разберём по шагам:

  • у символа A код 65;
  • у символа a код 97;
  • у строки Apple первый символ тоже A, поэтому результат снова 65;
  • для пустой строки PostgreSQL возвращает 0.

Это удобно, когда нас интересует не вся строка, а только её начало.

Например, можно посмотреть, с каких букв начинаются имена пользователей:

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

Что здесь происходит:

  1. left(name, 1) берёт первый символ имени.
  2. upper(...) приводит его к верхнему регистру.
  3. ascii(...) превращает символ в числовой код.
  4. count(*) считает, сколько пользователей попало в каждую группу.

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

Отбор по диапазону букв

Раз коды букв идут в понятном порядке, их можно сравнивать как числа.

Например, нужно выбрать пользователей, чьи имена начинаются с латинских букв от A до M:

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

Такой запрос читается почти как обычное условие:

возьми первую букву имени, приведи к верхнему регистру, получи её код и проверь, находится ли он между кодами A и M.

Это хороший приём для латинских букв, технических кодов, артикулов, коротких идентификаторов и других данных, где вы точно понимаете допустимый диапазон символов.

Но важно помнить: алфавиты, языки и Unicode сложнее, чем просто A–Z. Для полноценной языковой сортировки, поиска по локали и работы с разными регистрами лучше использовать обычные строковые инструменты PostgreSQL, а не пытаться вручную сравнивать коды символов.

Функция CHR: символ по коду

Функция chr(integer) делает обратную операцию: принимает числовой код и возвращает символ.

SELECT chr(65);
SELECT chr(97);
SELECT chr(8364);

Результат:

A
a
€

То есть:

  • chr(65) возвращает A;
  • chr(97) возвращает a;
  • chr(8364) возвращает знак евро.

В базе PostgreSQL с кодировкой UTF-8 chr умеет работать не только с базовыми ASCII-символами, но и с Unicode-кодами.

Например:

SELECT chr(233);

Вернёт символ с кодом 233 — латинскую букву с акцентом.

Есть и ограничения:

  • chr(0) в PostgreSQL запрещён;
  • слишком большое или недопустимое значение вызовет ошибку;
  • NULL на входе даст NULL на выходе.

То есть PostgreSQL не будет молча возвращать «какой-нибудь странный символ», если код некорректный. Он скорее честно остановит запрос ошибкой.

Генерация буквенных меток A, B, C

Один из самых понятных сценариев для ascii и chr — генерация буквенных меток.

Допустим, нужно получить метки A, B, C, D, E для отчёта:

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

Результат:

A
B
C
D
E

Логика простая:

  1. ascii('A') даёт код буквы A.
  2. n - 1 даёт смещение: 0, 1, 2, 3, 4.
  3. chr(...) превращает новый код обратно в символ.

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

Например, пронумеруем первые пять оплаченных заказов буквами:

WITH numbered_orders AS (
    SELECT
        id,
        amount,
        row_number() OVER (ORDER BY id) AS rn
    FROM orders
    WHERE status = 'paid'
)
SELECT
    chr(ascii('A') + (rn - 1)) AS label,
    id,
    amount
FROM numbered_orders
WHERE rn <= 5
ORDER BY rn;

Получится что-то вроде:

A | 101 | 1200
B | 102 | 850
C | 103 | 4300
D | 104 | 700
E | 105 | 1990

Для маленьких списков это удобно и наглядно.

Но есть важная граница: после Z идут уже не буквы.

SELECT chr(ascii('Z') + 1);

Результатом будет не новая буква, а символ после Z в таблице кодов.

Поэтому для меток длиннее 26 элементов нужно заранее продумать логику: например, переходить к AA, AB, AC или хранить метки в отдельной таблице.

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

chr особенно полезен для символов, которые сложно увидеть или неудобно писать прямо в строке.

Самые частые:

  • chr(9) — табуляция;
  • chr(10) — перевод строки;
  • chr(13) — возврат каретки;
  • chr(13) || chr(10) — перенос строки в формате CRLF, который часто встречается в Windows-файлах.

Например, соберём карточку пользователя в несколько строк:

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

Результат может выглядеть так:

Alex
alex@example.com

Снаружи это одно текстовое значение, но внутри него есть символ перевода строки.

Можно собрать и небольшой текстовый отчёт. Например, строки разделим переводом строки, а поля внутри строки — табуляцией:

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

Такой результат удобно скопировать в текстовый файл или быстро посмотреть в консоли.

Конечно, для настоящей CSV-выгрузки лучше использовать специальные инструменты экспорта. Но для диагностики, быстрых отчётов и служебных строк chr(9) и chr(10) очень выручают.

Как найти невидимые символы в данных

Представим ситуацию: в таблице есть имена, и часть из них почему-то не находится обычным поиском. Визуально всё похоже на нормальный текст, но что-то не так.

Можно посмотреть первый символ и его код:

SELECT
    name,
    left(name, 1) AS first_char,
    ascii(left(name, 1)) AS first_code
FROM users
ORDER BY first_code;

Если в начале строки случайно затесалась табуляция, перенос строки или другой управляющий символ, это станет заметно по коду.

Можно также проверить строки, которые начинаются не с латинской буквы:

SELECT
    id,
    name,
    ascii(upper(left(name, 1))) AS first_code
FROM users
WHERE ascii(upper(left(name, 1))) NOT BETWEEN ascii('A') AND ascii('Z');

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

Для проверки качества данных это очень полезный подход.

ASCII и Unicode в PostgreSQL

Название ascii может немного сбивать с толку. Кажется, будто функция должна работать только с классической ASCII-таблицей, где символы лежат в диапазоне от 0 до 127.

Но в PostgreSQL с кодировкой UTF-8 функция ascii возвращает кодовую точку Unicode первого символа.

То есть она не просто берёт первый байт строки. Она понимает символ целиком.

Например:

SELECT ascii('A');
SELECT ascii('€');

Первый запрос вернёт 65, а второй — Unicode-код символа евро.

Именно поэтому в PostgreSQL работает обратимость:

SELECT chr(ascii('A'));
SELECT chr(ascii('€'));

В обоих случаях мы берём символ, получаем его код, а потом по этому коду собираем символ обратно.

Это можно воспринимать как простой круг:

symbol -> ascii -> number -> chr -> symbol

Для UTF-8 базы PostgreSQL такая связка хорошо работает не только с латиницей.

Важная разница с MySQL

Самая неприятная ловушка появляется не внутри PostgreSQL, а при переносе запросов в другие СУБД.

В MySQL функция ASCII() ведёт себя иначе: она возвращает значение первого байта строки, а не полноценную Unicode-кодовую точку символа.

На обычной латинице разницы почти не видно:

SELECT ASCII('A');

И PostgreSQL, и MySQL дадут ожидаемый код 65.

Но на символах вне базового ASCII результат может отличаться. Например, буква с акцентом, кириллица или эмодзи в UTF-8 занимают несколько байт. PostgreSQL воспринимает первый символ как Unicode-символ, а MySQL ASCII() смотрит на первый байт.

Для полной Unicode-кодовой точки в MySQL используют другую функцию — ORD().

Обратная операция в MySQL тоже устроена иначе: для сборки символа по коду применяют CHAR(...), и иногда важно явно указывать кодировку, например CHAR(... USING utf8mb4).

Поэтому запрос, который отлично работает в PostgreSQL, не стоит без проверки переносить в MySQL:

SELECT chr(ascii('A'));

В PostgreSQL это естественная пара функций. В MySQL имена и поведение будут другими.

Что помнить про ClickHouse

В ClickHouse похожие задачи тоже решаются, но функции называются и работают не совсем так, как в PostgreSQL.

Там есть char, которая может принимать несколько числовых значений и собирать строку. Для работы с Unicode-кодовыми точками используются другие функции и подходы, связанные с UTF-8.

Главный практический совет простой: если переносите SQL между PostgreSQL, MySQL и ClickHouse, обязательно проверьте функции на тестовой таблице с разными символами.

Минимальный набор тестов:

  • латинская буква;
  • пустая строка;
  • NULL;
  • символ с акцентом;
  • кириллица;
  • знак евро;
  • эмодзи;
  • строка с табуляцией или переносом строки.

Проблемы почти никогда не проявляются на чистых данных вроде A, B, C. Они появляются там, где в строках есть Unicode, невидимые символы и реальные данные из внешних источников.

Производительность: почему обычный индекс может не помочь

Есть ещё один важный практический момент.

Если написать условие так:

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

PostgreSQL должен вычислить выражение ascii(upper(left(name, 1))) для строк таблицы.

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

Если таблица маленькая, вы этого даже не заметите. Но на большой таблице такой запрос может стать дорогим.

Что можно сделать:

  • создать выражательный индекс;
  • вынести первый код символа в generated column;
  • заранее нормализовать данные на этапе загрузки;
  • использовать отдельную витрину или view для аналитических проверок.

Например, выражательный индекс может выглядеть так:

CREATE INDEX users_first_name_code_idx
ON users ((ascii(upper(left(name, 1)))));

После этого PostgreSQL получает шанс использовать индекс именно под такое выражение.

Это не значит, что нужно индексировать каждую функцию в каждом запросе. Но если фильтр часто используется на большой таблице, его стоит вынести из разряда «просто удобная проверка» в нормальное правило данных.

Когда лучше вынести логику отдельно

Если вы один раз проверяете подозрительные данные, можно спокойно писать ascii прямо в запросе.

Но если такая логика повторяется в отчётах, проверках качества, миграциях или регулярных загрузках, лучше не копировать выражение по всему проекту.

Варианты:

  • сделать view с уже рассчитанным кодом первого символа;
  • добавить generated column;
  • добавить CHECK, если нужно запретить неправильные значения;
  • нормализовать строку в staging-таблице перед загрузкой в основную;
  • оформить правило в отдельном SQL-слое, который легко тестировать.

Например:

CREATE VIEW users_with_first_code AS
SELECT
    id,
    name,
    ascii(upper(left(name, 1))) AS first_code
FROM users;

Теперь аналитик или разработчик может использовать готовое поле:

SELECT id, name
FROM users_with_first_code
WHERE first_code BETWEEN ascii('A') AND ascii('M');

Так запросы становятся чище, а правило — заметнее и безопаснее.

Главное про ASCII и CHR

ascii(text) в PostgreSQL возвращает код первого символа строки. Если строка пустая, результатом будет 0. Остальная часть строки не учитывается.

chr(integer) возвращает символ по числовому коду. В PostgreSQL с UTF-8 это работает не только для базового ASCII, но и для Unicode-символов.

Связка chr(ascii(x)) в PostgreSQL с UTF-8 работает как понятное преобразование туда и обратно: символ → код → символ.

Самые частые практические сценарии:

  • сгенерировать буквенные метки A, B, C;
  • вставить перенос строки через chr(10);
  • вставить табуляцию через chr(9);
  • найти невидимые символы в данных;
  • проверить первый символ строки по числовому коду;
  • диагностировать странности после импорта.

Главная ловушка — переносимость. В PostgreSQL ascii работает с кодовой точкой Unicode, а в MySQL ASCII() смотрит на первый байт. Поэтому запросы с такими функциями нельзя бездумно переносить между PostgreSQL, MySQL и ClickHouse.

И главный практический вывод: ascii и chr — это не функции «для таблицы символов из учебника». Это полезные инструменты для диагностики строк, генерации служебного текста и аккуратной работы с невидимыми символами в реальных данных.

Закрепи на практике

Решай задачи в SQL-тренажёре с мгновенной проверкой и подсказками.

Открыть тренажёр