Files
cs-note/hhs/MySQL/01-入门基础/05-字符集与排序规则.md
T

273 lines
9.7 KiB
Markdown
Raw Normal View History

2026-05-24 11:42:38 +08:00
---
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%';
```
## 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/02-SQL核心/06-DDL 建表与结构变更]] — 大表 DDL 的在线变更策略
- [[hhs/MySQL/08-工程实践/40-常见踩坑]] — 隐式转换等更多常见问题
- [[hhs/MySQL/07-高可用与分布式/29-Binary Log]] — Binlog Format(ROW vs STATEMENT)对字符集的影响