MySQL数据库字符集问题小结
这两天尝试着帮COS中文论坛做一个SQL同步脚本,以便将phpwind论坛的数据导入bbpress论坛。由于两个论坛的数据库中使用了不同的字符集,折腾了好久都只得到一堆乱码。因此仔细研究了一番MySQL处理不同字符集的机制,所幸终于解决问题,于是将研究结果在此做一小结。
本文只简单总结有关字符集(Character Set)的配置,对于校对规则(Collation)的相关问题,暂不考虑。
为了使用的灵活,MySQL数据库在处理字符集问题时,允许对多个不同层次和环节分别进行字符集的定义。从数据库结构的层次上看,包括:服务器(server)、数据库(database)、表(table)、列(column)。从一次SQL查询的处理环节上看,又包括:客户端(Client)、连接(Connection)、结果(Result)。
数据库结构的四个层次的字符集定义,是在创建(数据库或表)时指定的。若未指定,则将缺省使用上一层次的字符集。例如,创建数据库时未指定字符集,则会使用服务器的字符集。而在这些层次中,最终决定存储结果与查询结果的,还是列的字符集。
而对于SQL查询的处理环节,MySQL则会根据每个环节之间的字符集差别分别进行转换。而正是由于可能出现的多次转换,导致了设置不当时经常会出现问题。下面举例来说明:
首先看看MySQL中字符集相关的环境变量:
1mysql> show variables like 'char%';
2+--------------------------+----------------------------+
3| Variable_name | Value |
4+--------------------------+----------------------------+
5| character_set_client | utf8 |
6| character_set_connection | utf8 |
7| character_set_database | utf8 |
8| character_set_filesystem | binary |
9| character_set_results | utf8 |
10| character_set_server | utf8 |
11| character_set_system | utf8 |
12| character_sets_dir | /usr/share/mysql/charsets/ |
13+--------------------------+----------------------------+
148 rows in set (0.31 sec)另外,与校对规则相关的环境变量(这里仅列出,不做进一步讨论):
1mysql> show variables like 'coll%';
2+----------------------+-------------------+
3| Variable_name | Value |
4+----------------------+-------------------+
5| collation_connection | utf8_swedish_ci |
6| collation_database | utf8_swedish_ci |
7| collation_server | utf8_swedish_ci |
8+----------------------+-------------------+
93 rows in set (0.01 sec)以上环境变量是在我的英文Ubuntu 9.10环境下默认安装的MySQL的字符集设置。可以发现,其中除了操作系统(character_set_system)使用了utf8外,其他的字符集全部采用了utf8(一种单字节的字符集)。
接下来,在这个环境下,我们分别创建两个数据库(test_utf8和test_utf8):
1mysql> create database test_utf8;
2Query OK, 1 row affected (0.13 sec)
3
4mysql> create database test_utf8 character set utf8;
5Query OK, 1 row affected (0.03 sec)然后,我们可以用“show create database”命令分别查看一下这两个库:
1mysql> show create database test_utf8;
2+-------------+----------------------------------------+
3| Database | Create Database |
4+-------------+----------------------------------------+
5| test_utf8 | CREATE DATABASE `test_utf8`
6 /*!40100 DEFAULT CHARACTER SET utf8 */ |
7+-------------+----------------------------------------+
81 row in set (0.02 sec)
9
10mysql> show create database test_utf8;
11+-----------+------------------------------------------+
12| Database | Create Database |
13+-----------+------------------------------------------+
14| test_utf8 | CREATE DATABASE `test_utf8`
15 /*!40100 DEFAULT CHARACTER SET utf8 */ |
16+-----------+------------------------------------------+
171 row in set (0.00 sec)可以看到,创建test_utf8时,没有指定字符集,它使用了服务器的缺省字符集utf8。
然后,我们分别在两个库中,各自创建两个表:
1mysql> create table test_utf8.test (test varchar(10));
2Query OK, 0 rows affected (0.38 sec)
3
4mysql> create table test_utf8.test_utf8 (test varchar(10)) character set utf8;
5Query OK, 0 rows affected (0.17 sec)
6
7mysql> create table test_utf8.test (test varchar(10));
8Query OK, 0 rows affected (0.14 sec)
9
10mysql> create table test_utf8.test_utf8 (test varchar(10)) character set utf8;
11Query OK, 0 rows affected (0.11 sec)用“show create table”来查看这四个表:
1mysql> show create table test_utf8.test;
2+-------+------------------------------+
3| Table | Create Table |
4+-------+------------------------------+
5| test | CREATE TABLE `test` (
6 `test` varchar(10) DEFAULT NULL
7 ) ENGINE=MyISAM DEFAULT CHARSET=utf8 |
8+-------+------------------------------+
91 row in set (0.05 sec)
10
11mysql> show create table test_utf8.test_utf8;
12+-----------+----------------------------+
13| Table | Create Table |
14+-----------+----------------------------+
15| test_utf8 | CREATE TABLE `test_utf8` (
16 `test` varchar(10) DEFAULT NULL
17 ) ENGINE=MyISAM DEFAULT CHARSET=utf8 |
18+-----------+----------------------------+
191 row in set (0.00 sec)
20
21mysql> show create table test_utf8.test;
22+-------+--------------------------------+
23| Table | Create Table |
24+-------+--------------------------------+
25| test | CREATE TABLE `test` (
26 `test` varchar(10) DEFAULT NULL
27 ) ENGINE=MyISAM DEFAULT CHARSET=utf8 |
28+-------+--------------------------------+
291 row in set (0.03 sec)
30
31mysql> show create table test_utf8.test_utf8;
32+-------------+-------------------------------+
33| Table | Create Table |
34+-------------+-------------------------------+
35| test_utf8 | CREATE TABLE `test_utf8` (
36 `test` varchar(10) DEFAULT NULL
37 ) ENGINE=MyISAM DEFAULT CHARSET=utf8 |
38+-------------+-------------------------------+
391 row in set (0.00 sec)同样可以看到,对于没有指定字符集时,所创建的表会缺省使用该数据库的字符集。
下面,我们分别往每个表里插入一条记录(该字段的值为单个汉字“阿”):
1mysql> insert into test_utf8.test values ('阿');
2Query OK, 1 row affected (0.00 sec)
3
4mysql> insert into test_utf8.test_utf8 values ('阿');
5Query OK, 1 row affected (0.00 sec)
6
7mysql> insert into test_utf8.test values ('阿');
8Query OK, 1 row affected (0.08 sec)
9
10mysql> insert into test_utf8.test_utf8 values ('阿');
11Query OK, 1 row affected (0.00 sec)为说明查询过程(包括插入与查询等操作)中的字符集关系,下面我们修改一下字符集:
1mysql> set names 'utf8';
2Query OK, 0 rows affected (0.00 sec)此时,我们可以看看环境变量:
1mysql> show variables like 'char%';
2+--------------------------+----------------------------+
3| Variable_name | Value |
4+--------------------------+----------------------------+
5| character_set_client | utf8 |
6| character_set_connection | utf8 |
7| character_set_database | utf8 |
8| character_set_filesystem | binary |
9| character_set_results | utf8 |
10| character_set_server | utf8 |
11| character_set_system | utf8 |
12| character_sets_dir | /usr/share/mysql/charsets/ |
13+--------------------------+----------------------------+
148 rows in set (0.29 sec)其中的客户端(character_set_client)、连接(character_set_connection)、结果(character_set_results)都变成了utf8。
然后,我们再向四个表中分别插入一条记录,还是插入单个汉字“阿”:
1mysql> insert into test_utf8.test values ('阿');
2Query OK, 1 row affected, 1 warning (0.00 sec)
3
4mysql> insert into test_utf8.test_utf8 values ('阿');
5Query OK, 1 row affected (0.00 sec)
6
7mysql> insert into test_utf8.test values ('阿');
8Query OK, 1 row affected (0.00 sec)
9
10mysql> insert into test_utf8.test_utf8 values ('阿');
11Query OK, 1 row affected, 1 warning (0.00 sec)最后,我们分别查询每个表中的数据(包括其长度与十六进制值):
1mysql> select test, length(test), hex(test) from test_utf8.test;
2+--------+--------------+-----------+
3| test | length(test) | hex(test) |
4+--------+--------------+-----------+
5| 阿 | 3 | E998BF |
6| ? | 1 | 3F |
7+--------+--------------+-----------+
82 rows in set (0.75 sec)
9
10mysql> select test, length(test), hex(test) from test_utf8.test_utf8;
11+--------+--------------+--------------+
12| test | length(test) | hex(test) |
13+--------+--------------+--------------+
14| 阿 | 6 | C3A9CB9CC2BF |
15| 阿 | 3 | E998BF |
16+--------+--------------+--------------+
172 rows in set (0.03 sec)
18
19mysql> select test, length(test), hex(test) from test_utf8.test;
20+--------+--------------+--------------+
21| test | length(test) | hex(test) |
22+--------+--------------+--------------+
23| 阿 | 6 | C3A9CB9CC2BF |
24| 阿 | 3 | E998BF |
25+--------+--------------+--------------+
262 rows in set (0.00 sec)
27
28mysql> select test, length(test), hex(test) from test_utf8.test_utf8;
29+--------+--------------+-----------+
30| test | length(test) | hex(test) |
31+--------+--------------+-----------+
32| 阿 | 3 | E998BF |
33| ? | 1 | 3F |
34+--------+--------------+-----------+可以看出,造成存储结果差异的不是数据库的字符集,而是表的字符集(注:其实应该是字段的字符集,不过由于示例中没有指定字段的字符集,所以字段缺省使用了表的字符集)。
另外,还可以看到,在第一次插入数据(环境为utf8)时,只有表的字符集也为utf8时,才正确存入了文字“阿”(E998BF三个字节为“阿”字的UTF8编码)。而第二次插入数据(环境为utf8)时,只有表的字符集也为utf8时,才正确存入了文字“阿”。因此,在使用MySQL数据库时,应确保环境(character_set_client、character_set_connection和character_set_results)与数据库表的字符集相一致。
而在字符集选择上,虽然环境与表都使用utf8时也能正确写入和读取汉字,但在使用MySQL函数length获取字符串长度时,得到的却是字节数3。所以,如果要很好地支持中文,最好避免采用MySQL的默认字符集utf8,而尽可能采用能够支持中文的字符集(GB2312、GBK、GB18030或UTF8)。GB系列的标准,每个汉字占用两个字节,相比UTF8会节省些空间。但是由于与其他国家的字符集有重叠,不利于多语言混用。因此要支持多语言,最好的还是使用UTF8字符集。
至于为什么在环境与表的字符集不同时,存储的结果会有那么大变化,这里可以简单分析一下:情况一,当环境为utf8、表为utf8时,由于系统是utf8,在通过MySQL的查询连接(Connection)时,汉字被从utf8当成utf8处理,即被当成3个字节;而当存入utf8表时,这三个字节被分别转换成utf8格式,由于每个字节的ASCII码都大于127,因此都被转成了两个字节,共计六个字节。情况二、当环境为utf8、表为utf8时,从系统到环境时字符集一致没有做任何转换,仍然是单个字符(3个字节),而当这个字符存入utf8表时,由于utf8的范围(0~127)没法存储下汉字,因此实际存入数据库的就成了个问号(ASCII值为3F)。
对于情况二,由于数据库里存入的是问号,因此无论查询时使用何种配置都无法正确读出结果。而对于情况一,可以采用如下的方式将结果转换后读出:
1mysql> select convert(cast(convert(test using utf8) as binary)
2-> using utf8) from test_utf8.test_utf8 limit 1;
3+----------------------------------------------------------------+
4| convert(cast(convert(test using utf8) as binary) using utf8) |
5+----------------------------------------------------------------+
6| 阿 |
7+----------------------------------------------------------------+
81 row in set (0.04 sec)这里经过三次转换:第一次将utf8表中的内容转为utf8,第二次将该字符串转为二进制格式,第三次将该二进制串以utf8格式转出。具体转换过程可参考:
1mysql> select
2->
3-> test as s1,
4-> hex(test) as h1,
5-> charset(test) as c1,
6->
7-> convert(test using utf8) as s2,
8-> hex(convert(test using utf8)) as h2,
9-> charset(convert(test using utf8)) as c2
10->
11-> from test_utf8.test_utf8 limit 1;
12+--------+--------------+------+--------+--------+--------+
13| s1 | h1 | c1 | s2 | h2 | c2 |
14+--------+--------------+------+--------+--------+--------+
15| 阿 | C3A9CB9CC2BF | utf8 | 阿 | E998BF | utf8 |
16+--------+--------------+------+--------+--------+--------+
171 row in set (0.00 sec)
18
19mysql>
20mysql> select
21->
22-> cast(convert(test using utf8) as binary) as s3,
23-> hex(cast(convert(test using utf8) as binary)) as h3,
24-> charset(cast(convert(test using utf8) as binary)) as c3,
25->
26-> convert(cast(convert(test using utf8) as binary) using utf8) as s4,
27-> hex(convert(cast(convert(test using utf8) as binary) using utf8)) as h4,
28-> charset(convert(cast(convert(test using utf8) as binary) using utf8)) as c4
29->
30-> from test_utf8.test_utf8 limit 1;
31+------+--------+--------+------+--------+------+
32| s3 | h3 | c3 | s4 | h4 | c4 |
33+------+--------+--------+------+--------+------+
34| 阿 | E998BF | binary | 阿 | E998BF | utf8 |
35+------+--------+--------+------+--------+------+
361 row in set (0.00 sec)