深入理解MySQL字符集及校对规则(二)

2021-03-03 09:59:03 浏览数 (1)

上一篇呢,从理论上介绍了MYSQL字符集和校对规则是什么以及如何正确的使用字符集;关于第一部分的内容可参考:深入理解MySQL字符集及校对规则(一)

下面这部分呢,主要介绍MySQL字符编码转换原理以及字符集转化流程案例测试,通过实际的案例来分析字符集转化的原理;

一、MySQL字符编码转换原理

有这样一个疑问:若character_set_client为UTF8,而character_set_database为GBK,则会出现需要进行编码转换的情况,字符集转换的原理是什么?

解答如下:假设gbk字符集的字符串“你好”,需要转为utf8字符集存储,实际就是对于“你好”字符串中的每个汉字去utf8编码表里面查询对应的二进制,然后存储。

下面我们通过一幅图来看一下字符集转换的过程:

①MySQL Server收到请求时将请求数据从character_set_client转换为character_set_connection;

②进行内部操作前将请求数据从character_set_connection转换为内部操作字符集

确定步骤: –使用每个数据字段的CHARACTER SET设定值; –若上述值不存在,则使用对应数据表的DEFAULT CHARACTER SET设定值; –若上述值不存在,则使用对应数据库的DEFAULT CHARACTER SET设定值; –若上述值不存在,则使用character_set_server设定值;

③将操作结果从内部操作字符集转换为character_set_results。

二、字符集常见处理操作

1.查看字符集编码设置

mysql> show variables like ‘%character%’;

2、设置字符集编码

mysql> set names ‘utf8’;

相当于同时:

set character_set_client = utf8;

set character_set_results = utf8;

set character_set_connection = utf8;

3、修改数据库字符集

mysql> alter database database_name character set xxx;

只修改库的字符集,影响后续创建的表的默认定义;对于已创建的表的字符集不受影响。(一般在数据库实现字符集即可,表和列都默认采用数据库的字符集)

4、修改表的字符集

mysql> alter table table_name character set xxx;

只修改表的字符集,影响后续该表新增列的默认定义,已有列的字符集不受影响。

mysql> alter table table_name convert to character set xxx;

同时修改表字符集和已有列字符集,并将已有数据进行字符集编码转换。

5、修改列字符集

格式:

ALTER TABLE table_name MODIFY column_name {CHAR | VARCHAR | TEXT} (column_length) [CHARACTER SET charset_name] [COLLATE collation_name]

mysql> alter table table_name modify col_name varchar(col_length) character set xxx;

6、字符集的正确实践:

MySQL软件本身是没有字符集的,主要是因为工具所在的OS的字符集(Windows:gbk、Linux:utf8),所以字符集的正确实践非常重要:

(1)对于insert来说,character_set_client、character_set_connection相同,而且正确反映客户端使用的字符集

(2)对于select来说,character_set_results正确反映客户端字符集

(3)字符集转换最多发生一次,这就要求character_set_client、character_set_connection相同

(4)所有的字符集转换都发生在数据库端

综述: 1、建立数据库的时候注意字符集(gbk、utf8); 2、连接数据库以后,无论是执行dml还是select,只要涉及到varchar、char列,就需要设置正确的字符集参数。

三、MYSQL字符编码转化流程测试

使用实验来进一步说明MySQL服务器字符集以及连接字符集之间的作用以及关系。这里我们就简单演示这几种情况,这里我使用SecureCRT(utf8字符集)连接Linux系统

实例案例一:

1、查看当前数据库实例的字符集

代码语言:javascript复制
root@localhost [wjqtest]>show variables like 'character%';
 -------------------------- ---------------------------------- 
| Variable_name            | Value                            |
 -------------------------- ---------------------------------- 
| character_set_client     | utf8                             |
| character_set_connection | utf8                             |
| character_set_database   | utf8mb4                          |
| character_set_filesystem | binary                           |
| character_set_results    | utf8                             |
| character_set_server     | utf8mb4                          |
| character_set_system     | utf8                             |
| character_sets_dir       | /usr/local/mysql/share/charsets/ |
 -------------------------- ---------------------------------- 
