Qual charset devo usar? ISO8859_1 WIN1252 ou UTF8?

Este artigo ainda está em rascunho, as informações estão corretas, mas falta ajustes na formatação, concordância gramatical e achar uma maneira mais didática. Mas se entender, pode utilizá-lo.

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 encaminhamos 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?

Se você chegou a conhecer o MS-DOS deve se lembrar do comando chcp que usavamos 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)

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.

[todo: mostrar no notepad++ essa transliteração]

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.

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:

[vou incluir no video o link:
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

todo: mostrar o exemplo no terminal do linux, mudando o prompt para exibir caracteres latinos e utf

todo: no windows trocar code page de 850 para chcp 65001

chcp

Comprovamos então 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

Aagora temos duas linhas iniciais exibidas corretamente, mas as duas ultimas erroneamente, por que? 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.

Mas se o unicode é tão bom, porque não usamos?

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) Lantin1, 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. Se você teve de criar editar um arquivo unicode usando bloco de notas e depois percebeu.

todo: demonstrar um script SQL usando o IBExpert, banco de dados UTF-8 não vai rodar um script aparentemente inofensivo que foi feito em Win1252.

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.

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ções 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 inves 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.

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

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;

Será que todos os caracteres tem 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

Entre as nossas frutas o limite de caracteres é 10, o maracujá foi a fruta 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 tivesse acento? Vamos tentar:

update T1 set
FRUTA_UNICODE='áéíóúáéíóú'-- 10 caracteres
where FRUTA_UNICODE='Maráçujãío'

Chegamos então a 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? No FirebirdSQL, este tamanho é ¼ do tamanho da pagina de dados. Se por exemplo, seu banco tem paginas de 4kb, significa que o tamanho de um índice não vai poder ultrapassar 1k então um indice simples ou composto que tenha mais de 1024bytes não será possível. Então quando estiver usando unicode tome cuidado com os limites do FirebirdSQL 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 o FirebirdSQL assume que cada caractere consumido é 4 bytes. Então se voce 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.

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 o metadados, se um tipo de limite for informado em bytes então usando unicode você terá 4x menos 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 limitados de caracteres. Se seu banco de dados precisa de caracteres latinos e ocidental ISO8859_1 tá 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 dum 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.

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 colates 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.

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.

Vamos testar se a ordenação foi influenciada pelo charset executando esta query:

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!

Vamos complicar e inserir caracteres não acentuados, executando essa sequencia 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.