This repository has been archived on 2026-05-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
all-in-kingsoft/hhs/MySQL/05-字符集与排序规则.md
T
2026-05-17 00:06:11 +08:00

277 lines
9.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags: [MySQL, 字符集, Collation, utf8mb4, utf8mb3, character_set]
create time: 2026-05-16 00:00
---
# 字符集与排序规则
## 概述
字符集(Character Set)决定「哪些字符能被存储」,排序规则(Collation)决定「这些字符如何比较和排序」。选错字符集不仅会导致乱码,还可能引发索引失效和安全漏洞。
## 字符集速览
MySQL 支持超过 80 种字符集,但生产环境中真正有用的只有几个:
```mermaid
graph TB
subgraph "单字节"
A["latin1"] -->|"遗留系统"| A1["西欧语言"]
end
subgraph "多字节变长"
B["utf8<br/>MySQL 特有"] -->|"仅支持 BMP"| B1["最多 3 字节"]
C["utf8mb4"] -->|"完整 UTF-8"| C1["最多 4 字节"]
end
subgraph "中文相关"
D["gbk"] -->|"国标简体"| D1["2 字节"]
E["gb2312"] -->|"老国标"| E1["2 字节"]
F["big5"] -->|"繁体(台湾)"| F1["2 字节"]
end
subgraph "Unicode 其他"
G["ucs2"] -->|"UTF-16 BE"| G1["定长 2 字节"]
H["utf32"] -->|"UTF-32"| H1["定长 4 字节"]
end
style C fill:#00B6BC,color:#fff
style B fill:#EE5A24,color:#fff
```
### 为什么一定要用 utf8mb4?
MySQL 中的 `utf8` 实际上只是 **utf8mb3** —— 它最多支持 3 字节编码,无法表示四字节字符(emoji、生僻汉字、部分符号)。
```sql
-- 用 utf8 插入 emoji 会报错!
SET NAMES utf8;
INSERT INTO posts (content) VALUES ('Hello 🚀');
-- ERROR 1366: Incorrect string value: '\xF0\x9F\x9A\x80'
-- 换 utf8mb4 就没有问题
SET NAMES utf8mb4;
INSERT INTO posts (content) VALUES ('Hello 🚀'); -- OK ✅
```
> [!WARNING] utf8mb4 的性能影响
> 相比 latin1,utf8mb4 每个字符多占 1~3 字节。这会带来:
> - 同样的 VARCHAR 长度存的内容更少
> - 索引体积增大,缓存命中率下降
> - 网络传输量增加
>
> **建议**:除非明确知道不需要中文/emoji,否则一律使用 utf8mb4。现代 SSD 和内存足够应对这个开销。
## 排序规则 Collation
排序规则命名格式:`<字符集>_<语言>_<后缀>`,后缀含义:
| 后缀 | 含义 | 示例 |
|------|------|------|
| `_ci` | Case **I**nsensitive(忽略大小写) | `utf8mb4_general_ci` |
| `_cs` | Case **S**ensitive(区分大小写) | `utf8mb4_general_cs` |
| `_bin` | Binary(按字节比较) | `utf8mb4_bin` |
> [!NOTE] MySQL 8.0 的新后缀
> 升级到 8.0 后,你会发现默认排序规则多了两个新后缀:
> - `_ai_ci` = **A**ccent **I**nsensitive — 忽略重音差异(é = e)
> - `_as_ci` = **A**ccent **S**ensitive — 保留重音差异(é ≠ e)
>
> 这解决了一个历史痛点:`utf8mb4_general_ci` 在处理带重音的拉丁字母时不够精确。
### 常见 Collation 对比
```mermaid
flowchart TB
subgraph "不区分大小写 ci"
GC["utf8mb4_general_ci<br/>⚡ 快速但略不精确"]
UC["utf8mb4_unicode_ci<br/>📐 基于 Unicode 标准"]
AC["utf8mb4_0900_ai_ci<br/>✅ MySQL 8.0 默认"]
end
subgraph "区分大小写"
GS["utf8mb4_general_cs"]
US["utf8mb4_unicode_cs"]
AS["utf8mb4_0900_as_ci<br/>Accent/Space Insensitive"]
end
subgraph "严格二进制 bin"
B2["utf8mb4_bin<br/>直接比较字节值"]
end
IN1["输入 'abc' vs 'ABC'"] --> GC
IN1 --> UC
IN1 --> AC
IN2["输入 'abc' vs 'ABC'"] --> GS
IN2 --> US
IN2 --> AS
IN3["输入 'A' vs 'a'"] --> B2
GC --> R1["结果: a = b = c"]
UC --> R1
AC --> R1
GS --> R2["结果: a ≠ A"]
US --> R2
AS --> R2
B2 --> R3["结果: A ≠ a (0x41 ≠ 0x61)"]
style AC fill:#00B6BC,color:#fff
style B2 fill:#EE5A24,color:#fff
style GC fill:#C44569,color:#fff
```
### 该选哪个 Collation?
| 场景 | 推荐 | 理由 |
|------|------|------|
| **中文项目** | `utf8mb4_0900_as_ci` | MySQL 8.0 默认排序,基于 ICU 标准 |
| **英文项目** | `utf8mb4_0900_ai_ci` | AI = Accent Insensitive,忽略重音符号 |
| **邮箱/用户名** | `utf8mb4_bin` | 严格区分大小写 |
| **密码存储** | 永远不用 Collation——用 Hash | bcrypt/argon2 |
> [!TIP] 一个常见的选型误区
> 很多人看到中文项目就选 `utf8mb4_unicode_ci`,但在 MySQL 8.0 下推荐优先使用 `utf8mb4_0900_*` 系列。它们基于 ICU 标准,对亚洲语言(中日韩)的排序更准确,性能也更好。`utf8mb4_unicode_ci` 本质上是 Unicode 4.0.0 的实现,而 0900 基于 Unicode 9.0.0。
```sql
-- 设置整个数据库的默认字符集和排序规则
CREATE DATABASE app_db
CHARACTER SET utf8mb4
COLLATE utf8mb4_0900_ai_ci;
-- 单个表的覆盖
CREATE TABLE usernames (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50)
) ENGINE=InnoDB
DEFAULT CHARSET=utf8mb4
COLLATE=utf8mb4_bin; -- 用户名严格区分大小写
```
## 层级关系:字符集 → Collation → 作用域
```mermaid
graph BT
subgraph "Server 层(全局默认)"
S["server<br/>utf8mb4_0900_ai_ci"]
end
subgraph "Database 层(库级覆盖)"
DB["database<br/>utf8mb4_unicode_ci"]
end
subgraph "Table 层(表级覆盖)"
T["table<br/>utf8mb4_bin"]
end
subgraph "Column 层(列级最高优先级)"
C["column<br/>utf8mb4_general_ci"]
end
S --> DB --> T --> C
INFO["优先级:Column > Table > Database > Server<br/>每层都会覆盖上层的设置"]
style S fill:#5F2799,color:#fff
style DB fill:#3D97BE,color:#fff
style T fill:#FF9F43,color:#000
style C fill:#C44569,color:#fff
style INFO fill:#333,color:#fff
```
查询当前配置:
```sql
-- 查看所有层级的字符集和排序规则
SHOW VARIABLES LIKE 'character_set_server';
SHOW VARIABLES LIKE 'collation_server';
SHOW CREATE DATABASE app_db;
SHOW CREATE TABLE users\G
-- 当前会话的字符集
SHOW SESSION VARIABLES LIKE 'character_set%';
```
## Binlog 中的字符集注意
当启用 `binlog_format = ROW` 时,主从复制会在 binlog 中携带原始字节,不受 collation 影响。
但如果是 `STATEMENT` 模式,SQL 文本中的字符比较可能在主库和从库得出不同结果(如果两边的 collation 不一致)。这也是为什么生产推荐 `ROW` 格式的原因之一。
## Go 应用层字符集配置
光在数据库层面设置 utf8mb4 还不够——连接通道必须一致,否则客户端发送的字节会被服务端用错误的字符集解析,轻则乱码,重则报错。
### DSN 中指定 charset
```go
// ✅ 标准写法:DSN 中加上 charset=utf8mb4
dsn := "user:password@tcp(127.0.0.1:3306)/dbname?charset=utf8mb4&parseTime=True&loc=Local"
db, err := sql.Open("mysql", dsn)
```
> [!QUESTION] 为什么 DSN 里也要写 charset?
> MySQL 客户端和服务端建立连接时有一个「握手阶段」,双方会协商使用哪个字符集。如果不在 DSN 中声明,MySQL 驱动会使用服务器默认的字符集。当服务器默认不是 utf8mb4 时(比如 legacy 系统的 latin1),就会出现连接层数据面编码不一致的问题。
### 运行时动态切换
```go
// 某个极端场景下需要临时切换会话字符集
_, err := db.Exec("SET NAMES utf8mb4 COLLATE utf8mb4_0900_ai_ci")
// ⚠️ 注意:这影响整个连接,在连接池场景下要避免频繁调用
```
> [!WARNING] 连接池中的陷阱
> `sql.Open` 不会立即创建连接,而是在首次查询时懒加载。如果你的程序启动后从未执行过 SET NAMES,而服务端的 `character_set_server` 恰好不是 utf8mb4 —— 那第一个请求就可能触发乱码。
>
> **最佳实践**:在初始化连接池后立刻跑一次健康检查,确保连接已正确初始化。
```go
// 建完连接池后立即验证
err = db.Ping() // 这一步会实际创建一个连接
```
## utf8 → utf8mb4 迁移指南
这是一个经典的线上改造场景。如果你的历史系统用了 MySQL 的 `utf8`(其实是 utf8mb3),逐步迁移到 utf8mb4 的步骤如下:
### 迁移步骤
```sql
-- Step 1: 检查当前哪些表用了旧 utf8
SELECT table_schema, table_name, column_name, character_set_name
FROM information_schema.columns
WHERE character_set_name IS NOT NULL
AND character_set_name != 'utf8mb4'
ORDER BY table_schema, table_name;
-- Step 2: 修改表的默认字符集(不影响已有数据)
ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
-- Step 3: 修改数据库默认字符集(新建表会自动继承)
ALTER DATABASE app_db CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
-- Step 4: 修改应用层 DSN,确保连接层也使用 utf8mb4
-- user:pass@tcp(host:3306)/db?charset=utf8mb4&parseTime=True
```
> [!NOTE] ALTER TABLE CONVERT 的影响
> - 对于小表(万行级别),几乎是瞬时完成
> - 对于大表(千万行+),`CONVERT TO` 会重建整张表:复制全量数据 → 重建索引 → 替换原表
> - **建议在低峰期操作**,或使用 `pt-online-schema-change` 进行在线 DDL,避免业务中断
>
> 如果只是改默认字符集而不影响现有列定义,可以用:
> ```sql
> ALTER TABLE users DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
> -- 注意:这是 DEFAULT,不改变已有列的字符集!
> ```
### 常见踩坑
| 坑 | 现象 | 解法 |
|----|------|------|
| **只改了表,没改连接** | 能插入 emoji,但查出来是 `???` | DSN 加 `charset=utf8mb4` |
| **只改了表默认值,没 CONVERT** | 已有列仍是 utf8mb3 | 必须用 `CONVERT TO` 重新编码 |
| **GORM AutoMigrate 覆盖** | 手动改了字符集,下次 AutoMigrate 又被还原 | 禁用自动迁移,改用 migration 脚本管理 |
### 关联笔记
- [[hhs/Redis/02-核心数据类型]] — Redis 的 STRING 类型对多字节字符的处理
- [[hhs/GORM/02-模型定义]] — GORM 建模时的字符串长度设置要点
- [[hhs/MySQL/10-DDL 建表与结构变更]] — 大表 DDL 的在线变更策略
- [[hhs/MySQL/40-常见踩坑]] — 隐式转换等更多常见问题