Qual o impacto do LC_CTYPE em um banco de dados PostgreSQL?

25

Então, eu tenho poucos servidores Debian com o PostgreSQL. Historicamente, esses servidores e o PostgreSQL são localizados com o charset Latin 9 e, naquela época, tudo estava bem. Agora temos que lidar com coisas como polonês, grego ou chinês, para mudar isso se torna um problema crescente.

Quando tentei criar um banco de dados UTF8, recebi a mensagem:

ERRO: a codificação UTF8 não corresponde à localidade fr_FR Detalhe: A configuração LC_CTYPE escolhida requer a codificação LATIN9.

Poucas vezes fiz algumas pesquisas sobre o assunto com meu antigo amigo Google, e tudo o que pude encontrar foram alguns procedimentos complicados, como atualizar o Debian LANG, recompilar o PostgreSQL com o conjunto de caracteres correto, editar todas as LC_variáveis do sistema e outras soluções obscuras. Por enquanto, deixamos essa questão de lado.

Recentemente, ele voltou novamente, os gregos querem o material e o Latin 9 não. E enquanto eu estava analisando esse problema novamente, um colega veio até mim e disse: "Não, é fácil, veja."

Ele não editou nada, não fez truques de mágica, apenas fez esta consulta SQL:

CREATE DATABASE my_utf8_db
  WITH ENCODING='UTF8'
       OWNER=admin
       TEMPLATE=template0
       LC_COLLATE='C'
       LC_CTYPE='C'
       CONNECTION LIMIT=-1
       TABLESPACE=pg_default;

E funcionou bem.

Na verdade, eu não sabia LC_CTYPE='C'e fiquei surpreso ao usar isso não nas primeiras soluções no Google e até no Stack Overflow. Olhei em volta e só encontrei uma menção na documentação do PostgreSQL.

Quando LC_CTYPE é C ou POSIX, qualquer conjunto de caracteres é permitido, mas para outras configurações de LC_CTYPE, existe apenas um conjunto de caracteres que funcionará corretamente. Como a configuração LC_CTYPE é congelada pelo initdb, a aparente flexibilidade para usar codificações diferentes em bancos de dados diferentes de um cluster é mais teórica do que real, exceto quando você seleciona a localidade C ou POSIX (desativando assim qualquer reconhecimento real da localidade).

Então isso me fez pensar: isso é fácil demais, perfeito demais, quais são as desvantagens? E ainda tenho dificuldade em encontrar uma resposta. Então aqui vou eu postar aqui:

tl; dr: Qual a desvantagem do uso LC_CTYPE='C'em uma localização específica? É ruim fazer isso? O que devo esperar quebrar?

Gregoire D.
fonte

Respostas:

26

Quais são as desvantagens de usar LC_CTYPE = 'C' em uma localização específica

A documentação menciona o relacionamento entre códigos de idioma e recursos SQL no Suporte local :

As configurações de localidade influenciam os seguintes recursos SQL:

  • Ordem de classificação em consultas usando ORDER BY ou os operadores de comparação padrão em dados textuais

  • As funções superior, inferior e initcap

  • Operadores de correspondência de padrões (expressões regulares estilo LIKE, SIMILAR TO e POSIX); localidades afetam a correspondência sem distinção entre maiúsculas e minúsculas e a classificação de caracteres por expressões regulares da classe de caracteres

  • A família de funções to_char

  • A capacidade de usar índices com cláusulas LIKE

O primeiro item (ordem de classificação) é sobre LC_COLLATEe os outros parecem ser sobre LC_CTYPE.

LC_COLLATE

LC_COLLATEafeta comparações entre cadeias. Na prática, o efeito mais visível é a ordem de classificação. LC_COLLATE='C'(ou POSIXque é um sinônimo) significa que é a ordem de bytes que gera comparações, enquanto um código de idioma na language_REGIONforma significa que regras culturais direcionam as comparações.

Um exemplo com nomes franceses, executado de dentro de um banco de dados UTF-8:

select firstname from (values ('bernard'), ('bérénice'), ('béatrice'), ('boris'))
 AS l(firstname)
order by firstname collate "fr_FR";

Resultado:

 primeiro nome 
-----------
 béatrice
 bérénice
 Bernard
 Boris

béatricevem antes boris, porque o E acentuado se compara ao O como se não fosse acentuado. É uma regra cultural.

Isso difere do que acontece com um Ccódigo de idioma:

select firstname from (values ('bernard'), ('bérénice'), ('béatrice'), ('boris')) 
 AS l(firstname)
order by firstname collate "C";

Resultado:

 primeiro nome 
-----------
 Bernard
 Boris
 béatrice
 bérénice

Agora, os nomes com E acentuado são pressionados no final da lista. A representação de bytes éem UTF-8 é hexadecimal C3 A9e, por oisso, é 6f. c3é maior que 6fisso na Clocalidade 'béatrice' > 'boris',.

