the order of chinese char in PostgreSQL DB with different encoding and it’s collate
背景
这是一篇比较老的文章,仅作记录,拼音相关建议看以下文章
《PostgreSQL WHY ERROR: invalid byte sequence for encoding “UTF8”》
《PostgreSQL SQL_ASCII encoding introduce》
《PostgreSQL Server Encoding sql_ascii attention》
《PostgreSQL 按拼音排序 - convert to GBK/EUC_CN coding》
《PostgreSQL 获取拼音首字母的函数 - 摘自互联网》
《在PostgreSQL中实现按拼音、汉字、拼音首字母搜索的例子》
正文
今天群里面在讨论关于中文排序的问题,下面从PostgreSQL数据库编码来整理一下。
下面是在一台Linux(RHEL5)机器上的测试,数据库版本PostgreSQL 9.1beta2
首先,
可以存储简体中文的服务端字符集编码(不含繁体):
1. EUC_CN
2. SQL_ASCII
3. UTF8
可以显示简体中文的客户端字符集编码(不含繁体),除以上三种还有如下:
1. GB18030
2. GBK
排序除与字符编码有关,还与COLLATE有关。
在pg_collation系统表可以查找到字符集对应的可以选择的collate和ctype信息。
下面来测试一组中文排序:
1. UTF8编码的数据库
postgres=# create database db_utf8 with owner digoal template template0 encoding 'UTF8';
CREATE DATABASE
postgres=# \c db_utf8 digoal
You are now connected to database "db_utf8" as user "digoal".
db_utf8=> create table tbl_chinese_order (info text);
CREATE TABLE
db_utf8=> insert into tbl_chinese_order values ('刘少奇');
INSERT 0 1
db_utf8=> insert into tbl_chinese_order values ('刘德华');
INSERT 0 1
db_utf8=> insert into tbl_chinese_order values ('张学友');
INSERT 0 1
以下SQL得到的结果一致:
select * from tbl_chinese_order order by info;
select * from tbl_chinese_order order by info collate "C";
select * from tbl_chinese_order order by info collate "zh_CN";
select * from tbl_chinese_order order by info collate "zh_CN.utf8";select * from tbl_chinese_order order by info collate "POSIX";
select * from tbl_chinese_order order by info collate "default";
结果都一样:
info
--------
刘少奇
刘德华
张学友
(3 rows)
2. SQL_ASCII编码的数据库(不推荐 “多字节字符” 使用sql_ascii, 参考手册对应部分)
postgres=# create database db_sql_ascii with owner digoal template template0 encoding 'SQL_ASCII';
CREATE DATABASE
测试数据同上.
以下SQL得到的结果一致:
select * from tbl_chinese_order order by info;
select * from tbl_chinese_order order by info collate "C";
select * from tbl_chinese_order order by info collate "POSIX";
select * from tbl_chinese_order order by info collate "default";
结果都一样:
info
--------
刘少奇
刘德华
张学友
(3 rows)
3. EUC_CN编码的数据库
postgres=# create database db_euc_cn with owner digoal template template0 encoding 'EUC_CN';
CREATE DATABASE
测试数据同上.
这次结果有变化:
select * from tbl_chinese_order order by info collate "zh_CN.gb2312";
select * from tbl_chinese_order order by info collate "zh_CN";
结果如下:
info
--------
刘少奇
刘德华
张学友
(3 rows)
以下SQL按拼音返回顺序了:
select * from tbl_chinese_order order by info;
select * from tbl_chinese_order order by info collate "C";
select * from tbl_chinese_order order by info collate "POSIX";
select * from tbl_chinese_order order by info collate "default";
info
--------
刘德华
刘少奇
张学友
(3 rows)
根据以上的测试结果,说明EUC_CN字符集的字符对应的编码顺序和SQL_ASCII以及UTF8不一样,因此使用collate=C来排序时得到了不一样的结果。
然而order by info collate “zh_CN”并没有带来所需要的结果,当然这个可能是LIBC有关, 也可能是其他原因.
https://www.postgresql.org/docs/9.6/static/collation.html
补充一个例子,PostgreSQL9.1可以针对列设置collate值,如下:
db_euc_cn=> drop table tbl_chinese_order;
DROP TABLE
db_euc_cn=> create table tbl_chinese_order (info text collate "zh_CN");
CREATE TABLE
db_euc_cn=> insert into tbl_chinese_order values ('刘少奇');
INSERT 0 1
db_euc_cn=> insert into tbl_chinese_order values ('刘少奇');
INSERT 0 1
db_euc_cn=> insert into tbl_chinese_order values ('刘德华');
INSERT 0 1
db_euc_cn=> insert into tbl_chinese_order values ('张学友');
INSERT 0 1
默认的collate就不再是数据库初始化时默认的C了, 而是指定的zh_CN.
db_euc_cn=> select * from tbl_chinese_order order by info;
info
--------
刘少奇
刘少奇
刘德华
张学友
(4 rows)
db_euc_cn=> select * from tbl_chinese_order order by info collate "POSIX";
info
--------
刘德华
刘少奇
刘少奇
张学友
(4 rows)
抛开数据库,来看看操作系统下的排序
1. 首先是LINUX(RHEL5)操作系统里面的排序风格:
postgres@db5-> export LANG=en_US.utf8
postgres@db5-> cat order
刘德华
张学友
刘少奇
postgres@db5-> cat order |sort
刘少奇
刘德华
张学友
postgres@db5-> export LANG=zh_CN.gbk
postgres@db5-> cat order |sort
张学友
刘德华
刘少奇
postgres@db5-> export LANG=zh_CN.utf8
postgres@db5-> cat order |sort
刘少奇
刘德华
张学友
postgres@db5-> export LC_COLLATE=zh_CN.gbk
postgres@db5-> cat order |sort
张学友
刘德华
刘少奇
postgres@db5-> export LC_COLLATE=C
postgres@db5-> cat order |sort
刘少奇
刘德华
张学友
postgres@db5-> export LC_COLLATE=POSIX
postgres@db5-> cat order |sort
刘少奇
刘德华
张学友
结果与在PostgreSQL数据库中的测试相当。
2. WINDOW7中文旗舰版下面,操作系统对文本的排序。
D:\>sort ./test
"刘德华"
"刘德华"
"刘少奇"
"张学友"
3. MAC LION系统下面,操作系统对文本的排序。
sort ./test
刘少奇
刘德华
张学友
4. 泥鳅哥在某LINUX版本下的测试:
[root@dbserver ~]# export LANG=zh_CN.UTF-8
[root@dbserver ~]# cat ts.txt | sort
刘德华
刘少奇
张学友
[root@dbserver ~]# uname -a
Linux dbserver.zhenghongkeji.com 2.6.32-131.6.1.el6.x86_64 #1 SMP Tue Jul 12 17:14:50 CDT 2011 x86_64 x86_64 x86_64 GNU/Linux
[root@dbserver ~]# cat /etc/issue
Scientific Linux release 6.1 (Carbon)
5. CENTOS6.0版本下的测试:
export LANG=zh_CN.UTF-8
sort ./test
刘德华
刘少奇
张学友
RHEL5的中文字符集包, fonts-chinese-3.02-12.el5
RHEL6的中文字符集包, 已经不使用这个包了.
后来我们这边的同事发现操作系统RHEL5有对中文排序的BUG。见最后的附录.
附录
postgres=# \l
List of databases
Name | Owner | Encoding | Collate | Ctype | Access privileges
--------------+----------+-----------+---------+-------+-----------------------
db_euc_cn | digoal | EUC_CN | C | C |
db_sql_ascii | digoal | SQL_ASCII | C | C |
db_utf8 | digoal | UTF8 | C | C |
这里要注意,如果在9.0下面测试,collate是库级别的,不能按列级别来调整.所以我选用9.1来测试就比较方便了.
另一种强制中文排序的方法是使用convert_to将字符转换成目标字符集的编码, 以bytea输出, 根据转换后的bytea排序即可.
digoal=> select *,convert_to(info,'gbk'),info::bytea from t order by convert_to(info,'gbk');
id | info | convert_to | info
----+--------+----------------+----------------------
3 | 郭富城 | \xb9f9b8bbb3c7 | \xe983ade5af8ce59f8e
4 | 黎明 | \xc0e8c3f7 | \xe9bb8ee6988e
1 | 刘德华 | \xc1f5b5c2bbaa | \xe58898e5beb7e58d8e
2 | 刘少奇 | \xc1f5c9d9c6e6 | \xe58898e5b091e5a587
5 | 张学友 | \xd5c5d1a7d3d1 | \xe5bca0e5ada6e58f8b
(5 rows)
digoal=> select *,convert_to(info,'euc_cn'),info::bytea from t order by convert_to(info,'euc_cn'); -- 转换排序, 可以使用PostgreSQL表达式索引, 实现拼音排序走索引
id | info | convert_to | info
----+--------+----------------+----------------------
3 | 郭富城 | \xb9f9b8bbb3c7 | \xe983ade5af8ce59f8e
4 | 黎明 | \xc0e8c3f7 | \xe9bb8ee6988e
1 | 刘德华 | \xc1f5b5c2bbaa | \xe58898e5beb7e58d8e
2 | 刘少奇 | \xc1f5c9d9c6e6 | \xe58898e5b091e5a587
5 | 张学友 | \xd5c5d1a7d3d1 | \xe5bca0e5ada6e58f8b
(5 rows)
digoal=> select *,convert_to(info,'euc_cn'),info::bytea from t order by t.info::bytea; -- 原始排序
id | info | convert_to | info
----+--------+----------------+----------------------
2 | 刘少奇 | \xc1f5c9d9c6e6 | \xe58898e5b091e5a587
1 | 刘德华 | \xc1f5b5c2bbaa | \xe58898e5beb7e58d8e
5 | 张学友 | \xd5c5d1a7d3d1 | \xe5bca0e5ada6e58f8b
3 | 郭富城 | \xb9f9b8bbb3c7 | \xe983ade5af8ce59f8e
4 | 黎明 | \xc0e8c3f7 | \xe9bb8ee6988e
(5 rows)
参考
1. http://www.postgresql.org/docs/9.1/static/multibyte.html
2. catalog : pg_collation
3. http://www.postgresql.org/docs/9.1/static/sql-createcollation.html
4. http://www.postgresql.org/docs/9.1/static/collation.html
5. http://en.wikipedia.org/wiki/Collation
RHEL5中文排序BUG
这其实是因为中文本地化数据并没有包含拼音排序的信息。即glibc中的local数据不包含对中文按拼音和笔画的排序
官方上bug的提交如下:
http://sources.redhat.com/ml/glibc-bugs/2007-02/msg00068.html
http://sourceware.org/bugzilla/show_bug.cgi?id=4024
现在给出解决方案,如下:
现在通过安装一种额外的中文本地化数据zh_CN@pinyin就可以让你在UTF-8和GB18030字符集下都可以正确的排序中文。
即locale-pinyin
locale-pinyin 介绍
按拼音排序的中文locale。
在类Unix中为了能够正确的排序电脑中的中文,你必须有能够正确对中文进行排序的locale数据。而glibc包含的locale数据一直没能对中文按照拼音或者笔画排序。本软件则是为你在电脑中增加一种按照拼音排序的中文locale。
locale-pinyin 安装
系统需求:
1. 系统需要有符合X/Open标准的localedef工具
2. 5.8.1以上的perl
安装:
$make
#make install
locale-pinyin 配置
为了生效,你必须正确配置locale环境变量。
推荐仅设置LC_COLLATE为本locale,比如:
LANG=zh_CN.UTF-8
LC_COLLATE=zh_CN@pinyin.utf8
注意:不要同时设置LC_ALL和LC_COLLATE,这样可能会导致本locale不能正确生效。
再跟踪glibc,发现官方在比较新的版本中已经把local_pinyin合并进去了(glibc2.6以上),如下:
http://sourceware.org/cgi-bin/cvsweb.cgi/libc/localedata/locales/iso14651_t1_pinyin?rev=1.1&content-type=text/x-cvsweb-markup&cvsroot=glibc
升级glibc是一个比较大的工程,涉及也比较广,建议在用rhel5时先打上local_pinyin补丁,或者可以考虑使用rhel6