我们知道在计算机中只能以二进制的方式对数据进行存储,那么他们之间是怎样对应并进行转换的?我们需要了解两个概念:
我们抽象出一个字符集的概念来描述某个字符范围的编码规则。比方说我们来自定义一个名称为 margu的字符集,它包含的字符范围和编码规则如下:包含字符 ‘a’ 、 ‘b’ 、 ‘A’ 、 ‘B’ 。
编码规则如下:
采用1个字节编码一个字符的形式,字符和字节的映射关系如下:
‘a’ -> 00000001 (十六进制:0x01)
‘b’ -> 00000010 (十六进制:0x02)
‘A’ -> 00000011 (十六进制:0x03)
‘B’ -> 00000100 (十六进制:0x04)
如果我们规定使用margu这个字符集,我们就可以用二进制形式表示一些字符串了,下边是一些字符串用 margu 字符集编码后的二进制表示:
‘Ba’ -> 0000010000000001 (十六进制:0x0401)
‘bAB’ -> 000000100000001100000100 (十六进制:0x020304)
‘CD’ -> 无法表示,字符集‘margu‘不包含字符’C’和’D’
所以,如果你想表示某个字符,一定要所选用的字符集支持。
在我们确定了 margu字符集表示字符的范围以及编码规则后,怎么比较两个字符的大小呢?比较简单的就是直接比较这两个字符对应的二进制编码的大小,比方说字符 ‘a’ 的编码为 0x01 ,字符 ‘b’ 的编码为 0x02 ,所以 ‘a’ 小于 ‘b’ ,这种简单的比较规则也可以被称为二进制比较规则,英文名为 binary collation 。
二进制比较规则是简单,但有时候并不符合现实需求,比如在很多场合对于英文字符我们都是不区分大小写的,也就是说 ‘a’ 和 ‘A’ 是相等的,在这种场合下二进制比较规则就不再适用了,这时候我们可以这样指定比较规则:
1 . 将两个大小写不同的字符全都转为大写或者小写。
2 . 再比较这两个字符对应的二进制数据。
这是一种稍微复杂一点的比较规则,但是实际生活中的字符不止英文字符一种,比如我们的汉字就有上万个,对于某一种字符集来说,比较两个字符大小可以有多种比较规则,后续会介绍目前在MySQL中一些常用的字符集以及它们的一些比较规则。
目前字符集的种类有很多,它们表示的字符范围和用到的编码规则可能都不一样。下面是一些常用的字符集:
共收录128个字符,包括空格、标点符号、数字、大小写字母和一些不可见字符。由于总共128个字符,所以可以使用1个字节(8个bit位)来进行编码,我们看一些字符的编码方式:
‘A’ -> 01000001(十六进制:0x41,十进制:65)
‘H’ -> 01001000(十六进制:0x48,十进制:72)
共收录256个字符,是在 ASCII 字符集的基础上又扩充了128个西欧常用字符(包括德法两国的字母),也可以使用1个字节来进行编码。这个字符集也有一个别名 latin1 。
收录了汉字以及拉丁字母、希腊字母、日文平假名及片假名字母、俄语西里尔字母。其中收录汉字6763个,其他文字符号682个。同时这种字符集又兼容 ASCII 字符集,所以在编码方式上显得有些奇怪:
这种表示一个字符需要的字节数可能不同的编码方式称为变长编码方式 。比方说字符串 ‘爱u’ ,其中 ‘爱’ 需要用2个字节进行编码,编码后的十六进制表示为 0xCED2 , ‘u’ 需要用1个字节进行编码,编码后的十六进制表示为 0x75 ,所以拼合起来就是 0xCED275 。
注意:如何区分某个字节代表一个单独的字符还是代表某个字符的一部分呢?前面说过ASCII
字符集只收录128个字符,使用0~127就可以表示全部字符,所以如果某个字节是在0~127之内的,就意味着一个字节代表一个单独的字符,否则就是两个字节代表一个单独的字符。
GBK 字符集只是在收录字符范围上对 GB2312 字符集作了扩充,编码方式上兼容 GB2312 。
utf8是目前包含最全字符的字符集,而且还在不断补充。这种字符集兼容 ASCII 字符集,采用变长编码方式,编码一个字符需要使用1~4个字节,比如:
‘A’ -> 01000001(十六进制:0x41)
‘啊’ -> 111001011001010110001010(十六进制:0xE5958A)
注意:准确的说,utf8只是Unicode字符集的一种编码方案,Unicode字符集可以采用utf8、utf16、utf32这几种编码方案,utf8使用1~4个字节编码一个字符,utf16使用2个或4个字节编码一个字符,utf32使用4个字节编码一个字符。
MySQL中并不区分字符集和编码方案的概念,所以后边唠叨的时候把utf8、utf16、utf32都当作
一种字符集对待。
对于同一个字符,不同字符集也可能有不同的编码方式。比如对于汉字 ‘我’ 来说, ASCII 字符集中没有收录这个字符, 而在utf8 和 gb2312 字符集对汉字我的编码方式如下:
utf8编码:111001101000100010010001 (3个字节,十六进制表示是:0xE68891)
gb2312编码:1100111011010010 (2个字节,十六进制表示是:0xCED2)
上边说过utf8 字符集表示一个字符需要使用1~4个字节,但是我们常用的一些字符使用1~3个字节就可以表示了。而在Mysql中字符集表示一个字符所用最大字节长度在某些方面会影响系统的存储和性能,所以Mysql衍生出utf8的两个子字符集概念:
utf8mb3 :精简版的utf8 字符集,只使用1~3个字节表示字符。
utf8mb4 :标准版的utf8 字符集,使用1~4个字节表示字符。
重点注意:在 MySQL 中 utf8 是 utf8mb3 的别名,所以之后在 MySQL 中提到 utf8 就意味着使用1~3个字节来表示一个字符,如果要想使用4字节编码一个字符的情况,比如存储一些emoji表情,那么需要完整指定使用utf8mb4 字符集。
MySQL 支持好多好多种字符集,查看当前 MySQL 中支持的字符集可以用下边这个语句:
SHOW (CHARACTER SET|CHARSET) [LIKE 匹配的模式];
其中 CHARACTER SET 和 CHARSET 是同义词,用任意一个都可以。
mysql> show character set;
+----------+---------------------------------+---------------------+--------+
| Charset | Description | Default collation | Maxlen |
+----------+---------------------------------+---------------------+--------+
| big5 | Big5 Traditional Chinese | big5_chinese_ci | 2 |
| dec8 | DEC West European | dec8_swedish_ci | 1 |
| cp850 | DOS West European | cp850_general_ci | 1 |
| hp8 | HP West European | hp8_english_ci | 1 |
| koi8r | KOI8-R Relcom Russian | koi8r_general_ci | 1 |
| latin1 | cp1252 West European | latin1_swedish_ci | 1 |
......
41 rows in set (0.00 sec)
从返回的结果中可以看到,目前使用的mysql(5.7版本)一共支持41种字符集,其中的 Default collation 列表示这种字符集中一种默认的比较规则,ci结尾的表示都是忽略大小写 。最后一列的Maxlen ,它代表该种字符集表示一个字符最多需要几个字节。如下:
字符集名称 | 描述 | 默认比较规则 | 最长字节数 |
---|---|---|---|
ascii | US ASCII | ascii_general_ci | 1 |
latin1 | cp1252 West European | latin1_swedish_ci | 1 |
gb2312 | GB2312 Simplified Chinese | gb2312_chinese_ci | 2 |
gbk | GBK Simplified Chinese | gbk_chinese_ci | 2 |
utf8 | UTF-8 Unicode | utf8_general_ci | 3 |
utf8mb4 | UTF-8 Unicode | utf8mb4_general_ci | 4 |
查看 MySQL 中支持的比较规则的命令如下:
SHOW COLLATION [LIKE 匹配的模式];
一种字符集可能对应有多种比较规则, MySQL 支持的字符集就已经非常多了,所以支持的比较规则更多,我们先只查看一下 utf8 字符集下的比较规则:
mysql> show collation like 'utf8\_%';
+--------------------------+---------+-----+---------+----------+---------+
| Collation | Charset | Id | Default | Compiled | Sortlen |
+--------------------------+---------+-----+---------+----------+---------+
| utf8_general_ci | utf8 | 33 | Yes | Yes | 1 |
| utf8_bin | utf8 | 83 | | Yes | 1 |
| utf8_unicode_ci | utf8 | 192 | | Yes | 8 |
......
| utf8_general_mysql500_ci | utf8 | 223 | | Yes | 1 |
+--------------------------+---------+-----+---------+----------+---------+
27 rows in set (0.00 sec)
从返回的结果可以看到关于utf8的比较规则都有27种,这些比较规则的命名有一定的规律,具体规律如下:
MySQL 有4个级别的字符集和比较规则,分别是:
MySQL 提供了两个系统变量来表示服务器级别的字符集和比较规则:
系统变量 | 说明 |
---|---|
character_set_server | 服务器级别的字符集 |
collation_server | 服务器级别的比较规则 |
我们看一下这两个系统变量的值,记不住全称的可以用%进行匹配。
mysql> show variables like '%_server';
+----------------------+-----------------+
| Variable_name | Value |
+----------------------+-----------------+
| character_set_server | utf8 |
| collation_server | utf8_general_ci |
+----------------------+-----------------+
2 rows in set (0.01 sec)
可以看到在我的计算机中服务器级别默认的字符集是 utf8 ,默认的比较规则是utf8_general_ci 。
我们可以在启动服务器程序时通过启动选项或者在服务器程序运行过程中使用 SET 语句修改这两个变量的值。比如我们可以在配置文件中这样配置:
[server]
character_set_server=gbk
collation_server=gbk_chinese_ci
当服务器启动的时候读取这个配置文件后这两个系统变量的值便修改了。
我们在创建和修改数据库的时候可以指定该数据库的字符集和比较规则,具体语法如下:
CREATE DATABASE 数据库名
[[DEFAULT] CHARACTER SET 字符集名称]
[[DEFAULT] COLLATE 比较规则名称];
ALTER DATABASE 数据库名
[[DEFAULT] CHARACTER SET 字符集名称]
[[DEFAULT] COLLATE 比较规则名称];
其中的 DEFAULT 可以省略,并不影响语句的语义。比方说我们新创建一个名叫 charset_demo_db 的数据库,在创
建的时候指定它使用的字符集为 gb2312 ,比较规则为 gb2312_chinese_ci :
mysql> create database charset_demo_db character set gb2312 collate gb2312_chinese_ci;
Query OK, 1 row affected (0.00 sec)
mysql>
如果想查看当前数据库使用的字符集和比较规则,可以查看下面两个系统变量的值(前提是使用 USE 语句选择要查看的数据库,如果没有指定要查看的数据库,则变量与相应的服务器级系统变量具有相同的值):
#未指定数据库,值与服务器级别的系统变量一致
mysql> show variables like '%_database';
+------------------------+-----------------+
| Variable_name | Value |
+------------------------+-----------------+
| character_set_database | utf8 |
| collation_database | utf8_general_ci |
| skip_show_database | OFF |
+------------------------+-----------------+
3 rows in set (0.00 sec)
mysql> use charset_demo_db;
Database changed
mysql> show variables like '%_database';
+------------------------+-------------------+
| Variable_name | Value |
+------------------------+-------------------+
| character_set_database | gb2312 |
| collation_database | gb2312_chinese_ci |
| skip_show_database | OFF |
+------------------------+-------------------+
3 rows in set (0.01 sec)
可以看到这个 charset_demo_db 数据库的字符集和比较规则就是我们在创建语句中指定的。需要注意的是:数据库中存在的两个变量(character_set_database、collation_database)都是只读的,没法通过修改这两个变量的值来修改数据的字符集和比较规则,想要修改数据库的字符集和比较规则,需要在创建或者修改数据库的时手动指定character set和collation变量的值,如果不指定的话,则默认使用服务器级别的字符集和比较规则。
我们也可以在创建和修改表的时候指定表的字符集和比较规则,语法如下:
CREATE TABLE 表名 (列的信息)
[[DEFAULT] CHARACTER SET 字符集名称]
[COLLATE 比较规则名称]]
ALTER TABLE 表名
[[DEFAULT] CHARACTER SET 字符集名称]
[COLLATE 比较规则名称]
比方说我们在刚刚创建的 charset_demo_db 数据库中创建一个名为 test 的表,并指定这个表的字符集和比较规则:
mysql> create table test(name varchar(10) ) CHARACTER SET utf8 COLLATE utf8_general_ci;
Query OK, 0 rows affected (0.08 sec)
mysql>
如果创建和修改表的语句中没有指明字符集和比较规则,将使用该表所在数据库的字符集和比较规则作为该表的字符集和比较规则。假设我们的创建表 test 的语句是这么写的:
CREATE TABLE test(name varchar(10) ) ;
因为表test的建表语句中并没有明确指定字符集和比较规则,则表test 的字符集和比较规则将继承所在数据库charset_demo_db 的字符集和比较规则,也就是 gb2312 和gb2312_chinese_ci 。
需要注意的是,对于存储字符串的列,同一个表中的不同的列也可以有不同的字符集和比较规则。我们在创建和修改列定义的时候可以指定该列的字符集和比较规则,语法如下:
CREATE TABLE 表名(
列名 字符串类型 [CHARACTER SET 字符集名称] [COLLATE 比较规则名称],
其他列…
);
ALTER TABLE 表名 MODIFY 列名 字符串类型 [CHARACTER SET 字符集名称] [COLLATE 比较规则名称];
比如我们修改一下表 test 中列 name的字符集和比较规则可以这么写:
mysql> alter table test modify name varchar(10) charset gbk collate gbk_chinese_ci;
Query OK, 0 rows affected (0.21 sec)
Records: 0 Duplicates: 0 Warnings: 0
对于某个列来说,如果在创建和修改的语句中没有指明字符集和比较规则,将使用该列所在表的字符集和比较规则作为该列的字符集和比较规则。比方说表 test的字符集是 utf8 ,比较规则是 utf8_general_ci ,修改列 name的
语句是这么写的:
ALTER TABLE test MODIFY name VARCHAR(10);
那列name的字符集和编码将使用表test的字符集和比较规则,也就是utf8和utf8_general_ci 。
注意:在转换列的字符集时需要注意,如果转换前列中存储的数据不能用转换后的字符集进行表示会发生错误。比方说原先列使用的字符集是utf8,列中存储了一些汉字,而现在把列的字符集转换为ascii的话就会出错,因为ascii字符集并不能表示汉字字符。
由于字符集和比较规则是互相有联系的,如果我们只修改了字符集,比较规则也会跟着变化,如果只修改了比较规则,字符集也会跟着变化,具体规则如下:
不论哪个级别的字符集和比较规则,这两条规则都适用,下面以服务器级别的字符集和比较规则为例测试一下:
mysql> set character_set_server = gb2312;
Query OK, 0 rows affected (0.00 sec)
mysql> show variables like '%_server';
+----------------------+-------------------+
| Variable_name | Value |
+----------------------+-------------------+
| character_set_server | gb2312 |
| collation_server | gb2312_chinese_ci |
+----------------------+-------------------+
2 rows in set (0.00 sec)
mysql> set collation_server = utf8_general_ci;
Query OK, 0 rows affected (0.00 sec)
mysql> show variables like '%_server';
+----------------------+-----------------+
| Variable_name | Value |
+----------------------+-----------------+
| character_set_server | utf8 |
| collation_server | utf8_general_ci |
+----------------------+-----------------+
2 rows in set (0.00 sec)
下面总结以上4个级别字符集和比较规则之间的联系:
知道了这些规则之后,对于给定的表,我们应该知道它的各个列的字符集和比较规则是什么,从而根据这个列的类型来确定存储数据时每个列的实际数据占用的存储空间大小了。具体使用的字符集类型可以通过show create table test;
进行查看,注意表名需要修改成自己要查询的表。
字符串在计算机上的体现就是一个字节串,如果你使用不同字符集去解码这个字节串,最后得到的结果可能就是一串乱码。
我们知道字符 ‘我’ 在 utf8 字符集编码下的字节串长这样: 0xE68891 ,如果一个程序把这个字节串发送到另一个程序里,另一个程序用不同的字符集去解码这个字节串,假设使用的是 gbk 字符集来解释这串字节,解码过程就是这样的:
假设用 latin1 字符集去解释这串字节,解码过程如下:
如果接收 0xE68891 这个字节串的程序按照 utf8 字符集进行解码,然后又把它按照 gbk 字符集进行编码,最后编码后的字节串就是 0xCED2 ,我们把这个过程称为字符集的转换 ,也就是字符串 ‘我’ 从 utf8 字符集转换为gbk 字符集。
我们知道从客户端发往服务器的请求本质上就是一个字符串,服务器向客户端返回的结果本质上也是一个字符串,而字符串其实是使用某种字符集编码的二进制数据。这个字符串可不是使用一种字符集的编码方式一条道走到黑的,从发送请求到返回结果这个过程中伴随着多次字符集的转换,在这个过程中会用到3个系统变量,我们先把它们写出来看一下:
系统变量 | 描述 |
---|---|
character_set_client | 服务器解码请求时使用的字符集 |
character_set_connection | 服务器处理请求时会把请求字符串从 character_set_client 转为 character_set_connection |
character_set_results | 服务器向客户端返回数据时使用的字符集 |
这几个系统变量在我的计算机上的默认值如下(不同操作系统的默认值可能不同),注意只关注上面给的几个变量,其他的在测试过程中应该手动改过。
mysql> show variables like 'character_set_%';
+--------------------------+----------------------------------+
| Variable_name | Value |
+--------------------------+----------------------------------+
| character_set_client | utf8 |
| character_set_connection | utf8 |
| character_set_database | gb2312 |
| character_set_filesystem | binary |
| character_set_results | utf8 |
| character_set_server | utf8 |
| character_set_system | utf8 |
| character_sets_dir | /usr/local/mysql/share/charsets/ |
+--------------------------+----------------------------------+
8 rows in set (0.01 sec)
可以看到这三个系统变量的值都是 utf8 ,测试需要,我们这里修改其中一个系统变量的值:
mysql> set character_set_connection = gbk;
Query OK, 0 rows affected (0.00 sec)
所以现在系统变量 character_set_client 和 character_set_results 的值还是 utf8 ,而 character_set_connection 的值为 gbk 。现在假设我们客户端发送的请求是下边这个字符串:
SELECT * FROM t WHERE s = ‘我’;
为了方便大家理解这个过程,我们只分析字符 ‘我’ 在这个过程中字符集的转换。 现在看一下在请求从发送到结果返回过程中字符集的变化:
了解了在 MySQL 中从发送请求到返回结果过程里发生的各种字符集转换,有么有觉得在这过程中进行多次转换很麻烦,而且效率也会大打折扣?
答:确实跟我们想的一样,所以我们通常都把 character_set_client 、character_set_connection、 character_set_results 这三个系统变量设置成和客户端使用的字符集一致的情况,这样减少了很多无谓的字符集转换。为了方便我们设置, MySQL 提供了一条非常简便的语句:
SET NAMES 字符集名;
这一条语句产生的效果和我们执行这3条的效果是一样的:
SET character_set_client = 字符集名;
SET character_set_connection = 字符集名;
SET character_set_results = 字符集名;
比方说我的客户端使用的是 utf8 字符集,所以需要把这几个系统变量的值都设置为 utf8 :
mysql> SET names utf8;
Query OK, 0 rows affected (0.00 sec)
mysql> show variables like 'character_set_%';
+--------------------------+----------------------------------+
| Variable_name | Value |
+--------------------------+----------------------------------+
| character_set_client | utf8 |
| character_set_connection | utf8 |
| character_set_database | gb2312 |
| character_set_filesystem | binary |
| character_set_results | utf8 |
| character_set_server | utf8 |
| character_set_system | utf8 |
| character_sets_dir | /usr/local/mysql/share/charsets/ |
+--------------------------+----------------------------------+
8 rows in set (0.01 sec)
注意: 如果使用的是Windows系统作为客户端,那应该设置成gbk。
另外,如果你想在启动客户端的时候就把 character_set_client 、 character_set_connection 、 character_set_results 这三个系统变量的值设置成一样的,那我们可以在启动客户端的时候指定一个叫 default-character-set 的启动选项,比如在配置文件里可以这么写:
[client]
default-character-set=utf8
它起到的效果和执行一遍 SET NAMES utf8 是一样一样的,都会将那三个系统变量的值设置成 utf8 。
比较规则的作用通常体现比较字符串大小的表达式以及对某个字符串列进行排序,所以有时候也称为排序规则 。比方说表 test 的列 name 使用的字符集是 gbk ,使用的比较规则是 gbk_chinese_ci ,我们向里边插入几条记录:
mysql> INSERT INTO test(name) VALUES('a'), ('b'), ('A'), ('B');
Query OK, 4 rows affected (0.04 sec)
Records: 4 Duplicates: 0 Warnings: 0
mysql> select * from test order by name;
+------+
| name |
+------+
| a |
| A |
| b |
| B |
| 我 |
+------+
5 rows in set (0.00 sec)
mysql>
可以看到在默认的比较规则 gbk_chinese_ci 中是不区分大小写的,我们现在把列 name 的比较规则修改为 gbk_bin :
mysql> ALTER TABLE test MODIFY name VARCHAR(10) COLLATE gbk_bin;
Query OK, 5 rows affected (0.17 sec)
Records: 5 Duplicates: 0 Warnings: 0
由于 gbk_bin 是直接比较字符的编码,所以是区分大小写的,我们再看一下排序后的查询结果:
mysql> select * from test order by name;
+------+
| name |
+------+
| A |
| B |
| a |
| b |
| 我 |
+------+
5 rows in set (0.01 sec)
所以如果以后大家在对字符串做比较或者对某个字符串列做排序操作时没有得到想象中的结果,需要思考一下是不是字符规则的问题。
总结:总的来说,在选用字符集时要选择能支持表示字符的字符集,并尽量精简。其次,使用时,尽量保证客户端服务端(包括服务器级别,数据库级别,表级别,列级别)字符集及比较规则一致,避免进行多次转化及转换出错。
更多关于mysql的知识分享,请前往博客主页。编写过程中,难免出现差错,敬请指出