🛢️ 第3章 使用MySQL图形化管理工具
📄 一句话概括

编码错了不是「显示难看」,是「根本没存进去」。 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 秒实验

  1. 把 samples 里的 emoji 换成两个汉字,再跑一遍。每行都是 3 字节,一个「存不进去」都没有。 这说明问题不在中文,在四字节字符。
  2. 把 sorted(names) 改成 sorted(names, key=lambda s: s.encode('gbk')),看结果变不变。 GBK 按拼音排,所以变了一点——但它不是通用做法,只是演示「排序依据不同,结果就不同」。
  3. 想一想:为什么 utf8 这个名字会被 MySQL 用成「三字节版」? 因为它是在 Unicode 还没超出三万多个码点的年代定下的,那时候三字节够用。 后来 emoji 来了,名字已经改不动了。

常见误区

  1. 「一个汉字占两个字节。」 那是 GBK。UTF-8 里一个汉字占三字节。 UTF-8 是变长编码,1 到 4 字节都有,别记成固定值。
  2. 「utf8 就是 UTF-8,只是写法不同。」 MySQL 的 utf8 是三字节版,完整版叫 utf8mb4。 8.0 之后官方已经在文档里把 utf8 标成 utf8mb3 了。
  3. 「排序规则只影响 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 变慢? —— **因为要先做隐式转换,转换之后索引的比较规则就对不上了,索引失效。**

下一篇讲用户和权限:建一个账号要定三件事, 以及为什么「最小权限」不是口号,是能挡住事故的具体做法。

相关笔记