Nota: Este guia aborda conceitos universais de charset que se aplicam a MS SQL Server, PostgreSQL e Firebird. Embora os exemplos práticos sejam baseados no Firebird, os princípios fundamentais são os mesmos para todos os SGBDs mencionados, com poucas ou nenhuma diferença na forma como os charsets funcionam.
Por que essa pergunta é importante?
Se você usa o Windows, você deve estar se perguntando porque essa pergunta é importante. Por gentileza abra o cmd e execute comigo o comando chcp, ele retorna o código de página de caracteres que está em uso no momento. Ele retornará o código de página 850.
Vamos no Google e pesquisar esta página com os termos "Code page 850" e seremos encaminhados para Wikipedia:
https://en.wikipedia.org/wiki/Code_page_850
E então descobrimos que estamos usando o ISO8859_1 também conhecido como "Latin1". Então, toda vez que nos referimos ao ISO8859_1, vamos chamar de Latin1, ok?
Histórico: ISO8859_1(Latin1), WIN1252 e Unicode
Se você chegou a conhecer o MS-DOS deve se lembrar do comando chcp que usávamos no autoexec.bat para tornar possível ver as acentuações na tela. Então desde os primórdios do MS-DOS usamos Latin1 (ou ISO8859_1) para tornar possível ver nossas acentuações no terminal. Latin1 (ou ISO8859_1) leva o prefixo "iso" porque foi aprovado pelo comitê internacional de mesmo nome: International Organization for Standardization (Organização Internacional de Normalização) o equivalente a nossa ABNT só que internacional.
Mas uma coisa é o terminal, algo considerado legado por alguns para rodar aplicações. Quando o Windows surgiu com uma interface gráfica, a Microsoft resolveu criar um novo código de página chamado "Win1252" por isso tem o "Win" no nome, foi feito exclusivamente para o ambiente Windows e teve a aprovação da ANSI, a ABNT americana.
Por que criar seu próprio padrão ao invés de continuar usando o ISO8859_1 (Latin1)? Essa é uma pergunta difícil de responder, o ISO8859_1 foi criado inicialmente pela Apple e depois aprovado pela ISO e naquela época a Microsoft gostava de ditar os padrões da indústria e talvez por isso tenham criado o Win1252. Ele deveria ser desnecessário já que teve como base o próprio ISO8859_1 e ambos com poucas exceções são cambiáveis entre si. Pessoalmente, só conheço duas diferenças entre ambos, o Win1252 possui um travessão longo que é diferente do traço normal e um jogo de aspas com serifa (curva).
O problema da fragmentação no Windows
Ter um padrão de geração de caracteres diferentes só criou problemas para a Microsoft, seu terminal (cmd) é Latin1, a interface gráfica é Win1252, o sistema de arquivos é UNICODE. A pouco tempo atrás, se você usasse um bloco de notas para editar um arquivo php ou html tinha boas chances de ter o seu trabalho perdido. Sistemas como o Linux são tudo UNICODE e normalmente não é preciso lidar com conversões entre código de páginas diferentes.
Um outro problema como o ambiente Windows é que ele não é mais tão universal como antes, hoje as aplicações estão em nuvem e a maioria dos serviços de banco de dados são hospedados em computadores que não rodam Windows. Se você hospedar dados num servidor Windows armazenando dados no formato WIN1252 provavelmente teria de pensar o que aconteceria se resolvesse hospedar um banco de dados num sistema operacional Linux que segue apenas padrões internacionais ISO onde o WIN1252 não existe. Você talvez tenha de lidar com problemas de transliteração de caracteres.
Recomendação: ISO8859_1 para portabilidade
Então, o desejável se queremos uma aplicação ou banco de dados que seja portável entre sistemas operacionais é desistir do WIN1252 e em seu lugar optar pelo ISO8859_1 (latin1). Agora, daqui em diante vou falar apenas do Latin1 (ISO8859_1) e o padrão Unicode. Mas ao me referir ao Latin1, lembre-se que também é aplicável ao WIN1252.
Unicode: a solução moderna
O UNICODE veio para resolver alguns problemas. Há uma ótima palestra Firebird Conference 2011 Luxembourg onde o discursante falou muito sobre charsets, na sua palestra intitulada "Character Sets and Unicode in Firebird" e citou as vantagens:
Referência: https://www.firebirdsql.org/file/community/ppts/fbcon11/FbCon2011-Charsets-Heymann.pdf
- Um único conjunto de caracteres para todos os idiomas / scripts
- Sem sobreposições de código, não é necessário um código arbitrário para corrigir incongruências
- Independente de hardware e sistema operacional
Nota: Embora a referência seja de uma palestra sobre Firebird, os conceitos de Unicode são universais e se aplicam igualmente ao MS SQL Server, PostgreSQL e outros SGBDs modernos.
Testando no terminal do Windows
Vamos comprovar que estamos usando o latin1, vamos agora mudar para win1252:
chcp 1252
echo linha #1: teste com com página latin1(iso8859_1)>teste.txt
echo linha #2: atenção, minha acentuação é inoqüa>>teste.txt
hexeditor teste.txt
Parece tudo certo, né? Mas vamos mudar agora o código da página para utf:
chcp 65001
echo linha #3: teste com com página unicode>>teste.txt
echo linha #4: atenção, minha acentuação é inoqüa>>teste.txt
hexeditor teste.txt
Agora temos duas linhas iniciais exibidas corretamente, mas as duas últimas erroneamente, por quê? Porque ao iniciar o arquivo pela primeira vez, não há indicativos de codepage dentro do arquivo e então o Windows assume que é Ansi.
Vamos criar as mesmas 4 linhas no Notepad++, note que o mesmo já é criado em UTF8, mas se olharmos no bloco de notas, notaremos que nenhuma das linhas parece correta porque nem Latin1 e nem UTF são compatíveis com o WIN1252 (Ansi).
Se usarmos o Notepad++ e convertermos o arquivo para UTF-8 (Formatar → Codificação UTF-8 ou Europa Ocidental) as duas primeiras ou as duas últimas linhas estão corretas, mas nunca as 4 linhas.
Por que não usar Unicode sempre?
Usamos o tempo todo, a Microsoft já se rendeu ao UNICODE no desenvolvimento, embora o Windows ainda tenha uma salada de código de páginas, por exemplo, o sistema de arquivos é unicode, o ambiente gráfico ainda é win1252 e o cmd (terminal) Latin1, por isso, há muitas inconsistências quando programamos em Windows visando o unicode. Se um programador gerar um script SQL e salvá-lo no disco usando WIN1252, mas os caracteres gerados nele estiverem em UNICODE, haverá problemas.
Isso é um inferno que apenas existe no Windows, e que quando você tentar portar para Linux tem de resolver problemas que antes não existiam. Por isso, para nossa comodidade, os bancos de dados incluam WIN1252 entre os seus charsets, embora o mesmo não seja o ideal.
Charset e performance em bancos de dados
Ainda sobre banco de dados, nem sempre o UNICODE é desejável, pois um único caractere armazenado nele pode ocupar de 1 a 4 bytes, por isso, não é largamente utilizado se uma aplicação já "fala" ISO8859_1 ou WIN1252, pois nações que usam ideogramas ou outros símbolos tipográficos também sabem "falar" ISO8859_1 ou WIN1252, embora o inverso não seja verdadeiro, se você fosse de uma empresa japonesa teria de escrever uma aplicação que usasse os ideogramas japoneses e muito provavelmente também o ocidental e só o UNICODE resolveria este problema.
Então se você mora num país ocidental que usa os caracteres A-Z pode-se considerar sortudo porque temos opções de escolha, e geralmente optamos pela mais econômica que também é a mais performática, desconsiderar UNICODE e escolher entre WIN1252 ou ISO8859_1.
Um banco de dados inteiramente em UNICODE é menos performático quando comparado ao Latin1, não apenas por ser mais guloso em termos de espaço, lembre-se um varchar(64) a depender do UNICODE usado, ao invés de 64 caracteres pode conter apenas 16, mas também porque o algoritmo de "case insensitive" e "accent insensitive" podem vir a ser mais complexos. O MySQL por exemplo, é reconhecido por adotar uma gambiarra de UNICODE que às vezes criam problemas.
Nota sobre SGBDs: Os conceitos de performance relacionados a charset são universais. No MS SQL Server, você pode usar VARCHAR (1 byte por caractere em Latin1) ou NVARCHAR (2 bytes por caractere em Unicode). No PostgreSQL, o comportamento é similar, com VARCHAR usando o charset do banco e TEXT também respeitando a codificação. No Firebird, os tipos VARCHAR e CHAR podem usar diferentes charsets. Em todos os casos, Unicode tende a ocupar mais espaço e pode impactar a performance de índices.
Exemplo prático: Criando um banco de teste
Nota: Os exemplos a seguir são baseados no Firebird, mas os conceitos e princípios são os mesmos para MS SQL Server e PostgreSQL. A sintaxe pode variar ligeiramente, mas a lógica de funcionamento dos charsets é universal.
IBExpert – Criar banco de dados:
C:\TEMP\TEST_ISO8859_1.FDB
CHARSET: ISO8859_1
COLLATE: PT_BR
ALIAS: TEST_ISO8859_1.FDB
PORTA: 3040
Criar a tabela:
CREATE TABLE T1(
FRUTA_ISO8859 VARCHAR(10) CHARACTER SET ISO8859_1 COLLATE PT_BR,
FRUTA_WIN1252 VARCHAR(10) CHARACTER SET WIN1252 COLLATE WIN_PTBR,
FRUTA_UNICODE VARCHAR(10) CHARACTER SET UTF8 COLLATE UNICODE_CI_AI
);
Equivalente em outros SGBDs:
- MS SQL Server: Use
VARCHAR(10) COLLATE Latin1_General_CI_ASpara Latin1,VARCHAR(10) COLLATE SQL_Latin1_General_CP1252_CI_ASpara WIN1252, ouNVARCHAR(10)para Unicode. - PostgreSQL: O charset é definido na criação do banco. Use
VARCHAR(10)que herdará o charset do banco, ou especifique comSET client_encoding. Para Unicode, useTEXTouVARCHARcom encoding UTF8.
Popular valores acentuados com o script:
INSERT INTO T1 (FRUTA_ISO8859, FRUTA_WIN1252, FRUTA_UNICODE)
VALUES ('Maracujá', 'Maracujá', 'Maracujá');
INSERT INTO T1 (FRUTA_ISO8859, FRUTA_WIN1252, FRUTA_UNICODE)
VALUES ('Açaí', 'Açaí', 'Açaí');
INSERT INTO T1 (FRUTA_ISO8859, FRUTA_WIN1252, FRUTA_UNICODE)
VALUES ('Gravatá', 'Gravatá', 'Gravatá');
INSERT INTO T1 (FRUTA_ISO8859, FRUTA_WIN1252, FRUTA_UNICODE)
VALUES ('Avelã', 'Avelã', 'Avelã');
INSERT INTO T1 (FRUTA_ISO8859, FRUTA_WIN1252, FRUTA_UNICODE)
VALUES ('Melão', 'Melão', 'Melão');
INSERT INTO T1 (FRUTA_ISO8859, FRUTA_WIN1252, FRUTA_UNICODE)
VALUES ('Maçã', 'Maçã', 'Maçã');
INSERT INTO T1 (FRUTA_ISO8859, FRUTA_WIN1252, FRUTA_UNICODE)
VALUES ('Mamão', 'Mamão', 'Mamão');
INSERT INTO T1 (FRUTA_ISO8859, FRUTA_WIN1252, FRUTA_UNICODE)
VALUES ('Jiló', 'Jiló', 'Jiló');
INSERT INTO T1 (FRUTA_ISO8859, FRUTA_WIN1252, FRUTA_UNICODE)
VALUES ('Babaçu', 'Babaçu', 'Babaçu');
COMMIT WORK;
Verificar tamanho em bytes:
Será que todos os caracteres têm o mesmo tamanho em bytes? Execute:
SELECT
a.fruta_iso8859||'('||OCTET_LENGTH(a.fruta_iso8859)||')' AS iso8859,
a.fruta_win1252||'('||OCTET_LENGTH(a.fruta_win1252)||')' AS win1252,
a.fruta_unicode||'('||OCTET_LENGTH(a.fruta_unicode)||')' AS unicode
FROM T1 a;
Equivalente em outros SGBDs:
- MS SQL Server: Use
DATALENGTH()ouLEN()para obter o tamanho em bytes. - PostgreSQL: Use
LENGTH()para caracteres ouOCTET_LENGTH()para bytes, similar ao Firebird.
Entre as nossas frutas o limite de caracteres é 10, o maracujá foi a fruta que mais ocupou espaço em bytes com 9 bytes em unicode. O que vai acontecer se aumentar para 10 caracteres e acentuarmos ainda mais letras? Vamos conferir:
UPDATE T1 SET
FRUTA_UNICODE='Maráçujãío' -- 10 caracteres, deveria caber, o limite é 10
WHERE FRUTA_UNICODE='Maracujá';
Conseguimos, mas note a divergência entre o tamanho de caracteres e o tamanho de byte ocupados:
SELECT
a.fruta_iso8859||OCTET_LENGTH(a.fruta_iso8859),
a.fruta_win1252||OCTET_LENGTH(a.fruta_win1252),
a.fruta_unicode||OCTET_LENGTH(a.fruta_unicode)
FROM T1 a
WHERE FRUTA_UNICODE='Maráçujãío';
Maracujá tem 10 caracteres, mas ocupa agora 14 bytes. Então o tamanho de um campo não é o mesmo que dizer que o tamanho ocupado. O que acha que aconteceria se todas as 10 letras tivessem acento? Vamos tentar:
UPDATE T1 SET
FRUTA_UNICODE='áéíóúáéíóú' -- 10 caracteres
WHERE FRUTA_UNICODE='Maráçujãío';
Chegamos então à conclusão que em unicode – em especial UTF-8 – o armazenamento do seu banco será maior contendo acentuações do que seria em Latin1 ou Ansi.
Com o armazenamento tão barato, devo me preocupar?
Se seu objetivo for internacionalizar, não deve se preocupar com o armazenamento, mas deve se preocupar com os limites teóricos de seus metadados, como nome de objetos. Por exemplo, se eu criar uma tabela cuja chave é um varchar(x) qual é o tamanho máximo de uma chave dentro de um índice?
Informação sobre limites de índices: Cada SGBD tem seus próprios limites, mas o princípio é o mesmo: quando o limite é especificado em bytes, campos Unicode podem armazenar menos caracteres do que campos Latin1.
- FirebirdSQL: A partir do Firebird 2.0, o comprimento máximo de uma string indexada é calculado pela fórmula:
max_char_length = FLOOR((page_size / 4 - 9) / N)onde N é o número de bytes por caractere no charset. - MS SQL Server: O limite de índice é de 900 bytes para chaves não-clusterizadas e 1700 bytes para índices clusterizados. Com
NVARCHAR(Unicode), cada caractere ocupa 2 bytes. - PostgreSQL: O limite de índice B-tree é de aproximadamente 2704 bytes (1/3 do tamanho da página padrão de 8KB). Com UTF-8, caracteres acentuados podem ocupar 2-4 bytes.
Então quando estiver usando unicode tome cuidado com os limites do seu SGBD que forem em bytes, pois o que antes era 1 byte = 1 caractere não é mais aplicável. Os limites que eram estabelecidos em caracteres, estes não mudam, se o limite para tamanho de nome para uma tabela é 63 caracteres, continuará sendo 63 caracteres não importando se é unicode ou não.
Aqui temos uma gritante diferença para ISO8859_1 ou WIN1252, pois nestes cada caractere é 1 byte, enquanto em unicode os SGBDs assumem que cada caractere pode consumir mais bytes. Então se você criar um campo varchar(18000) e inserir 18.000 caracteres acentuados, ele irá deixar, mas o espaço ocupado será 18.000 × 2 ou 18.000 × 4 dependendo do acento e do SGBD.
Portanto, quando criar um campo que você sabe que no máximo terá X caracteres, leve em consideração:
- O tamanho declarado não é o mesmo de tamanho ocupado.
- Cuidado com os metadados, se um tipo de limite for informado em bytes então usando unicode você terá menos caracteres disponíveis do que o informado.
Vamos ao COLLATE
Charset é o conjunto de caracteres disponíveis, majoritariamente ISO8859_1, WIN1252 e UNICODE. Cada qual com o seu conjunto limitado de caracteres. Se seu banco de dados precisa de caracteres latinos e ocidentais ISO8859_1 está bom para você, você não deveria usar o WIN1252 porque ele foi provido apenas para Windows e sua condição pode mudar no futuro.
Mas se precisa de um conjunto grande de caracteres que permita escrever na mesma sentença caracteres ocidentais e ideogramas japoneses então você tem que optar pelo UNICODE, geralmente UTF-8.
O collate é a forma como os caracteres serão tratados e/ou ordenados. Será que 'A' vem antes 'á'? Será que 'Pharmacia' e 'Farmacia' é a mesma coisa? Será que 'José' e 'Jose' também são iguais?
Quem cria os collates define isso, no Brasil não tem essa coisa de 'Ph' de pharmacia dos anos 30, mas em outros países podem haver agrupamento de caracteres (collate) que devam ser tratados conjuntamente e não de forma individual.
Exemplo prático de Collate:
CREATE TABLE T2 (
NOME VARCHAR(30) NOT NULL COLLATE PT_BR
);
INSERT INTO T2 (NOME) VALUES ('FARMACIA');
INSERT INTO T2 (NOME) VALUES ('FARMÁCIA');
INSERT INTO T2 (NOME) VALUES ('Jose');
INSERT INTO T2 (NOME) VALUES ('José');
INSERT INTO T2 (NOME) VALUES ('JOSÉ');
Note agora a ordenação de dados entre dois collates diferentes usando o mesmo charset:
SELECT * FROM T2 a ORDER BY a.nome COLLATE PT_PT;
Agora o outro:
SELECT * FROM T2 a ORDER BY a.nome COLLATE PT_BR;
Os collates estão atrelados ao charset porque sem eles, o banco não teria a regionalidade de ordenação, ou quais sinais diacríticos são iguais a suas versões sem esses sinais e assim por diante.
Collates em diferentes SGBDs:
- FirebirdSQL: Suporta vários collations para UTF8 como
UCS_BASIC,UNICODE,UNICODE_CI(case-insensitive),UNICODE_CI_AI(case-insensitive e accent-insensitive). - MS SQL Server: Oferece collates como
Latin1_General_CI_AS(case-insensitive, accent-sensitive),SQL_Latin1_General_CP1252_CI_AI(case e accent insensitive), entre outros. - PostgreSQL: Usa collates do sistema operacional, como
pt_BR.UTF-8,C(binário), ou collates customizados. SuportaCOLLATE "C"para comparações binárias.
Embora a sintaxe varie, o conceito de collate é universal: ele define como os caracteres são comparados e ordenados.
Mesmo que um banco de dados tenha uma tabela usando 3 charsets diferentes, contendo os mesmos dados, o collate poderá fazer com que os dados se comportem usando a mesma regra linguística. Por exemplo, para o Brasil, case/accent insensitive significa que o collate não fará distinção entre maiúsculos e minúsculos e que a ordenação seguirá um mesmo padrão.
Testando ordenação por charset:
Vamos testar se a ordenação foi influenciada pelo charset executando esta query (exemplo específico do Firebird, mas o conceito se aplica a todos os SGBDs):
EXECUTE BLOCK
RETURNS(
iso8859_1 VARCHAR(10),
win1252 VARCHAR(10),
unicode VARCHAR(10))
AS
BEGIN
--iso8859_1
iso8859_1='Sim';
win1252='-';
unicode='-';
SUSPEND;
FOR SELECT
a.fruta_iso8859, a.fruta_win1252, a.fruta_unicode
FROM T1 a
ORDER BY a.fruta_iso8859
INTO iso8859_1, win1252, unicode
DO BEGIN
SUSPEND;
END
-- win1252
iso8859_1='-';
win1252='Sim';
unicode='-';
SUSPEND;
FOR SELECT
a.fruta_iso8859, a.fruta_win1252, a.fruta_unicode
FROM T1 a
ORDER BY a.fruta_win1252
INTO iso8859_1, win1252, unicode
DO BEGIN
SUSPEND;
END
-- unicode
iso8859_1='-';
win1252='-';
unicode='Sim';
SUSPEND;
FOR SELECT
a.fruta_iso8859, a.fruta_win1252, a.fruta_unicode
FROM T1 a
ORDER BY a.fruta_unicode
INTO iso8859_1, win1252, unicode
DO BEGIN
SUSPEND;
END
END
Notamos na saída do comando EXECUTE BLOCK que a ordenação pelo charset ISO8859_1 (latin1), WIN1252 ou unicode não teve diferença!
Nota: Embora este exemplo use a sintaxe específica do Firebird (EXECUTE BLOCK), o conceito de testar ordenação por diferentes charsets é aplicável a todos os SGBDs. No MS SQL Server, você usaria procedimentos armazenados ou CTEs. No PostgreSQL, você usaria funções ou CTEs similares.
Inserindo caracteres não acentuados:
Vamos complicar e inserir caracteres não acentuados, executando essa sequência de ExecSQL:
INSERT INTO T1 (FRUTA_ISO8859, FRUTA_WIN1252, FRUTA_UNICODE)
VALUES ('Maracuja', 'Maracuja', 'Maracuja');
INSERT INTO T1 (FRUTA_ISO8859, FRUTA_WIN1252, FRUTA_UNICODE)
VALUES ('Açai', 'Açai', 'Açai');
INSERT INTO T1 (FRUTA_ISO8859, FRUTA_WIN1252, FRUTA_UNICODE)
VALUES ('Gravata', 'Gravata', 'Gravata');
INSERT INTO T1 (FRUTA_ISO8859, FRUTA_WIN1252, FRUTA_UNICODE)
VALUES ('Avela', 'Avela', 'Avela');
INSERT INTO T1 (FRUTA_ISO8859, FRUTA_WIN1252, FRUTA_UNICODE)
VALUES ('Melao', 'Melao', 'Melao');
INSERT INTO T1 (FRUTA_ISO8859, FRUTA_WIN1252, FRUTA_UNICODE)
VALUES ('Maça', 'Maça', 'Maça');
INSERT INTO T1 (FRUTA_ISO8859, FRUTA_WIN1252, FRUTA_UNICODE)
VALUES ('Mamao', 'Mamao', 'Mamao');
INSERT INTO T1 (FRUTA_ISO8859, FRUTA_WIN1252, FRUTA_UNICODE)
VALUES ('Jilo', 'Jilo', 'Jilo');
INSERT INTO T1 (FRUTA_ISO8859, FRUTA_WIN1252, FRUTA_UNICODE)
VALUES ('Babacu', 'Babacu', 'Babacu');
Executamos o EXECUTE BLOCK outra vez e notamos que não houve diferença, a ordem foi a mesma para todos os casos, apenas a versão não acentuada teve precedência.
Resumo e Recomendações
| Charset | Bytes por caractere | Uso recomendado | Vantagens | Desvantagens |
|---|---|---|---|---|
| ISO8859_1 (Latin1) | 1 byte | Sistemas legados, aplicações brasileiras que não precisam de internacionalização | Melhor performance, menor uso de espaço, compatível com padrões ISO | Limitado a caracteres latinos ocidentais |
| WIN1252 | 1 byte | Aplicações Windows que não serão migradas para Linux | Melhor performance, menor uso de espaço, inclui símbolo Euro | Específico do Windows, problemas em migração para Linux |
| UTF8 | 1-4 bytes | Sistemas novos, aplicações internacionais, bancos que precisam suportar múltiplos idiomas | Universal, suporta todos os caracteres, padrão moderno | Maior uso de espaço, menor performance em alguns casos, complexidade nos índices |
Recomendações finais:
- Para sistemas novos: Use UTF8, especialmente se há possibilidade de internacionalização. Esta recomendação é válida para MS SQL Server, PostgreSQL e Firebird.
- Para sistemas legados brasileiros: ISO8859_1 é a melhor escolha para portabilidade entre diferentes SGBDs e sistemas operacionais.
- Evite WIN1252: A menos que você tenha certeza absoluta que nunca migrará para Linux ou para outro SGBD.
- Atenção aos limites: Em UTF8, considere que caracteres acentuados podem ocupar 2-4 bytes, afetando limites de índices em todos os SGBDs.
- Collate adequado: Escolha o collate apropriado para sua região (PT_BR para Brasil). Cada SGBD tem sua própria sintaxe, mas o conceito é universal.
Referências e compatibilidade entre SGBDs:
- FirebirdSQL - CHARACTER SET Documentation
- MS SQL Server - Collation and Unicode Support
- PostgreSQL - Character Set Support
- Firebird Conference 2011 - Character Sets and Unicode (conceitos universais aplicáveis a todos os SGBDs)