8 rows in set (0.00 sec)
 
root@localhost [wjqtest]>show variables like 'collation%';
 ---------------------- -------------------- 
| Variable_name        | Value              |
 ---------------------- -------------------- 
| collation_connection | utf8_general_ci    |
| collation_database   | utf8mb4_general_ci |
| collation_server     | utf8mb4_general_ci |
 ---------------------- -------------------- 
3 rows in set (0.00 sec)

2、创建测试表,字符集指定为utf8

代码语言:javascript复制
root@localhost [wjqtest]>create table charset_test(id int,col1 varchar(30)) charset=utf8;            
Query OK, 0 rows affected (0.01 sec)

3、改变连接字符集为gbk

代码语言:javascript复制
root@localhost [wjqtest]>set names gbk;
Query OK, 0 rows affected (0.00 sec)
 
root@localhost [wjqtest]>show variables like 'character%';
 -------------------------- ---------------------------------- 
| Variable_name            | Value                            |
 -------------------------- ---------------------------------- 
| character_set_client     | gbk                              |
| character_set_connection | gbk                              |
| character_set_database   | utf8mb4                          |
| character_set_filesystem | binary                           |
| character_set_results    | gbk                              |
| character_set_server     | utf8mb4                          |
| character_set_system     | utf8                             |
| character_sets_dir       | /usr/local/mysql/share/charsets/ |
 -------------------------- ---------------------------------- 
8 rows in set (0.00 sec)
 
root@localhost [wjqtest]>show variables like 'collation%';
 ---------------------- -------------------- 
| Variable_name        | Value              |
 ---------------------- -------------------- 
| collation_connection | gbk_chinese_ci     |
| collation_database   | utf8mb4_general_ci |
| collation_server     | utf8mb4_general_ci |
 ---------------------- -------------------- 
3 rows in set (0.00 sec)

4、由于连接字符集修改成了gbk,这里插入数据会出现报错

代码语言:javascript复制
root@localhost [wjqtest]>insert into charset_test values(1,'中国'); 
ERROR 1366 (HY000): Incorrect string value: 'xADxE5x9BxBD' for column 'col1' at row 1

注意:这里在测试的过程中,出现一些其他的情况,插入其他的汉字是没有问题(比如插入北京),那为什么会出现这种情况呢?是什么原因呢?

代码语言:javascript复制
root@localhost [wjqtest]>insert into charset_test(col1) values('北京');
Query OK, 1 row affected (0.04 sec)

那么我们来看一下为什么北京可以插入,而中国不可以插入呢?这就涉及到了一个字符转码的问题;我们来看一下转码的问题


得到中文‘北京’的gbk下的16进制编码

代码语言:javascript复制
root@localhost [wjqtest]>SELECT hex(CONVERT( '北京' USING gbk ));    
 ------------------------------------ 
| hex(CONVERT( '北京' USING gbk ))   |
 ------------------------------------ 
| E58C97E4BAAC                       |
 ------------------------------------ 
1 row in set (0.00 sec)

反推gbk的16进制编码,取回中文

代码语言:javascript复制
root@localhost [wjqtest]>SELECT CONVERT( unhex('E58C97E4BAAC') USING gbk);            
 ------------------------------------------- 
| CONVERT( unhex('E58C97E4BAAC') USING gbk) |
 ------------------------------------------- 
| 北京                                      |
 ------------------------------------------- 
1 row in set (0.00 sec)

从gbk的16进制编码直接转成utf8的16进制编码

代码语言:javascript复制
root@localhost [wjqtest]>SELECT HEX(CONVERT(CONVERT( unhex('E58C97E4BAAC') USING gbk) USING utf8));            
 -------------------------------------------------------------------- 
| HEX(CONVERT(CONVERT( unhex('E58C97E4BAAC') USING gbk) USING utf8)) |
 -------------------------------------------------------------------- 
| E98D96E6A4BEE590AB                                                 |
 -------------------------------------------------------------------- 
1 row in set (0.00 sec)

通过utf8的16进制编码取回中文

代码语言:javascript复制
root@localhost [wjqtest]>SELECT CONVERT( unhex('E98D96E6A4BEE590AB') USING utf8);              
 -------------------------------------------------- 