Não são apenas sotaques. Existem regras mais complexas com hifenização, pontuação e caracteres estranhos como œ. Regras culturais estranhas são esperadas em todos os locais.

Agora, se as sequências a serem comparadas misturam idiomas diferentes, como ao ter uma firstnamecoluna para pessoas de todo o mundo, pode ser que qualquer localidade em particular não domine, de qualquer maneira, porque diferentes alfabetos para diferentes idiomas não foram projetados para serem ordenados um contra o outro.

Nesse caso, Cé uma escolha racional e tem a vantagem de ser mais rápido, porque nada pode superar as comparações de bytes simples.

LC_CTYPE

Tendo LC_CTYPEdefinido para 'C' implica que as funções C como isupper(c)ou tolower(c)dar resultados esperados apenas para caracteres no intervalo US-ASCII (isto é, até codepoint 0x7F em Unicode).

Porque funções SQL gosta upper(), lower()ou initcap são implementados em Postgres em cima dessas funções libc, eles são afetados por isso assim que existem caracteres não US-ASCII em cordas.

Exemplo:

test=> show lc_ctype;
  lc_ctype   
-------------
 fr_FR.UTF-8
(1 row)

-- Good result
test=> select initcap('élysée');
 initcap 
---------
 Élysée
(1 row)

-- Wrong result
-- collate "C" is the same as if the db has been created with lc_ctype='C'
test=> select initcap('élysée' collate "C");
 initcap 
---------
 éLyséE
(1 row)

Para o Ccódigo do idioma, éé tratado como um caractere não categorizável.

Da mesma forma, resultados errados também são obtidos com expressões regulares:

test=> select 'élysée' ~ '^\w+$';
 ?column? 
----------
 t
(1 row)

test=> select 'élysée' COLLATE "C" ~ '^\w+$';
 ?column? 
----------
 f
(1 row)
Daniel Vérité
fonte
Portanto, se eu acertar, teríamos o problema do pedido, mesmo que você fizesse um servidor UTF-8? Eu acho que ter o sistema LC_CTYPE definido em UTF-8 ou compilar o PostgreSQL em UTF-8 resultará no mesmo problema de comparação que você aponta.
Gregoire D.
Para estender isso, seria possível forçar a intercalação nas consultas para que a comparação seja localmente correta?
Gregoire D.
Sim, comparações de cadeias inviduais podem incorporar suas próprias regras de ordenação, como faço nesta resposta com o collate "C"depois do order by. Cabe a você determinar se e onde seu aplicativo precisa. A maioria dos aplicativos por aí realmente não se importa.
Daniel Vérité
11
Observe também que colunas individuais podem ter um COLLATEespecificador diferente do banco de dados.
Daniel Vérité
2
Esta resposta é realmente para LC_COLLATE, não para LC_CTYPE. LC_CTYPE é usado para decidir se um personagem é um dígito, letra, espaços, pontuação, etc.
jjanes
10

Em referência à resposta aceita por Daniel sobre a classificação usando agrupamentos, lembre-se de que, se você estiver executando o PostgreSQL em um Mac, o agrupamento preferido pode não funcionar conforme o esperado, devido a configurações inadequadas para alguns agrupamentos no nível do sistema operacional. Você pode ler mais sobre o problema aqui:

http://www.postgresql.org/message-id/[email protected]

Este não é um problema específico do PostgreSQL, especificamente, mas um problema com a configuração padrão do Mac para configurações de agrupamento. Meu sistema atual está executando o PostgreSQL 9.3 no OS X El Capitan versão 10.11 e sofre com esse problema. Meu sistema retorna os mesmos resultados da consulta, independentemente de eu usar o agrupamento "fr_FR" ou "en_US". Por exemplo:

Usando o agrupamento "fr_FR":

select firstname from (values ('bernard'), ('bérénice'), ('béatrice'), ('boris'))
AS l(firstname)
order by firstname collate "fr_FR";

results:
==============
bernard
boris
béatrice
bérénice

Usando o agrupamento "en_US":

select firstname from (values ('bernard'), ('bérénice'), ('béatrice'), ('boris'))
AS l(firstname)
order by firstname collate "en_US";

results:
==============
bernard
boris
béatrice
bérénice

No meu sistema, as configurações de agrupamento (no nível do sistema operacional) são as mesmas para "fr_FR" e "en_US", conforme demonstrado no shell, executando diff:

cd /usr/share/locale
diff fr_FR.UTF-8/LC_COLLATE en_US.UTF-8/LC_COLLATE

Esperamos que essas informações adicionais sejam úteis para quem estiver lendo isso e estiver usando o PostgreSQL em um Mac que sofre desse problema.

cafecoder905
fonte
Como posso fazê-lo funcionar em Macs modernos. Você já passou por algo para fazê-lo funcionar no seu mac?
Dinesh Kumar 10/09