9.7 KiB
tags, create time
| tags | create time | ||||||
|---|---|---|---|---|---|---|---|
|
2026-05-16 00:00 |
字符集与排序规则
概述
字符集(Character Set)决定「哪些字符能被存储」,排序规则(Collation)决定「这些字符如何比较和排序」。选错字符集不仅会导致乱码,还可能引发索引失效和安全漏洞。
字符集速览
MySQL 支持超过 80 种字符集,但生产环境中真正有用的只有几个:
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、生僻汉字、部分符号)。
-- 用 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 Insensitive(忽略大小写) | utf8mb4_general_ci |
_cs |
Case Sensitive(区分大小写) | utf8mb4_general_cs |
_bin |
Binary(按字节比较) | utf8mb4_bin |
[!NOTE] MySQL 8.0 的新后缀 升级到 8.0 后,你会发现默认排序规则多了两个新后缀:
_ai_ci= Accent Insensitive — 忽略重音差异(é = e)_as_ci= Accent Sensitive — 保留重音差异(é ≠ e)这解决了一个历史痛点:
utf8mb4_general_ci在处理带重音的拉丁字母时不够精确。
常见 Collation 对比
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。
-- 设置整个数据库的默认字符集和排序规则
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 → 作用域
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
查询当前配置:
-- 查看所有层级的字符集和排序规则
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
// ✅ 标准写法: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),就会出现连接层数据面编码不一致的问题。
运行时动态切换
// 某个极端场景下需要临时切换会话字符集
_, err := db.Exec("SET NAMES utf8mb4 COLLATE utf8mb4_0900_ai_ci")
// ⚠️ 注意:这影响整个连接,在连接池场景下要避免频繁调用
[!WARNING] 连接池中的陷阱
sql.Open不会立即创建连接,而是在首次查询时懒加载。如果你的程序启动后从未执行过 SET NAMES,而服务端的character_set_server恰好不是 utf8mb4 —— 那第一个请求就可能触发乱码。最佳实践:在初始化连接池后立刻跑一次健康检查,确保连接已正确初始化。
// 建完连接池后立即验证
err = db.Ping() // 这一步会实际创建一个连接
utf8 → utf8mb4 迁移指南
这是一个经典的线上改造场景。如果你的历史系统用了 MySQL 的 utf8(其实是 utf8mb3),逐步迁移到 utf8mb4 的步骤如下:
迁移步骤
-- 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,避免业务中断如果只是改默认字符集而不影响现有列定义,可以用:
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)对字符集的影响