sqlpostgresqlstringsunicode

ascii e chr no SQL: codigos e caracteres

ascii() retorna o ponto de codigo do primeiro caractere e chr() monta um caractere a partir de um codigo; no PostgreSQL ambos lidam com Unicode completo, enquanto MySQL e ClickHouse diferem.

5 min de leituraReferênciasql · postgresql · strings · unicode · mysql · clickhouse

O par ascii e chr converte um caractere no seu codigo numerico e vice-versa. Parece trivial, mas sobre essas duas funcoes se apoiam a geracao de rotulos de letras, a insercao de caracteres de controle como a quebra de linha e o trabalho cuidadoso com Unicode. Vamos ver as duas no PostgreSQL e apontar onde MySQL e ClickHouse se comportam diferente.

O fato central a fixar desde o inicio: em um banco com codificacao UTF-8 ascii retorna o ponto de codigo Unicode completo do primeiro caractere, nao o valor de um byte, portanto e a inversa exata de chr. E isso que diferencia o PostgreSQL do MySQL, cuja funcao homonima ASCII() olha apenas o primeiro byte. Trabalhar com codigos de caracteres brilha ao diagnosticar dados invisiveis: tabulacoes, quebras de linha, caracteres de controle e importacoes suspeitas onde o olho nao distingue um espaco de um espaco inquebravel. Uma consulta com ascii(col) mostra o codigo real de um caractere na propria expressao, nao como o terminal o desenha.

ascii: o codigo do primeiro caractere

ascii(text) retorna o ponto de codigo do primeiro caractere de uma string e ignora o resto. Para uma string vazia o resultado e zero. Em um banco UTF-8 este e um ponto de codigo Unicode real, nao um numero de byte.

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

Como apenas o primeiro caractere conta, a funcao serve para classificar pela inicial. Agrupe funcionarios pelo codigo em maiuscula da primeira letra:

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

A jogada pratica central e comparar codigos em vez de letras. Por exemplo, selecione usuarios cujo nome comeca com uma letra latina entre A e M:

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

chr: o caractere para um codigo

chr(integer) faz o inverso: retorna o caractere para um ponto de codigo dado. No PostgreSQL e um ponto de codigo Unicode completo, nao apenas ASCII: chr(233) da um e acentuado, chr(8364) o sinal do euro, e qualquer codigo acima de 127 e codificado como UTF-8. O argumento 0 nao e permitido, e um valor grande demais lanca um erro em vez de retornar um caractere de lixo.

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

O idioma chr(ascii(x) + n) desloca uma letra pelo alfabeto somando um deslocamento ao seu codigo. Gere rotulos A, B, C, ... para numerar linhas em um relatorio:

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

A mesma abordagem constroi codigos de letras para departamentos ou identificadores curtos sem uma tabela de consulta a parte. Basta manter a aritmetica dentro da faixa pretendida: depois de ascii('Z') vem nao letras, mas os sinais [, \, ], entao um contador de A a Z precisa reiniciar ou passar para rotulos de duas letras na mao.

Caracteres de controle: quebras de linha e tabulacoes

chr e a forma padrao de produzir caracteres invisiveis que sao incomodos ou impossiveis de digitar dentro de um literal de string. Os mais comuns sao a quebra de linha chr(10) e a tabulacao chr(9); cole-os aos seus dados com || e voce monta um valor de varias linhas na propria consulta.

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

Monte uma exportacao tipo CSV de pedidos em que as linhas sao separadas por quebra de linha e os campos por tabulacao:

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';

Codigos uteis que vale a pena memorizar:

  • chr(9) e uma tabulacao horizontal;
  • chr(10) e uma quebra de linha (LF);
  • chr(13) e um retorno de carro (CR);
  • chr(13) || chr(10) e o par CRLF para compatibilidade com Windows.

Pegadinha: no PostgreSQL o par e simetrico, no MySQL nao

Ao contrario de um mito comum, no PostgreSQL ascii nao corta um caractere ate um byte. Em um banco com codificacao UTF-8 ambas as funcoes operam sobre pontos de codigo Unicode completos e sao inversas mutuas: para um e acentuado ascii retorna 233 e chr(233) reconstroi a mesma letra. A ida e volta chr(ascii(x)) e correta nao apenas para ASCII, mas para qualquer caractere Unicode, desde que a codificacao do banco seja UTF-8. A armadilha aparece ao levar o codigo para outro engine, onde uma funcao com o mesmo nome significa algo bem diferente.

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

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

A armadilha de verdade se esconde justamente aqui, nas diferencas entre engines:

  • No MySQL a funcao se chama ASCII() e retorna o valor do primeiro byte, nao o ponto de codigo — justamente o corte que o PostgreSQL nao faz. Para o ponto Unicode completo use ORD(). A inversa e CHAR(N USING utf8mb4), e sem charset CHAR() retorna uma string binaria.
  • No ClickHouse existe char() (aceita varios codigos de uma vez) e ajudantes de ponto de codigo como ord sobre UTF-8; os nomes e o comportamento diferem do Postgres.

A pegadinha com ascii e chr quase nunca esta na funcao do PostgreSQL em si, mas nos valores extremos e na passagem para outro engine: o primeiro caractere versus a string inteira, o Unicode multibyte e as diferentes expectativas sobre a codificacao. Antes de migrar entre PostgreSQL, MySQL e ClickHouse, rode uma tabela pequena com NULL, uma string vazia e alguns caracteres fora do ASCII — digamos cirilico e um emoji. Os engines costumam concordar nas letras latinas e divergir justamente onde os dados de teste sao limpos demais: codigo como ascii(name) vai retornar em silencio numeros diferentes no PostgreSQL e no MySQL para a mesma letra é.

Outro ponto pratico sao os indices. A expressao ascii(upper(left(name, 1))) em um WHERE e calculada por linha, e um B-tree comum sobre name nao ajuda. Se esse filtro esta em um caminho quente, crie um indice de expressao para ele ou leve o primeiro codigo para uma coluna gerada. Em uma amostra pequena a diferenca e invisivel, mas em tabelas grandes uma varredura sequencial sobre ascii(...) sai caro.

A conclusao pratica: dentro do PostgreSQL, ascii e chr sao simetricas e trabalham com Unicode completo, entao para gerar rotulos latinos e caracteres de controle sao confiaveis. O perigo nao e o par em si, mas o nome: ASCII significa "primeiro byte" no MySQL e "ponto de codigo Unicode" no PostgreSQL, e e preciso ter essa diferenca em mente ao portar consultas.

Pratique com exercícios reais

Resolva exercícios no treinador de SQL com correção instantânea e dicas.

Abrir o treinador