--- 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
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
⚡ 快速但略不精确"] UC["utf8mb4_unicode_ci
📐 基于 Unicode 标准"] AC["utf8mb4_0900_ai_ci
✅ MySQL 8.0 默认"] end subgraph "区分大小写" GS["utf8mb4_general_cs"] US["utf8mb4_unicode_cs"] AS["utf8mb4_0900_as_ci
Accent/Space Insensitive"] end subgraph "严格二进制 bin" B2["utf8mb4_bin
直接比较字节值"] 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
utf8mb4_0900_ai_ci"] end subgraph "Database 层(库级覆盖)" DB["database
utf8mb4_unicode_ci"] end subgraph "Table 层(表级覆盖)" T["table
utf8mb4_bin"] end subgraph "Column 层(列级最高优先级)" C["column
utf8mb4_general_ci"] end S --> DB --> T --> C INFO["优先级:Column > Table > Database > Server
每层都会覆盖上层的设置"] 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)对字符集的影响