| CONVERT( unhex('E98D96E6A4BEE590AB') USING utf8) |
 -------------------------------------------------- 
| 北京                                             |
 -------------------------------------------------- 
1 row in set (0.00 sec)

下面按照同样的方式编码取出‘中国’的GBK和utf8编码

代码语言:javascript复制
root@localhost [wjqtest]>SELECT hex(CONVERT( '中国' USING gbk ));
 ----------------------------------- 
| hex(CONVERT( 'SING gbk ))     |
 ----------------------------------- 
| E4B8ADE59BBD                      |
 ----------------------------------- 
1 row in set (0.00 sec)
 
root@localhost [wjqtest]>SELECT CONVERT( unhex('E4B8ADE59BBD') USING gbk);            
 ------------------------------------------- 
| CONVERT( unhex('E4B8ADE59BBD') USING gbk) |
 ------------------------------------------- 
| 中国                                      |
 ------------------------------------------- 
1 row in set (0.00 sec)
 
root@localhost [wjqtest]>
root@localhost [wjqtest]>SELECT HEX(CONVERT(CONVERT( unhex('E4B8ADE59BBD') USING gbk) USING utf8));            
 -------------------------------------------------------------------- 
| HEX(CONVERT(CONVERT( unhex('E4B8ADE59BBD') USING gbk) USING utf8)) |
 -------------------------------------------------------------------- 
| E6B6933FE6B597                                                     |
 -------------------------------------------------------------------- 
1 row in set (0.00 sec)
 
root@localhost [wjqtest]>SELECT CONVERT( unhex('E6B6933FE6B597') USING utf8);                  
 ---------------------------------------------- 
| CONVERT( unhex('E6B6933FE6B597') USING utf8) |
 ---------------------------------------------- 
|                                          |
 ---------------------------------------------- 
1 row in set (0.00 sec)

通过上面的测试发现,北京经过转码回来之后依旧是北京,而中国在转码回来就不在是中国字符了;


好,那就接着上面的步骤4继续进行实验测试;

5、这里改变客户端的字符集为utf8,就可插入成功了

代码语言:javascript复制
root@localhost [wjqtest]>set character_set_client=utf8;
Query OK, 0 rows affected (0.00 sec)
 
root@localhost [wjqtest]>show variables like 'character%';
 -------------------------- ---------------------------------- 
| Variable_name            | Value                            |
 -------------------------- ---------------------------------- 
| character_set_client     | utf8                             |
| character_set_connection | gbk                              |
| character_set_database   | utf8mb4                          |
| character_set_filesystem | binary                           |
| character_set_results    | gbk                              |
| character_set_server     | utf8mb4                          |
| character_set_system     | utf8                             |
| character_sets_dir       | /usr/local/mysql/share/charsets/ |
 -------------------------- ---------------------------------- 
8 rows in set (0.01 sec)
 
root@localhost [wjqtest]>insert into charset_test values(1,'中国');
Query OK, 1 row affected (0.01 sec)

6、查看数据,虽然插入成功了,但是发现返回的结果是乱码

代码语言:javascript复制
root@localhost [wjqtest]>select * from charset_test;
 ---- ------ 
| id | col1 |
 ---- ------ 
|  1 |     |
 ---- ------ 
1 row in set (0.00 sec)

7、改变character_set_results(服务器反馈SQL语句结果使用的字符集)字符集为utf8后,再次查询数据显示就正常了

代码语言:javascript复制
root@localhost [wjqtest]>set character_set_results=utf8;
Query OK, 0 rows affected (0.00 sec)
 
root@localhost [wjqtest]>select * from charset_test;
 ---- -------- 
| id | col1   |
 ---- -------- 
|  1 | 中国   |
 ---- -------- 
1 row in set (0.00 sec)

8、下面接着进行测试,将SecureCRT工具调整为GB2312字符集,然后继续插入数据

代码语言:javascript复制
root@localhost [wjqtest]>insert into charset_test(col1) values('涓);
Query OK, 1 row affected (0.02 sec)

9、查看数据发现还是显示乱码

代码语言:javascript复制
root@localhost [wjqtest]>select * from charset_test;
 ------ -------- 
| id   | col1   |
 ------ -------- 
| NULL | 涓  |
| NULL | 涓  |
 ------ -------- 
2 rows in set (0.00 sec)

10、下面将SecureCRT工具改回utf8字符集,查询数据显示就正常了

代码语言:javascript复制
root@localhost [wjqtest]>select * from charset_test;
 ------ -------- 
| id   | col1   |
 ------ -------- 
| NULL | 中国   |
| NULL | 中国   |
 ------ -------- 
2 rows in set (0.00 sec)

11、把客户端字符集修改为gbk,插入数据还是会报错

代码语言:javascript复制
(注意:gbk往utf8中插入报错,utf8往gbk可以插入成功)
root@localhost [wjqtest]>set character_set_client=gbk;
Query OK, 0 rows affected (0.00 sec)
 
root@localhost [wjqtest]>show variables like 'char%';
 -------------------------- ---------------------------------- 
| Variable_name            | Value                            |
 -------------------------- ---------------------------------- 
| character_set_client     | gbk                              |
| character_set_connection | gbk                              |
| character_set_database   | utf8mb4                          |
| character_set_filesystem | binary                           |
| character_set_results    | utf8                             |
| character_set_server     | utf8mb4                          |
| character_set_system     | utf8                             |
| character_sets_dir       | /usr/local/mysql/share/charsets/ |
 -------------------------- ---------------------------------- 
8 rows in set (0.00 sec)
 
root@localhost [wjqtest]>insert into charset_test(col1) values('中国');
ERROR 1366 (HY000): Incorrect string value: 'xADxE5x9BxBD' for column 'col1' at row 1

12、把连接变量的字符串修改为uft8,可以成功插入数据,但是会有警告

代码语言:javascript复制
root@localhost [wjqtest]>set character_set_connection=utf8;
Query OK, 0 rows affected (0.00 sec)
 
root@localhost [wjqtest]>insert into charset_test(col1) values('中国');
Query OK, 1 row affected, 1 warning (0.00 sec)
 
root@localhost [wjqtest]>show warnings;
 --------- ------ ---------------------------------------------------------- 
| Level   | Code | Message                                                  |
 --------- ------ ---------------------------------------------------------- 
| Warning | 1300 | Invalid gbk character string: 'xE4xB8xADxE5x9BxBD' |
 --------- ------ ---------------------------------------------------------- 
1 row in set (0.00 sec)

13、再次查看数据,发现刚刚虽然插入了,但是发现是乱码

代码语言:javascript复制
root@localhost [wjqtest]>select hex(col1),col1 from charset_test;
 ---------------- --------- 
| hex(col1)      | col1    |
 ---------------- --------- 
| E4B8ADE59BBD   | 中国    |
| E4B8ADE59BBD   | 中国    |
| E6B6933FE6B597 | 涓?浗   |
 ---------------- --------- 
3 rows in set (0.00 sec)

实验案例二:

注意:当前我的机器的终端编码为UTF8,数据库的编码设定以下

代码语言:javascript复制
root@localhost [wjqtest]>show variables like '%char%';
 -------------------------- ---------------------------------- 
| Variable_name            | Value                            |
 -------------------------- ---------------------------------- 
| character_set_client     | utf8                             |
| character_set_connection | utf8                             |
| character_set_database   | utf8mb4                          |
| character_set_filesystem | binary                           |
| character_set_results    | utf8                             |
| character_set_server     | utf8mb4                          |
| character_set_system     | utf8                             |
| character_sets_dir       | /usr/local/mysql/share/charsets/ |
 -------------------------- ---------------------------------- 
8 rows in set (0.00 sec)

首先,创建一张测试表charset_test1

代码语言:javascript复制
root@localhost [wjqtest]>CREATE TABLE `charset_test1` (
    ->   `gbk` varchar(2) CHARACTER SET gbk DEFAULT NULL,
    ->   `utf8` varchar(2) CHARACTER SET utf8 DEFAULT NULL,
    ->   `latin_utf8` varchar(6) DEFAULT NULL
    -> ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
Query OK, 0 rows affected (0.01 sec)

注意:测试表的编码是latin1,而表中三个字段的编码各不同样,分别为gbk编码,utf8编码以及latin1编码.之所以这样创建,正是为了验证mysql字符集编码的转换过程。

好了,重点来了,下面插入数据:

代码语言:javascript复制
root@localhost [wjqtest]> insert into charset_test1 values("中文", "中文", "中文");   
ERROR 1366 (HY000): Incorrect string value: 'xE4xB8xADxE6x96x87' for column 'latin_utf8' at row 1
 
root@localhost [wjqtest]> insert into charset_test1 values("北京", "北京", "北京");
ERROR 1366 (HY000): Incorrect string value: 'xE5x8Cx97xE4xBAxAC' for column 'latin_utf8' at row 1

发现在插入的时候出现上面的报错信息,为了方便后面的测试,修改session级别的sql_mode为非严格模式,这样就可以插入成功了

代码语言:javascript复制
root@localhost [wjqtest]>set session sql_mode='NO_ENGINE_SUBSTITUTION';
Query OK, 0 rows affected (0.01 sec)
 
root@localhost [wjqtest]>show variables like 'sql_mode';
 --------------- ------------------------ 
| Variable_name | Value                  |
 --------------- ------------------------ 
| sql_mode      | NO_ENGINE_SUBSTITUTION |
 --------------- ------------------------ 
1 row in set (0.00 sec)
 
root@localhost [wjqtest]> insert into charset_test1 values("中文", "中文", "中文");
Query OK, 1 row affected, 1 warning (0.00 sec)

虽然是插入成功了,但是会有一个告警信息

代码语言:javascript复制
root@localhost [wjqtest]>show warnings;
 --------- ------ ------------------------------------------------------------------------------------- 
| Level   | Code | Message                                                                             |
 --------- ------ ------------------------------------------------------------------------------------- 
| Warning | 1366 | Incorrect string value: 'xE4xB8xADxE6x96x87' for column 'latin_utf8' at row 1 |
 --------- ------ ------------------------------------------------------------------------------------- 
1 row in set (0.00 sec)

查看测试表的内容,发现latin_utf8字段出现了乱码

代码语言:javascript复制
root@localhost [wjqtest]>select * from charset_test1;
 -------- -------- ------------ 
| gbk    | utf8   | latin_utf8 |
 -------- -------- ------------ 
| 中文   | 中文   | ??         |
 -------- -------- ------------ 
1 row in set (0.00 sec)

下面就查看一下测试表中实际存储的内容:

代码语言:javascript复制
root@localhost [wjqtest]>select hex(gbk),hex(utf8),hex(latin_utf8) from charset_test1;
 ---------- -------------- ----------------- 
| hex(gbk) | hex(utf8)    | hex(latin_utf8) |
 ---------- -------------- ----------------- 
| D6D0CEC4 | E4B8ADE69687 | 3F3F            |
 ---------- -------------- ----------------- 
1 row in set (0.00 sec)

能够发现直接select查看的时候latin_utf8字段乱码了,而通过hex函数查看发现原来latin_utf8字段存储的内容有问题。出现这个问题的解决办法就是编码转换过程出了错,依照之前的原理来分析下整个编码转换过程:

(1)首先我们mysql客户端发送插入语句insert into charset_test1 values(“中文”, “中文”, “中文”);,注意到”中文”的编码是跟我们的环境相关的,我这里是UTF8,因此”中文”字节表示为xE4xB8xADxE6x96x87。 (2)server端接收到该语句会当作utf8编码,由于character_set_client=utf8,接下来是会进行第一步转换,即将语句从character_set_client转成character_set_connection的编码,由于我们这里这2个编码同样,实际就不会转换(此外,假设插入的数据前面有latin1或者utf8等introducer标记,也不会转换,由于introducer标记已经指明了字面值字符的编码)。 (3)接下来,数据要存储到数据库了,这个时候实际要插入的三个字段的编码都是原始编码xE4xB8xADxE6x96x87,这个时候发生第二次编码转换,即由character_set_connection编码转换为数据表字段指定的编码.那么接下来,我们能够看到,由本身的UTF8编码与字段utf8同样,不须要进行转换。接下来看gbk字段,它的编码是gbk,这时会将原始编码s=”xE4xB8xADxE6x96x87″依照utf8编码转换为GBK编码,即运行s.decode(‘utf8’).encode(‘gbk’),所以存储的是D6D0CEC4,也没有问题. 最后,看latin_utf8字段,相同须要转换编码,因为latin1表示不了utf8编码的范围,所以s.decode(‘utf8’).encode(‘latin1′)这个转换过程会出错,导致的结果就是latin_utf8字段存储的是??,即3F3F。 (4)最后就是select语句返回的结果分析,这是第三个须要转换编码的地方,即将字段从字段编码转换为character_set_results指定的编码.这也是我们上面为什么gbk字段和utf8字段都能正常显示中文的原因,因为在返回结果的时候,gbk字段会经过’xD6xD0xCExC4’.decode(‘gbk’).encode(‘utf8’)返回,这样我们在utf8编码的mysqlclient可以正常显示gbk字段.同理,因为utf8字段本身与character_set_results,所以不会发生编码转换,原样返回xE4xB8xADxE6x96x87,因此也是能正常显示的.而latin_utf8字段本身存储的就是3F3F,再经过编码转换,尽管utf8编码可以兼容latin1,可是本身的编码是3F3F,所以终于结果就是”??”。

依据上面的分析,为了表charset_test1中的latin_utf8字段可以正常的插入内容,在不设置character_set_client和character_set_connection的情况下,那么有个好的方法就是增加introducer,关于introducer可以參见mysql官方文档.那么我们的插入语句改为

代码语言:javascript复制
root@localhost [wjqtest]> insert into charset_test1 values("中文", "中文", _latin1"中文"); 
Query OK, 1 row affected (0.01 sec)

由于指定了latin_utf8字段的introducer为_latin1,这样在第一次由character_set_client转换为character_set_connection的时候会忽略latin_utf8的转换,所以还是保持原来的utf8字符,接下来将其存入到latin1字段中,亦不会有问题,由于编码同样,不须要转换,所以latin_utf8字段实际存储的还是xE4xB8xADxE6x96x87.这点能够通过以下的命令来验证:

代码语言:javascript复制
root@localhost [wjqtest]>select hex(gbk),hex(utf8),hex(latin_utf8) from charset_test1;    
 ---------- -------------- ----------------- 
| hex(gbk) | hex(utf8)    | hex(latin_utf8) |
 ---------- -------------- ----------------- 
| D6D0CEC4 | E4B8ADE69687 | 3F3F            |
| D6D0CEC4 | E4B8ADE69687 | E4B8ADE69687    |
 ---------- -------------- ----------------- 
2 rows in set (0.00 sec)

那么我们假设直接select查询,还会是乱码吗?答案是会的;

由于如前所说,查询的时候会将字段编码转换为character_set_results编码的,显然gbk和utf8字段都没有问题,可是对于latin_utf8字段,其值会通过s.decode(‘latin1’).encode(‘gbk’),从而导致在查询的时候会乱码.

代码语言:javascript复制
root@localhost [wjqtest]>select * from charset_test1;
 -------- -------- ---------------- 
| gbk    | utf8   | latin_utf8     |
 -------- -------- ---------------- 
| 中文   | 中文   | ??             |
| 中文   | 中文   | 中文         |
 -------- -------- ---------------- 
2 rows in set (0.00 sec)

那么解决办法,就是select语句中的字段前面加上binary标识,表示该字段查询结果不须要经过character_set_results的转换.例如以下:

代码语言:javascript复制
root@localhost [wjqtest]>select gbk, utf8, binary latin_utf8 from charset_test1;        
 -------- -------- ------------------- 
| gbk    | utf8   | binary latin_utf8 |
 -------- -------- ------------------- 
| 中文   | 中文   | ??                 |
| 中文   | 中文   | 中文               |
 -------- -------- ------------------- 
2 rows in set (0.00 sec)

关于字符集Introducers详细可参考官方文档说明:

https://dev.mysql.com/doc/refman/5.7/en/charset-introducer.html

0 人点赞