暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

如何避免数据库乱码?一文剖析MySQL字符集与Collation(上)

279

引言


在你使用MySQL的时候,有没有遇到过字符串出现乱码的情况?

想查一个英文字符串结果查出来了特殊字符?

字符串排序有时小写在前有时大写在前?

如果在使用MySQL时不在意字符集、Collation等相关配置,那么就可能会碰到这些问题,本文将以几个例子作为开篇。


由于篇幅较长,本文将分为上、下两篇进行介绍,组织结构如下:

第一部分(上篇:概念介绍与MySQL的配置项)

  • 引言
  • 字符集(Charset)和Collation是什么?
  • 查看MySQL支持的Charset和Collation
  • Unicode与UTF-8
  • 配置Charset和Collation

第二部分(下篇:字符集转换与Unicode、Binary字符集的排序算法)

  • 转换Charset
  • Unicode字符串排序算法
  • binary Charset与_bin Collation
  • 总结
▶︎ 例1
一个客户端的错误配置,可能导致字符串二进制存储错乱,并且导致其他正常客户端查询出现乱码,如下:
-- 1.创建表t1,有两列,一列使用utf8mb4字符集,一列使用latin1字符集
mysql> create table t1 (a char(5character set utf8mb4, b char(5character set latin1);

-- 2.客户端实际使用utf8mb4字符集,这里模拟客户端在服务端错误配置,并插入数据
mysql> set names latin1;
-- 哪怕列b的latin1字符集没有字符'张'也能成功插入,正常情况下都无法插入该列,从这里就开始出错了
mysql> insert into t1 values('张','张');

-- 3.正常客户端查询数据
mysql> set names utf8mb4;
-- a列(utf8mb4字符集)内容完全错乱,b列(latin1字符集)内容竟是'张'的utf8mb4编码
mysql> select a,hex(a),b,hex(b) from t1;
+--------+--------------+--------+--------+
| a      | hex(a)       | b      | hex(b) |
+--------+--------------+--------+--------+
| å¼     | C3A5C2BCC2A0 | å¼     | E5BCA0 |
+--------+--------------+--------+--------+

▶︎ 例2

用等值条件查询字符串列时,多返回了一个完全不同的字符串,难道MySQL出Bug了?其实不是的,字符串的比较和Collation配置息息相关,稍不注意就可能得到令人费解的结果。

-- 1.创建表t1,包含一列,默认使用utf8mb4字符集和utf8mb4_0900_ai_ci Collation
mysql> create table t1 (a char(5));

-- 2.插入两行数据
mysql> insert into t1 values ('ß'),('ss');

-- 3.按等值条件查询该表,结果编码完全不同的两个字符串都返回了!
mysql> select a,hex(a) from t1 where a='ss';
+------+--------+
| a    | hex(a) |
+------+--------+
| ß    | C39F   |
| ss   | 7373   |
+------+--------+

-- 4.换一个Collation试试,又正常了
mysql> select a,hex(a) from t1 where a='ss' collate utf8mb4_0900_as_cs;
+------+--------+
| a    | hex(a) |
+------+--------+
| ss   | 7373   |
+------+--------+

▶︎ 例3

下面这个例子展现了Collation对于字符串排序的影响,怎么有时候是小写优先,有时候是大写优先,不同字符串的先后顺序是怎么确定的?这都是配置的Collation决定的。

-- 1.创建表t1,包含一列,默认使用utf8mb4字符集和utf8mb4_0900_ai_ci Collation
mysql> create table t1 (a char(5));

-- 2.插入两行数据
mysql> insert into t1 values ('abc'),('ABC');

-- 3.返回字符串排序结果,小写优先
mysql> select * from t1 order by a;
+------+
| a    |
+------+
| abc  |
| ABC  |
+------+

-- 4.换一个Collation,成大写优先了
mysql> select * from t1 order by a collate utf8mb4_0900_bin;
+------+
| a    |
+------+
| ABC  |
| abc  |
+------+

通过上面3个例子可以看出,我们在MySQL中使用字符串时,字符集和Collation是非常重要的配置项,一旦配置错误就可能无法按照预期进行使用。当我们在使用MySQL查看表定义时,经常可以看到如下例所示的CHARSET、COLLATE等字眼,所以相信大家对于这些字眼并不陌生。然而很多时候我们并不清楚这些字符集、Collation的具体含义,也不知道究竟如何配置才好,要么跟着默认配置走,要么从已有库表的定义那里copy过来,但是这些“祖传配置”真的适合当前的应用么?

合理地选择字符集、Collation、了解字符串如何比较将能很大程度上帮我们避免前文例子中所描述的问题,因此本文就将对此展开介绍,希望能对你有帮助。

mysql> show create table t1\G
*************************** 1. row ***************************
       Table: t1
Create TableCREATE TABLE `t1` (
  `a` char(10CHARACTER SET utf8mb3 COLLATE utf8mb3_general_ci DEFAULT NULL,
  `b` varchar(10CHARACTER SET latin2 COLLATE latin2_general_ci DEFAULT NULL,
  `c` char(10COLLATE utf8mb4_0900_as_cs DEFAULT NULL,
  `d` char(5CHARACTER SET latin1 COLLATE latin1_bin DEFAULT NULL,
  `e` varchar(5CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci DEFAULT NULL
ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_as_cs

MySQL本身支持很多种字符集(Character Set)与Collation,本身的配置有时候令用户眼花缭乱。本文将基于MySQL 8.0,围绕字符集与Collation由浅入深进行介绍,希望能够回答以下几个问题:
  1. 字符集(Charset)和Collation是什么?

  2. 如何配置不同层次的字符集和Collation?它们的影响范围有多大?

  3. 最常用的Unicode字符集是如何存储、比较字符串的?

  4. Binary字符集和非二进制字符集的_bin Collation该如何选择?

01

字符集(Charset)和Collation是什么?

字符集(Character Set)是一组符号和编码。Collation是一组用于比较字符集中字符的规则。让我们以MySQL默认的utf8mb4字符集和utf8mb4_0900_ai_ci Collation为例,明确这个区别。如下是六个字符在utf8mb4字符集中的编码,utf8mb4字符集允许字符的编码是非定长的,长度可以是1~4 bytes,具体的编码方式将在“Unicode与UTF-8”章节介绍。

上面就可以当作一个字符到编码的映射,也就是字符集。字符集的作用是提供字符到编码的映射,但是不定义字符之间的比较关系,而这一部分工作就是由Collation定义的。

对于上面6个字符,如果我们仅看二进制编码对应的数值大小进行比较那么它们的大小关系就是“E<a<e<ắ<ế<𝔸”,这是最简单的比较方法,被称为二进制Collation,在MySQL中以“_bin”结尾。

但是很多时候,我们希望不论大小写,a都应该排在e和E前面,对于欧洲一些语言或者中文拼音来说,不论字母上面有没有音调符号,a也应该排在e前面,于是更加精细的Collation就应运而生,它包含了很多契合人类语言习惯的规则定义。比如上面例子中6个字符在utf8mb4_0900_ai_ci Collation看来,它们的大小关系是“a=ắ=𝔸<E=e=ế”,在该Collation里,所有字符都忽略了音调、大小写再进行比较,这就比二进制Collation复杂了一些。

上面只是用utf8mb4字符集中的6个字符举了个例子,实际上其内几乎包含了世界上各个语言的文字,这些字符之间的定制化比较规则将更加复杂,不光是音调、大小写,有时还会有多字符映射(例如utf8mb4_0900_ai_ci中 ß = ss)。为此Unicode专门定义了字符串比较算法,解释了如何进行字符的比较,MySQL也是据此实现的自己的比较算法,这一部分将在“Unicode字符串比较算法”详细介绍。

从上面的描述我们可以看出,字符集是符号到编码一一映射的集合,一组Collation是对字符进行比较的一系列规则,一个字符集上可以有很多组Collation。为方便描述,后文使用Charset代表字符集。

02


查看MySQL支持的Charset和Collation

查看Charset

MySQL支持了很多种Charset以及Collation,可以使用如下语句查看支持的Charset,其中Charset列代表字符集的名称、Description列代表描述、Default Collation列代表该字符集的默认Collation、Maxlen列代表该字符集中最长编码的字节数:
mysql> SHOW CHARACTER SET-- 对应视图INFORMATION_SCHEMA.CHARACTER_SETS
+----------+---------------------------------+---------------------+--------+
| Charset  | Description                     | Default collation   | Maxlen |
+----------+---------------------------------+---------------------+--------+
| big5     | Big5 Traditional Chinese        | big5_chinese_ci     |      2 |
| binary   | Binary pseudo charset           | binary              |      1 |
...
| latin1   | cp1252 West European            | latin1_swedish_ci   |      1 |
...
| ucs2     | UCS-2 Unicode                   | ucs2_general_ci     |      2 |
...
| utf8mb3  | UTF-8 Unicode                   | utf8mb3_general_ci  |      3 |
| utf8mb4  | UTF-8 Unicode                   | utf8mb4_0900_ai_ci  |      4 |
...

查看Collation

MySQL 8.0默认使用的Charset是utf8mb4,这里以它为例使用如下语句查看该字符集可以使用的Collation,其中Compiled列代表该Collation是否在MySQL源码中实现:
mysql> SHOW COLLATION WHERE Charset = 'utf8mb4'-- 对应视图INFORMATION_SCHEMA.COLLATIONS
+----------------------------+---------+-----+---------+----------+---------+---------------+
| Collation                  | Charset | Id  | Default | Compiled | Sortlen | Pad_attribute |
+----------------------------+---------+-----+---------+----------+---------+---------------+
| utf8mb4_0900_ai_ci         | utf8mb4 | 255 | Yes     | Yes      |       0 | NO PAD        |
| utf8mb4_0900_as_ci         | utf8mb4 | 305 |         | Yes      |       0 | NO PAD        |
| utf8mb4_0900_as_cs         | utf8mb4 | 278 |         | Yes      |       0 | NO PAD        |
| utf8mb4_0900_bin           | utf8mb4 | 309 |         | Yes      |       1 | NO PAD        |
| utf8mb4_bin                | utf8mb4 |  46 |         | Yes      |       1 | PAD SPACE     |
...
| utf8mb4_general_ci         | utf8mb4 |  45 |         | Yes      |       1 | PAD SPACE     |
...
| utf8mb4_spanish2_ci        | utf8mb4 | 238 |         | Yes      |       8 | PAD SPACE     |
...
| utf8mb4_unicode_520_ci     | utf8mb4 | 246 |         | Yes      |       8 | PAD SPACE     |
| utf8mb4_unicode_ci         | utf8mb4 | 224 |         | Yes      |       8 | PAD SPACE     |
...
| utf8mb4_zh_0900_as_cs      | utf8mb4 | 308 |         | Yes      |       0 | NO PAD        |
+----------------------------+---------+-----+---------+----------+---------+---------------+

在MySQL中,Charset和Collation遵循以下规则:

  • 同一个Collation不能被多个Charset使用,也就是Collation依附于某一个Charset
  • 每个Charset都有一个默认Collation

Collation的Pad_attribute

查看Collation信息时可以发现有一个Pad_attribute列,其值为“NO PAD”或“PAD SPACE”,这定义了Collation对待字符串尾部空格的态度。MySQL中大部分Collation该属性为“PAD SPACE”,基于UCA 9.0.0(名称中带0900字样)实现的Collation该属性为“NO PAD”:

  • PAD SPACE:当比较(不包括LIKE)两个字符串的时候,尾部空格将被忽略,例如认为"a"与"a "相等;
  • NO PAD:当比较两个字符串的时候,尾部空格不能忽略,例如认为"a"与"a "不等;
  • 使用LIKE操作符时,不受该值影响,尾部空格不能忽略。

mysql> SELECT COLLATION_NAME, PAD_ATTRIBUTE
       FROM INFORMATION_SCHEMA.COLLATIONS
       WHERE COLLATION_NAME LIKE 'utf8mb4%bin';
+------------------+---------------+
| COLLATION_NAME   | PAD_ATTRIBUTE |
+------------------+---------------+
| utf8mb4_bin      | PAD SPACE     |
| utf8mb4_0900_bin | NO PAD        |
+------------------+---------------+

-- 1. 具有'PAD SPACE'属性的utf8mb4_bin认为'a '='a'
mysql> SELECT 'a ' = 'a' COLLATE utf8mb4_bin;
+--------------------------------+
| 'a ' = 'a' COLLATE utf8mb4_bin |
+--------------------------------+
|                              1 |
+--------------------------------+
-- 2. 具有'NO PAD'属性的utf8mb4_0900_bin认为'a '!='a'
mysql> SELECT 'a ' = 'a' COLLATE utf8mb4_0900_bin;
+-------------------------------------+
| 'a ' = 'a' COLLATE utf8mb4_0900_bin |
+-------------------------------------+
|                                   0 |
+-------------------------------------+
-- 3. LIKE操作符不受'PAD SPACE'影响,尾部空格不可忽略
mysql> SELECT 'a ' LIKE 'a' COLLATE utf8mb4_bin;
+-----------------------------------+
| 'a ' LIKE 'a' COLLATE utf8mb4_bin |
+-----------------------------------+
|                                 0 |
+-----------------------------------+


Collation的命名规则

Collation名称以其关联的Charset名称开头,通常后面跟着一个或多个后缀,表示其他特征。例如,utf8mb4_0900_ai_ci和latin1_swedish_ci分别是utf8mb4和latin1的Collation。binary Charset只有一个Collation,也命名为binary,没有后缀。
● 对于特定语言的Collation包括一个区域代码或语言名称。例如,utf8mb4_tr_0900_ai_ci和utf8mb4_hu_0900_ai_ci中的_tr和_hu分别代表使用土耳其语和匈牙利语的规则对utf8mb4中的字符进行排序。
● Collation后缀表示Collation是否区分大小写、音调、平(片)假名,或者是二进制的,下表显示了用于表示这些特征的后缀。如果Collation名称中不包含_ai或_as,那么名称中的_ci隐喻着_ai(例如utf8mb4_unicode_ci既不区分音调也不区分大小写),名称中的_cs隐喻着_as(例如latin1_general_cs既区分音调也区分大小写)

● Unicode字符集的Collation名称可能包括版本号,以表示Collation基于哪个版本的Unicode排序算法(UCA,后文会详细介绍)。名称中不含版本号的Collation则默认基于4.0.0版本的UCA。例如:

  • utf8mb4_0900_ai_ci基于UCA 9.0.0
http://www.unicode.org/Public/UCA/9.0.0/allkeys.txt
  • utf8mb4_unicode_520_ci基于UCA 5.2.0
http://www.unicode.org/Public/UCA/5.2.0/allkeys.txt
  • utf8mb4_unicode_ci基于UCA 4.0.0
http://www.unicode.org/Public/UCA/4.0.0/allkeys-4.0.0.txt

03


Unicode与UTF-8

为了便于阅读后文,这里先介绍一下Unicode标准和UTF-8编码。

Unicode标准

Unicode标准由非盈利组织Unicode联盟维护(https://home.unicode.org/),致力于整理和编码世界上大部分文字系统。Unicode标准最新版为2022年9月发布的15.0.0版本,收录超过14万字符,每个字符都被分配了独一无二的码点(数值编号)。Unicode已经成为ISO国际标准的一部分,最常见的Unicode编码格式有与ASCII兼容的UTF-8和与UCS-2兼容的UTF-16。
Unicode标准将编码空间划分为17个平面,编号从0到16,其中第0平面称为基本多文种平面(BMP,U+0000到U+FFFF),而第1到16平面被称为辅助平面(U+10000到U+10FFFF),这些平面与BMP平面一起至少需要21bit的编码空间,略少于3个字节。BMP平面的码点可以使用单个UTF-16编码单位(2字节)表示,或者使用1~3个字节的UTF-8进行编码;辅助平面的码点在UTF-8中使用4个字节进行编码,在UTF-16中使用4个字节进行编码。

UTF-8编码

出于节省空间等目的,实际上对Unicode标准进行编码有不同实现方式,Unicode的实现方式被称为Unicode转换格式(Unicode Transformation Format,简称为UTF),最常见的当属UTF-8编码,其他实现方式还包括UTF-16(字符用两个字节或四个字节表示)和UTF-32(字符用四个字节表示)等,下面给出一些示例:

可以看出Unicode给字符分配的原始码点(编号)和UTF-8具体编码还是有很大不同的,这是因为UTF-8编码以8bit(1字节)为单位对Unicode字符进行变长编码,具体转换规则如下表所示(单元格内首行为16进制表示,次行为2进制表示):

可以看出转换规则其实很直观,就是把Unicode码点对应的数值用3个字节存下来(最多用21个bit),然后根据自己所处的范围将bit位依次填入UTF-8对应空位即可。

汉字的码点空间如下,utf16对于BMP平面字符(在下表中Unicode码点仅需两字节的部分)会使用2字节编码,而utf8对于BMP平面的汉字需要使用3字节编码,非BMP平面的汉字utf16和utf8都需要4字节编码。所以,如果存储字符基本都是汉字时,utf16字符集的编码长度始终优于或等于utf8字符集的编码长度,可以有效帮助减少存储空间,最多减少1/3的存储空间。

注:汉字的码点空间取自 🔗https://www.zhangxinxu.com/study/201611/chinese-language-unicode-range.html

[1] 字符集地址
基本汉字:https://www.zhangxinxu.com/study/201611/show-character-by-charcode.php?range=4E00-9FA5
基本汉字补充:https://www.zhangxinxu.com/study/201611/show-character-by-charcode.php?range=9FA6-9FCB
扩展A:https://www.zhangxinxu.com/study/201611/show-character-by-charcode.php?range=3400-4DB5
扩展B:https://www.zhangxinxu.com/study/201611/show-character-by-charcode.php?range=20000-2A6D6
扩展C:https://www.zhangxinxu.com/study/201611/show-character-by-charcode.php?range=2A700-2B734
扩展D:https://www.zhangxinxu.com/study/201611/show-character-by-charcode.php?range=2B740-2B81D
康熙部首:https://www.zhangxinxu.com/study/201611/show-character-by-charcode.php?range=2F00-2FD5
部首扩展:https://www.zhangxinxu.com/study/201611/show-character-by-charcode.php?range=2E80-2EF3
兼容汉字:https://www.zhangxinxu.com/study/201611/show-character-by-charcode.php?range=F900-FAD9
兼容扩展:https://www.zhangxinxu.com/study/201611/show-character-by-charcode.php?range=2F800-2FA1D
PUA(GBK)部件:https://www.zhangxinxu.com/study/201611/show-character-by-charcode.php?range=E815-E86F
部件扩展:https://www.zhangxinxu.com/study/201611/show-character-by-charcode.php?range=E400-E5E8
PUA增补:https://www.zhangxinxu.com/study/201611/show-character-by-charcode.php?range=E600-E6CF
汉字笔画:https://www.zhangxinxu.com/study/201611/show-character-by-charcode.php?range=31C0-31E3
汉字结构:https://www.zhangxinxu.com/study/201611/show-character-by-charcode.php?range=2FF0-2FFB
汉语注音:https://www.zhangxinxu.com/study/201611/show-character-by-charcode.php?range=3105-3120
注音扩展:https://www.zhangxinxu.com/study/201611/show-character-by-charcode.php?range=31A0-31BA

〇:https://www.zhangxinxu.com/study/201611/show-character-by-charcode.php?range=3007

MySQL内支持Unicode标准的Charset

MySQL支持多种Unicode Charset:
  • utf8mb4:使用一到四个字节表示每个字符的Unicode字符集的UTF-8编码。

  • utf8mb3:使用一到三个字节表示每个字符的Unicode字符集的UTF-8编码,仅支持表示基本多文种平面(BMP)。在MySQL 8.0中,这个字符集已被deprecated,应该尽快改用utf8mb4。

  • utf8:utf8mb3的别名。在MySQL 8.0中,这个别名已被deprecated,应改用utf8mb4。预计在未来的版本中,utf8将成为utf8mb4的别名。

  • ucs2:使用两个字节表示每个字符的UCS-2编码,仅支持表示基本多文种平面(BMP)。在MySQL 8.0.28中已被deprecated,预计在未来的版本中将被移除。

  • utf16:使用两个或四个字节表示每个字符的UTF-16编码,类似于ucs2,但包含了对补充辅助平面的扩展。

  • utf16le:类似于utf16,但是小端序而不是大端序。

  • utf32:使用四个字节表示每个字符的UTF-32编码。

04


配置Charset和Collation

系统变量

使用show variables命令可以很方便地查看相关变量:
mysql> show variables like "character\_set\_%";
+--------------------------+---------+
| Variable_name            | Value   |
+--------------------------+---------+
| character_set_client     | utf8mb4 |
| character_set_connection | utf8mb4 |
| character_set_database   | utf8mb4 |
| character_set_filesystem | binary  |
| character_set_results    | utf8mb4 |
| character_set_server     | utf8mb4 |
| character_set_system     | utf8mb3 |
+--------------------------+---------+

mysql> show variables like "collation%";
+-------------------------------+--------------------+
| Variable_name                 | Value              |
+-------------------------------+--------------------+
| collation_connection          | utf8mb4_0900_ai_ci |
| collation_database            | utf8mb4_0900_ai_ci |
| collation_server              | utf8mb4_0900_ai_ci |
+-------------------------------+--------------------+

MySQL之所以有这么多和Charset相关的变量,是因为这些变量将在不同维度上起作用。这些系统变量中最重要的是跟连接相关的几个变量:character_set_client、character_set_results、character_set_connection、collation_connection,将在后面“配置连接”小节中详细介绍。剩余几个变量的含义如下:

▶︎ character_set_server和collation_server:如果在CREATE DATABASE语句中没有指定DATABASE的Charset和Collation,则该DATABASE默认Charset和Collation就是character_set_server和collation_server,它们没有其他用途。

▶︎ character_set_database和collation_database:该组变量其实是用于展现当前所在DATABASE(通过use db_name切换)的默认Charset和Collation,由数据库本身进行设置,可以发现切换DATABASE时这两个变量会跟着变化,当没有在use某个DATABASE的时候,这一对变量和character_set_server、collation_server的值相同。因此可以发现该组变量其实是用于展示信息的,虽然还是可以被用户修改,但不建议使用,从8.0.14开始只有有权限的用户才能修改,未来版本会变成read only的变量。

▶︎ character_set_filesystem:此变量用于涉及文件路径的字符串字面量,例如在LOAD DATA和SELECT ... INTO OUTFILE语句以及LOAD_FILE()函数中。这类文件名会在尝试打开文件之前,从character_set_client转换为character_set_filesystem。默认值是binary,这意味着不进行转换。如果文件系统使用某种编码例如UTF-8表示文件名,则应将character_set_filesystem设置为utf8mb4。

▶︎ character_set_system:此变量是Global read only变量,设置为utf8mb3,所有元数据使用此Charset,元数据包括列名、database名、用户名、版本名以及SHOW命令的大部分字符串结果。使用utf8mb3存储元数据并不意味着服务器以character_set_system字符集返回列名,当执行“SELECT column1 FROM t”时,列名“column1”本身是转换为character_set_results系统变量确定的Charset,再从服务器返回到客户端的。
  • 下面是一个尝试在表名中包含非utf8mb3字符的例子,可以看出是不成功的:

-- 1.虽然创建成功,但由于utf8mb3不包含字符'𠀫',因此被转成问号,字符集转换过程后文描述
mysql> create table `𠀫` (a int);
Query OK, 0 rows affected, 1 warning (0.26 sec)
mysql> show warnings;
+---------+------+------------------------------------------------------------------+
| Level   | Code | Message                                                          |
+---------+------+------------------------------------------------------------------+
| Warning | 1300 | Cannot convert string '\xF0\xA0\x80\xAB' from utf8mb4 to utf8mb3 |
+---------+------+------------------------------------------------------------------+
mysql> select namehex(namefrom mysql.tables where schema_id=6;
+--------+--------------+
| name   | hex(name)    |
+--------+--------------+
| ?      | 3F           | # 0x3F是'?'的编码
+--------+--------------+

-- 2.尝试创建另一张表,命名为'𠀪',创建不成功,因为都被转为'?',表名重复
mysql> create table `𠀪` (a int);
ERROR 1050 (42S01): Table '?' already exists


配置连接

在真正介绍character_set_client、character_set_results、character_set_connection、collation_connection这四个变量之前,先介绍一下字符串字面量的概念,如下面最简单的一个例子,'string'这个字符串就是字符串字面量,也就是客户端发来的语句里面的字符串,所有字符串字面量都有自己的Charset和Collation,可以显式指定(具体方法后续小节详细介绍,这里先有个概念)

character_set_connection和collation_connection就是字符串字面量的默认Charset和Collation。

SELECT 'string';

一个例子

下面用一个比较极端的例子来介绍一下这几个变量是如何起作用的。
假设客户端使用utf8mb4字符集,并且在建立连接时将本Session的character_set_client和character_set_results设置成了utf8mb4;由于想让字符串字面量使用latin1字符集的Collation,所以又将character_set_connection和collation_connection分别设置成了latin1和latin1_swedish_ci;而查询语句访问到的列使用的Charset和Collation又分别是ucs2和ucs2_general_ci。那么整个过程将会根据这些变量的设置发生多次字符集转换,如下图所示:

下面文字描述一下上图中的各个步骤:
  1. 查询语句中的字符串字面量'á'最开始在客户端的编码使用的是utf8mb4字符集,编码为0xC3A1;

  2. 服务端接收后会根据character_set_client与character_set_connection的值选择是否进行转换,因为例子中两个变量值不同,因此'á'的编码转为使用latin1字符集,成了0xE1;

  3. 经过解析语句,发现查询的列使用ucs2字符集,所以'á'的编码再一次发生变化,转为使用ucs2字符集,成了0x00E1;

  4. 真正执行完语句会得到结果,结果中也包含了'á'这个字符串,使用的是ucs2字符集;

  5. 在发送结果给客户端之前,会将结果中不是用character_set_results编码的部分进行转换,本例中就将结果中的字符串'á'转换为了utf8mb4字符集,成了0xC3A1;

  6. 客户端接收到结果后,按照自身的utf8mb4字符集进行解码,再进行后续的处理。
下面是复现该例的步骤:
-- 1. 创建表,包含一列,使用ucs2字符集
mysql> create table t1 (a varchar(5character set ucs2);

-- 2. 插入一行数据'á'
mysql> insert into t1 values ('á');

-- 3. 设置character_set_connection
mysql> set character_set_connection=latin1;

-- 4. 查询'á'的编码,从前向后依次是ucs2、latin1、utf8mb4字符集的编码
mysql> select a, hex(a), hex('á'), hex(_utf8mb4'á'from t1 where a='á';
+------+--------+-----------+-------------------+
| a    | hex(a) | hex('á')  | hex(_utf8mb4'á')  |
+------+--------+-----------+-------------------+
| á    | 00E1   | E1        | C3A1              |
+------+--------+-----------+-------------------+

变量具体含义

经过刚才例子相信你已经对几个变量的大致用途有了概念,这几个变量的具体含义如下:
  • character_set_connection和collation_connection:该组变量被用于字符串字面量,当字符串字面量本身没有指定Charset和Collation的时候,就用character_set_connection和collation_connection作为该字面量的Charset和Collation。同时该组变量也被用于数字转字符串的时候,目标字符串的Charset和Collation也是该组变量。

  • character_set_client:来自客户端的语句所使用的Charset,这个变量的Session值是客户端在连接到服务器时设置的(许多客户端支持一个--default-character-set选项来显式指定这个字符集);当客户端请求的值未知或不可用(ucs2、utf16、utf16le、utf32),或者根本没有请求设置Charset,或者服务器配置--skip-character-set-client-handshake忽略客户端请求时,变量的Global值被用来设置Session值。

  • character_set_results:将查询结果返回给客户端时使用的Charset,包括结果数据如列值、结果元数据如列名以及错误信息,这不会影响查询过程,只是最后发送查询结果给客户端时可能会进行一次Charset转换。
大多数情况下,乱码就是因为客户端没有正确设置好character_set_client和character_set_results导致的,因为服务端只能依靠客户端传来的信息决定这两个变量的值,当客户端没有传或错传的时候,就会导致“服务端认为的”与“客户端实际的”驴唇不对马嘴,经过一系列Charset转换后不可预期的乱码就诞生了,这也是本文开篇提到的例1发生乱码的原因

快捷命令

可以看出跟一个连接相关的变量主要是character_set_client、character_set_results、character_set_connection和collation_connection,为了方便MySQL有SET NAMES和SET CHARACTER SET两个命令可以一次性设置4个变量。

SET NAMES

SET NAMES 'charset_name'这一条语句等价于如下三条语句:

  • SET character_set_client = charset_name;
  • SET character_set_results = charset_name;
  • SET character_set_connection = charset_name;

其中设置character_set_connection时会隐式将collation_connection设置为该Charset的默认Collation,如果想更细致地设置Collation,可以使用SET NAMES 'charset_name' COLLATE 'collation_name'指定。

SET CHARACTER SET

SET CHARACTER SET 'charset_name'这一条语句等价于如下三条语句:

  • SET character_set_client = charset_name;
  • SET character_set_results = charset_name;
  • SET collation_connection = @@collation_database;

与SET NAMES的唯一不同在于最后一句设置的是collation_connection,变量@@collation_database代表当前所在数据库(use db_name切换库)的默认Collation,这里设置collation_connection也会隐式地将character_set_connection修改为对应Charset。

配置库、表、列

库表列创建、修改时设置Charset和Collation的语法如下:

-- 创建库
CREATE DATABASE db_name CHARACTER SET charset_name COLLATE collation_name;
-- 修改库
ALTER DATABASE db_name CHARACTER SET charset_name COLLATE collation_name;

-- 创建表
CREATE TABLE tbl_name (column_list) CHARACTER SET charset_name COLLATE collation_name;
-- 修改表
ALTER TABLE tbl_name CHARACTER SET charset_name COLLATE collation_name;

-- 创建列
CREATE TABLE tbl_name(col_name VARCHAR(5CHARACTER SET charset_name COLLATE collation_name);
-- 修改列
ALTER TABLE tbl_name MODIFY col_name VARCHAR(5CHARACTER SET charset_name COLLATE collation_name;

在配置时遵循以下规则:
  • 如果创建时没有显式指定Charset和Collation:

库:使用character_set_server和collation_server作为自己默认的Charset和Collation;

表:使用所在库的默认Charset和Collation作为自己默认的Charset和Collation;

列:使用所在表的默认Charset和Collation作为自己的Charset和Collation。
  • 如果显式指定了Charset和Collation,那么使用显式指定的Charset和Collation;
  • 如果显式指定了Charset没有显式指定Collation,那么使用显式指定的Charset和其默认Collation;
  • 如果显式指定了Collation没有显式指定Charset,那么使用显式指定的Collation和其所属的Charset;
  • CHAR、VARCHAR、TEXT、ENUM、SET列都支持指定Charset和Collation;
  • 修改列的Charset时,MySQL会尝试映射数据值,但如果修改前后Charset不兼容,可能会发生数据丢失。

字符串字面量

所有字符串字面量都有自己的Charset和Collation,可以如下显式指定,指定时Charset前需要加一个下划线,同时该语法也可以和b、X前缀搭配使用:
_charset_name'string' COLLATE collation_name

下面是一些示例:

SELECT 'abc';
SELECT _latin1'abc';
SELECT _binary'abc';
SELECT _utf8mb4'abc' COLLATE utf8mb4_danish_ci;

SELECT _latin1 X'4D7953514C';
SELECT _utf8mb4 0x4D7953514C COLLATE utf8mb4_danish_ci;

SELECT _latin1 b'1000001';
SELECT _utf8mb4 0b1000001 COLLATE utf8mb4_danish_ci;

在配置时遵循以下规则:

  • 如果没有显式指定Charset和Collation,那么将使用character_set_connection和collation_connection作为其Charset和Collation,对于b、X前缀的字符串Charset和Collation将设置为binary;

  • 如果显式指定了Charset和Collation,那么使用显式指定的Charset和Collation;
  • 如果显式指定了Charset没有显式指定Collation,那么使用显式指定的Charset和其默认Collation;
  • 如果显式指定了Collation没有显式指定Charset,那么显式指定的Collation需要是character_set_connection的某个Collation;
  • _charset_name被称为Introducer,没有Introducer的情况下解析器会把字符串字面量的字符集都转为character_set_connection,Introducer的作用是告诉解析器,紧随其后的字符串使用charset_name作为Charset,但它不会像CONVERT()函数那样将字符串转换为目标Charset,也不会改变字符串的二进制编码,只会在一些情况下进行padding,这也可能导致乱码:
-- 1.创建表t1,有一列使用latin1字符集
mysql> create table t1(a char(5character set latin1);

-- 2.直接插入'张'不成功是因为latin1没有该字符,但是加上_latin1前缀就插入成功了,
--   这是由于Introducer让MySQL解析器误以为这个utf8mb4字符串是latin1字符集的,
--   后续插入相应列的时候就不加检查、转换了
mysql> insert into t1(a) values('张');
ERROR 1366 (HY000): Incorrect string value: '\xE5\xBC\xA0' for column 'a' at row 1
mysql> insert into t1(a) values(_latin1'张');
Query OK, 1 row affected (0.00 sec)

-- 3.select出来存入的是utf8mb4上'张'的编码0xE5BCA0,产生乱码
mysql> select a, hex(a) from t1;
+--------+--------+
| a      | hex(a) |
+--------+--------+
| å¼     | E5BCA0 |
+--------+--------+

  • 大部分情况下_binary'11110000'和b'11110000'和X'F0'是等价的,都被认为是binary字符串,也可以使用,但在使用位运算的时候会有一些不同,如下面例子所示,b、X前缀的二进制字符串的位运算结果是数字,所以没有前导0;而binary前缀的二进制字符串的位运算结果也同样是字符串,所以有前导0:
mysql> SET @v1 = X'000D' | X'0BC0';
mysql> SET @v2 = _binary X'000D' | X'0BC0';
mysql> SELECT HEX(@v1), HEX(@v2);
+----------+----------+
| HEX(@v1) | HEX(@v2) |
+----------+----------+
| BCD      | 0BCD     |
+----------+----------+


错误信息的Charset

MySQL构建错误消息的方式如下:
▶︎ 错误消息模板使用utf8mb3字符集;

▶︎ 当模板中的参数被替换为一些值时:

  • 表名或列名等元信息,在内部同样使用utf8mb3,因此它们按原样被复制

  • 非二进制Charset的字符串值从其Charset转换为utf8mb3

  • 二进制字符串值在0x20到0x7E范围内的字节按原样复制,范围外的字节使用\x十六进制编码。例如,如果在尝试向VARBINARY唯一列中插入0x41CF9F时发生重复键错误,生成的错误信息会变成“Duplicate entry 'A\xCF\x9F' for key 1”,后两个字节0xCF9F被十六进制字符串形式输出,而第一个字节0x41则被转为字符A
一旦错误消息构建完成,可以由MySQL写入Error Log或发送给客户端:
▶︎ 将错误消息写入Error Log时,会以构建时的utf8mb3字符集写入,无需转换;
▶︎ 将错误消息发送给客户端程序时,会将错误消息从utf8mb3转换为由character_set_results系统变量指定的Charset,如果character_set_results的值为NULL或binary,则不进行转换;如果变量值是utf8mb3或utf8mb4,也不会发生转换;

▶︎ 如果有字符无法在character_set_results中表示,转换过程中可能会发生一些编码,编码使用Unicode码点:

  • 基本多语言平面(BMP)范围内的字符(0x0000到0xFFFF)使用\nnnn表示法编写;

  • BMP范围之外的字符(0x10000到0x10FFFF)使用\+nnnnnn表示法编写。

End



以上为本文上篇内容,重点介绍了相关概念与MySQL的配置项。下篇内容将于近期推出,重点介绍字符集转换与Unicode、Binary字符集的排序算法,敬请期待。


如对上述内容感兴趣或有任何问题,欢迎在评论区与我们留言讨论。

点击了解 云数据库RDS

喜欢就请 点赞/分享/在看 哦

文章转载自阿里云瑶池数据库,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论