编码错了不是「显示难看」,是「根本没存进去」。
MySQL 的 utf8 只收三字节,utf8mb4 才收四字节——emoji 就卡在这一个字节上。
校对规则决定「怎么比大小」:ci 不分大小写,bin 按字节比。
上一篇搬数据,这一篇讲「搬的时候按什么编码读写」。 它是全书最容易出「玄学问题」的一节:乱码、问号、插入失败、排序不对,根都在这儿。 和第 5 章建表、第 22 章 JDBC 连接的关系:三处字符集必须一致——服务端、库表、连接。 任何一处不对,都会在某一刻咬人。
一、一个字符到底占几个字节
UTF-8 是变长编码:ASCII 占 1 字节,汉字占 3 字节,emoji 占 4 字节。 这个「最多几字节」直接决定了你的列能存什么。
| 字符 | 码点 | UTF-8 字节数 | 三字节的 utf8 能存吗 |
|---|---|---|---|
| A | U+0041 | 1 | 能 |
| 中 | U+4E2D | 3 | 能 |
| 😀 | U+1F600 | 4 | 存不进去 |
| é | U+00E9 | 2 | 能 |
| ① | U+2460 | 3 | 能 |
所以「中文占两个字节」这句话是错的,那是 GBK 的事。 UTF-8 里一个汉字三字节,一个 emoji 四字节,这是两个必须记住的数。
二、utf8 和 utf8mb4:名字像,能力差一代
MySQL 的 utf8 是个历史遗留的名字,它最多只收三字节。
要收四字节,得用 utf8mb4(mb4 就是 most bytes 4)。
| 项 | utf8(别名 utf8mb3) | utf8mb4 |
|---|---|---|
| 最大字节数 | 3 | 4 |
| 能存什么 | 基本汉字、拉丁字母 | 全部 Unicode,含 emoji |
| 8.0 的默认值 | 不是 | 是 |
| 老库怎么升 | 要显式 CONVERT | —— |
为什么这个坑还在?因为 5.7 时代建的库全是 utf8。
库升到 8.0,字符集不会跟着变——数据文件里存的是三字节的东西,改不了。
三、校对规则:怎么比大小
字符集决定「能存什么」,校对规则决定「怎么比、怎么排」。 两件事经常被混成一件。
| 后缀 | 什么意思 | 用在哪 |
|---|---|---|
| _ci | case insensitive,不分大小写 | 用户名、邮箱、搜索 |
| _cs | case sensitive,分大小写 | 口令、令牌、区分大小写的编码 |
| _bin | 按字节比,最严格 | 哈希、二进制内容 |
| _ai | accent insensitive,不分重音 | é 和 e 视为相同 |
utf8mb4_general_ci 是 5.7 的老默认值,utf8mb4_0900_ai_ci 是 8.0 的新默认值。
后者按 Unicode 9.0 的规则排,中文和 emoji 的排序更准。
实用结论只有两条:
登录名、邮箱用 ci(用户不该因为大小写登不进去);
口令哈希、令牌用 bin(差一个字节就是不同的东西)。
四、混着用会怎样
同一个库里,如果两张表的字符集不一样,join 的时候数据库要先做隐式转换。
转换一发生,索引就用不上了。
这种问题最阴的地方是:EXPLAIN 里看不出原因,只能靠 SHOW CREATE TABLE 一张张对。
所以规范里要写死一条:同库所有表、所有列,字符集和校对规则必须一致。
连接层还有第三个地方要一致:
JDBC 连接串里的 characterEncoding、PHP 的 charset、命令行的 --default-character-set。
三处任一不对,都会出现「存进去是乱码」或者「查出来是问号」。
五、动手 Demo:编码格式,一个字几个字节的事
这一课把一个字符一个字符地量过去,看它在 UTF-8 里到底占几个字节。
然后真跑一遍校对规则的比较,看 ci 和 bin 差在哪。
五段:字节数、两种 utf8、校对规则、中文排序、混用的代价。
# 一、逐字符量字节数
samples = "A中😀é①"
for ch in samples:
n = len(ch.encode("utf-8"))
print(ch, "U+%04X" % ord(ch), n, "字节",
"能" if n <= 3 else "存不进三字节的 utf8")
# 二、校对规则:大小写敏不敏感
c.execute("SELECT 'a' = 'A'") # 0,假
c.execute("SELECT 'a' = 'A' COLLATE NOCASE") # 1,真
c.execute("SELECT 'a' < 'B'") # 0
c.execute("SELECT 'a' < 'B' COLLATE NOCASE") # 1
# 三、中文排序:码点序 vs 拼音序
names = ["张三", "李四", "王五", "赵六", "陈七"]
print(sorted(names)) # 按 Unicode 码点,不是拼音
# 拼音序:李 王 张 赵 陈 —— SQLite 没有拼音排序,MySQL 要中文校对规则
实跑输出:
--------------------------------------------------------------------
第十四课:编码格式,一个字几个字节的事
--------------------------------------------------------------------
**3.6 节讲设置编码格式。这一节看着像配置项,其实是数据正确性的底线。**
**编码错了,数据不是「显示难看」,是「根本没存进去」。**
一、一个字符到底占几个字节
--------------------------------------------------------------------
字符 码点 UTF-8 字节数 utf8(最多 3 字节)能存吗
------------------------------------------------------
A U+0041 1 能
中 U+4E2D 3 能
😀 U+1F600 4 存不进去
é U+00E9 2 能
① U+2460 3 能
**前两行和后面两个都没问题,第三行那个 emoji 是 4 字节。**
**这就是 MySQL 的 `utf8` 只收 3 字节、`utf8mb4` 才收 4 字节的分界线。**
二、utf8 和 utf8mb4,名字像,能力差一代
--------------------------------------------------------------------
项 utf8(别名 utf8mb3) utf8mb4
----------------------------------------------------------
最大字节数 3 4
能存的字符 基本汉字、拉丁字母 全部 Unicode,含 emoji
8.0 的默认值 不是 是
老库升级 要显式 CONVERT ——
**一个字的差别,代价是一整批数据存不进去。**
**所以新建库一律用 `utf8mb4`,没有例外。**
三、校对规则:怎么比大小
--------------------------------------------------------------------
SELECT 'a' = 'A'
用什么规则 比什么 结果
-------------------------------
默认比较 'a' = 'A' 假
COLLATE NOCASE 'a' = 'A' 真
默认比较 'a' < 'B' 假
COLLATE NOCASE 'a' < 'B' 真
**默认按码点比:大写 A 是 65、小写 a 是 97,所以 `'a' = 'A'` 是假。**
**加了 NOCASE 之后,字母不分大小写,`'a' = 'A'` 变成真。**
**这就是 MySQL 里 `utf8mb4_general_ci` 和 `utf8mb4_bin` 的差别:**
**`ci` 是「不分大小写」,`bin` 是「按字节比」。**
**登录名、邮箱这类字段要用 `ci`,口令哈希要用 `bin`。**
四、中文怎么排
--------------------------------------------------------------------
排序依据 结果
---------------------------------------------
按 Unicode 码点 张三、李四、王五、赵六、陈七
按拼音 李四、王五、张三、赵六、陈七
**第一行是真跑出来的;第二行是手写死的,因为 SQLite 没有拼音排序。**
**MySQL 要用 `utf8mb4_zh_0900_as_cs` 这类中文校对规则才有拼音序。**
**这件事的后果很实在:通讯录、名单、下拉框,排序不对就是不好用。**
五、混着用会怎样
--------------------------------------------------------------------
**一张表 utf8、另一张 utf8mb4,join 的时候数据库得先做隐式转换。**
**转换一发生,索引就用不上了。**
**这一条在本沙箱复现不了(SQLite 没有列级字符集),只能记住结论:**
**同库同表同列,字符集必须一致;不一致等于自己给自己埋了一个慢查询。**
第一段量出四个数:1、3、4、2、3。
第三行那个 4 就是 emoji,也是 utf8 和 utf8mb4 全部分歧的来源。
第二段四行比较,前两行回答「大小写算不算一样」,后两行回答「谁大谁小」。
默认 'a' = 'A' 是假,因为按码点比:A 是 65,a 是 97。
加了 NOCASE 之后变成真。
MySQL 里没有 COLLATE NOCASE 这个写法,
对应的是 utf8mb4_general_ci(不分)和 utf8mb4_bin(按字节)。
名字不同,规则一样。
第三段那个排序结果值得盯一眼:它不是拼音序。
按码点排是「张、李、王、赵、陈」,按拼音该是「陈、李、王、张、赵」。
通讯录、名单、下拉框,排序不对就是不好用。
30 秒实验
- 把
samples里的 emoji 换成两个汉字,再跑一遍。每行都是 3 字节,一个「存不进去」都没有。 这说明问题不在中文,在四字节字符。 - 把
sorted(names)改成sorted(names, key=lambda s: s.encode('gbk')),看结果变不变。 GBK 按拼音排,所以变了一点——但它不是通用做法,只是演示「排序依据不同,结果就不同」。 - 想一想:为什么
utf8这个名字会被 MySQL 用成「三字节版」? 因为它是在 Unicode 还没超出三万多个码点的年代定下的,那时候三字节够用。 后来 emoji 来了,名字已经改不动了。
常见误区
- 「一个汉字占两个字节。」 那是 GBK。UTF-8 里一个汉字占三字节。 UTF-8 是变长编码,1 到 4 字节都有,别记成固定值。
- 「
utf8就是 UTF-8,只是写法不同。」 MySQL 的utf8是三字节版,完整版叫utf8mb4。 8.0 之后官方已经在文档里把utf8标成utf8mb3了。 - 「排序规则只影响 ORDER BY,不影响别的。」
它影响所有比较:
=、IN、DISTINCT、GROUP BY、JOIN,还有索引能不能用。 两张表校对规则不同,join 时会隐式转换,索引直接失效。
自测三问(点开对答案)
1. UTF-8 里一个汉字、一个 emoji 各占几个字节? —— **汉字 3 字节,emoji 4 字节。** **所以三字节的 `utf8` 能存汉字、存不了 emoji。** 2. `_ci`、`_cs`、`_bin` 分别什么意思? —— **`_ci` 不分大小写,`_cs` 分大小写,`_bin` 按字节比。** **登录名用 `_ci`,口令哈希用 `_bin`。** 3. 为什么两张表字符集不同会让 join 变慢? —— **因为要先做隐式转换,转换之后索引的比较规则就对不上了,索引失效。**下一篇讲用户和权限:建一个账号要定三件事, 以及为什么「最小权限」不是口号,是能挡住事故的具体做法。
- ☐ (本篇完,下一篇 3.7 添加服务器新用户)