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

9.9 KiB
Raw Blame History

tags, create time
tags create time
MySQL
字符集
Collation
utf8mb4
utf8mb3
character_set
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%';

Binlog 中的字符集注意

当启用 binlog_format = ROW 时,主从复制会在 binlog 中携带原始字节,不受 collation 影响。

但如果是 STATEMENT 模式,SQL 文本中的字符比较可能在主库和从库得出不同结果(如果两边的 collation 不一致)。这也是为什么生产推荐 ROW 格式的原因之一。

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 脚本管理

关联笔记