之前我们解析frm中comment的时候,发现有部分编码比较怪,是"编码的编码",最终发现其原因竟然和字符集参数(character_set_client)有关. 所以本期我们来讲讲mysql字符集相关的参数.
我们可以在数据库中使用show variables like '%char%';来查询字符集相关的变量,发现有一堆:
(root@127.0.0.1) [(none)]> show variables like '%char%';
+--------------------------+--------------------------------------------------+
| Variable_name | Value |
+--------------------------+--------------------------------------------------+
| character_set_client | utf8 |
| character_set_connection | utf8 |
| character_set_database | utf8 |
| character_set_filesystem | binary |
| character_set_results | utf8 |
| character_set_server | utf8 |
| character_set_system | utf8 |
| character_sets_dir | /soft/mysql_5744/mysqlbase/mysql/share/charsets/ |
+--------------------------+--------------------------------------------------+好家伙,这么多,看着相当的懵. 从变量命名来看应该是作用域不同.
我们按顺序一个个的看(这里是按照变量名称的顺序)
先来看看character_set_client, 客户端发送SQL语句给服务端,网络传输的是二进制的数据,服务端接收到后就需要先解码,那么按照哪种编码来解码呢? 就由参数character_set_client设置.
比如客户端发送了包b"C\x00\x00\x00\x03create table db1.t20260730(id int,c1 varchar(20)) comment '\xe6\xb5\x8b\xe8\xaf\x95'",对于这个SQL服务端会使用character_set_client设置的编码来解码.
输入的编码由客户端决定,比如我的OS是LANG="en_US.UTF-8", 所以会先将"测试"使用utf8编码为'\xe6\xb5\x8b\xe8\xaf\x95',然后发送
如果设置的编码(character_set_client)和实际的编码不一致的话,就gg了. 比如:
utf8的"测试",即'\xe6\xb5\x8b\xe8\xaf\x95', 如果让服务端以latin1去解码的话就是'测试',如果该字段字符集是utf8的话,则存储的时候会再encode一下,变成b'\xc3\xa6\xc2\xb5\xe2\x80\xb9\xc3\xa8\xc2\xaf\xe2\x80\xa2',读者可使用ibd2sql去验证.看起来虽然怪怪的, 但再按照这一套编码过程解析的话还是能解析出来的.前提是知道character_set_client的值.(由于该值为会话变量,不会记录在元数据信息里面,所以实际上是无法解析的,当然也可以当作默认值来处理, character_set_client在5.5/5.6默认为latin1,在5.7默认是utf8,在8.0默认是utf8mb4).
mysql的frm中comment是以utf8存储的,如果character_set_client是latin1的话,就会出现comment乱码:
13:39:43 [root@ddcw21 ~]#mysqlfrm --diagnostic /data/mysql_5744/mysqldata/db1/t20260730.frm | grep -v "^#"
CREATE TABLE `db1`.`t20260730` (
`id` int(11) DEFAULT NULL,
`c1` varchar(60) DEFAULT NULL
) ENGINE=InnoDB COMMENT '测试';有的小伙伴可能会纳闷,既然老版本的mysql默认是latin1,那为啥select/show create table看到的都是正常的呢? 别急,等到character_set_results的时候就知道了.
简单来说character_set_client就是服务端认为的客户端的编码(The character set for statements that arrive from the client)
我们知道表的各字段都有字符集,不同的字符集是无法做比较的,不然会报错,比如
ERROR 1267 (HY000): Illegal mix of collations (latin1_swedish_ci,IMPLICIT) and (gbk_chinese_ci,IMPLICIT) for operation '>'那么如果不是对表的字段进行比较,而是对常量进行比较呢? 比如
(root@127.0.0.1) [(none)]> select 'a' > 'A';
+-----------+
| 'a' > 'A' |
+-----------+
| 1 |
+-----------+
1 row in set (0.00 sec)不但没报错,反而结果还是True. 也就是说服务端知道那两个字符串常量的字符集和排序规则,并使用该排序规则进行比较. 那么它咋知道是啥呢? 当然是我们的变量character_set_connection了,还有对应的排序规则变量collation_connection. 比如:
(root@127.0.0.1) [(none)]> select @@collation_connection;
+------------------------+
| @@collation_connection |
+------------------------+
| latin1_general_cs |
+------------------------+
1 row in set (0.00 sec)
(root@127.0.0.1) [(none)]> select 'a' > _gbk 'A';
ERROR 1267 (HY000): Illegal mix of collations (latin1_general_cs,COERCIBLE) and (gbk_chinese_ci,COERCIBLE) for operation '>'从报错我们就可以看出: 使用latin1_general_cs的'a'无法和使用gbk_chinese_ci的'b'进行比较.
字符串常量可以在其前面加上 _charset,后面加上 collate collation的方式来指定其排序规则, 比如 _utf8mb4'a' COLLATE utf8mb4_bin; 字段和常量比较则转常量为字段字符集
简单来说character_set_connection就是字符串常量使用的字符集.
character_set_database表示当前数据库的默认字符集,如果没有use任何数据库,则和character_set_server值相同. 可以修改该值,但没啥意义,就一条warning:"Updating 'character_set_database' is deprecated. It will be made read-only in a future release."
collation_database表示当前数据库的默认排序规则.
character_set_filesystem表示load data/select into outfile时操作系统上的文件名的编码方式. 比如:
select * from db1.t20260730_01 into outfile '/tmp/t20260730/测试.sql';
-- 对应二进制包如下:
-- b"H\x00\x00\x00\x03select * from db1.t20260730_01 into outfile '/tmp/t20260730/\xe6\xb5\x8b\xe8\xaf\x95.sql'"会先根据character_set_client将'/tmp/t20260730/\xe6\xb5\x8b\xe8\xaf\x95.sql'做decode,然后使用character_set_filesystem指定的编码在OS创建相关文件. 如果os上的LANG编码也一样的话,就能正常看到文件(其实还和终端编码有关,不然看到的是乱码.):
14:59:55 [root@ddcw21 ibd2sql-ibd2sql-v2.x]#ll /tmp/t20260730/*.sql
-rw-rw-rw- 1 mysql mysql 6 Jul 30 14:54 /tmp/t20260730/????.sql
14:59:56 [root@ddcw21 ibd2sql-ibd2sql-v2.x]#LANG='zh_CN.gbk'
15:00:00 [root@ddcw21 ibd2sql-ibd2sql-v2.x]#ll /tmp/t20260730/*.sql
-rw-rw-rw- 1 mysql mysql 6 7月 30 14:54 /tmp/t20260730/测试.sql简单来说character_set_filesystem表示服务器上的文件名的编码方式.
character_set_client表示服务端认为客户端的编码, 而character_set_results表示服务端返回的时候需要怎么编码.
比如上面使用latin1创建的表db1.t20260730,对应的comment存储的则是b'\xc3\xa6\xc2\xb5\xe2\x80\xb9\xc3\xa8\xc2\xaf\xe2\x80\xa2', 会先使用utf8解码(frm使用的是utf8)得到'测试',然后将其编码为character_set_results并返回给客户端.
# 当character_set_results=latin1时,返回内容如下:
b"\x98\x00\x00\x04\tt20260730\x8dCREATE TABLE `t20260730` (\n `id` int(11) DEFAULT NULL,\n `c1` varchar(20) DEFAULT NULL\n) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='\xe6\xb5\x8b\xe8\xaf\x95'"
由于终端是使用的utf8编码,则能够将'\xe6\xb5\x8b\xe8\xaf\x95'解码为'测试', 所以我们看到的是正常的.
# 当character_set_results=utf8时,返回内容如下(其实就是存储的内容):
b"\xa0\x00\x00\x04\tt20260730\x95CREATE TABLE `t20260730` (\n `id` int(11) DEFAULT NULL,\n `c1` varchar(20) DEFAULT NULL\n) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='\xc3\xa6\xc2\xb5\xe2\x80\xb9\xc3\xa8\xc2\xaf\xe2\x80\xa2'"
终端使用utf8进行解码,得到'测试'
# 当character_set_results=gbk时,返回内容如下:
b"\x99\x00\x00\x04\tt20260730\x8eCREATE TABLE `t20260730` (\n `id` int(11) DEFAULT NULL,\n `c1` varchar(20) DEFAULT NULL\n) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='???\xa8\xa8??'"
由于部分字符不在gbk里面,故使用?代替简单来说character_set_results就是服务端返回数据时使用的编码.
character_set_server表示服务端默认字符集, 就是create database时使用的字符集.直接看例子吧:
(root@127.0.0.1) [(none)]> set character_set_server=gbk;
Query OK, 0 rows affected (0.00 sec)
(root@127.0.0.1) [(none)]> create database db20260730_gbk;
Query OK, 1 row affected (0.01 sec)
(root@127.0.0.1) [(none)]> show create database db20260730_gbk;
+----------------+------------------------------------------------------------------------+
| Database | Create Database |
+----------------+------------------------------------------------------------------------+
| db20260730_gbk | CREATE DATABASE `db20260730_gbk` /*!40100 DEFAULT CHARACTER SET gbk */ |
+----------------+------------------------------------------------------------------------+
1 row in set (0.00 sec)
(root@127.0.0.1) [(none)]> set character_set_server=latin1;
Query OK, 0 rows affected (0.00 sec)
(root@127.0.0.1) [(none)]> create database db20260730_latin1;
Query OK, 1 row affected (0.00 sec)
(root@127.0.0.1) [(none)]> show create database db20260730_latin1;
+-------------------+------------------------------------------------------------------------------+
| Database | Create Database |
+-------------------+------------------------------------------------------------------------------+
| db20260730_latin1 | CREATE DATABASE `db20260730_latin1` /*!40100 DEFAULT CHARACTER SET latin1 */ |
+-------------------+------------------------------------------------------------------------------+
1 row in set (0.00 sec)对应的排序规则变量为:collation_server
我们刚才不是说存储的元数据信息使用的utf8编码的么, 其实就是这个变量指定的, 所以这个变量一旦变更,影响巨大,故为只读(实际上无法修改).
安装字符集的目录. 不可修改. 其目录下是一些字符集的排序规则和对应的utf8编码文件.
其实ibd2sql也有类似的编码转换文件. 毕竟mysql支持的编码还是挺多的,而py并不都自带.
至此字符集相关的变量就讲完了.比较乱,我们简单总结下:
变量 | 描述 |
|---|---|
character_set_client | 服务端收到客户端的包时,使用的解码字符集 |
character_set_connection | 服务端处理一些没有字符集的常量时,使用的字符集 |
character_set_database | 显示当前数据库的字符集 |
character_set_filesystem | 对应的文件系统上文件名的编码方式 |
character_set_results | 服务端返回数据给客户端时,使用的编码 |
character_set_server | 服务端默认字符集,就是create database的默认字符集 |
character_set_system | 元数据信息使用的编码,固定为utf8.无法修改 |
character_sets_dir | 字符集目录.无法修改. |
再来画个图整理下

参考:
https://dev.mysql.com/doc/refman/8.0/en/server-system-variables.html
https://dev.mysql.com/doc/refman/8.0/en/charset-connection.html
https://dev.mysql.com/doc/refman/8.0/en/charset-introducer.html
https://dev.mysql.com/doc/refman/8.0/en/charset-metadata.html
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。