MySQL 8.4 字符集与排序规则选型:存储、比较行为与索引开销的取舍
目录
在 MySQL 8.4 LTS 中,服务端默认字符集延续了 utf8mb4,默认排序规则为 utf8mb4_0900_ai_ci。但在升级或混用的系统里,历史遗留的 utf8(即 utf8mb3)、latin1 与 utf8mb4_general_ci 仍然很常见。
字符集与排序规则的选型代价通常不会在建表时立刻暴露,而是在三个具体场景里体现:存储变长数据时的内存缓冲预分配与 3072 字节索引前缀限制、0900 系列规则下的 NO PAD 尾随空格比对行为,以及多表 JOIN 时因排序规则不一致导致的索引失效。
utf8mb4 与 utf8mb3:存储与索引前缀的取舍#
MySQL 历史版本中的 utf8 是 utf8mb3 的别名。在 MySQL 8.4 LTS 中,utf8mb3 处于官方废弃状态(Deprecated),执行包含该字符集的 DDL 会收到 Warning 1287 告警,官方计划在未来大版本中彻底移除该字符集。
字符覆盖范围与截断报错#
utf8mb3 仅支持 Unicode 基本多语言平面(BMP,U+0000 到 U+FFFF),最多使用 3 个字节表示一个字符。互联网业务常见的 Emoji 表情、部分罕见汉字以及历史符号位于辅助平面,编码需要 4 个字节。
当客户端尝试向 utf8mb3 字段写入 4 字节字符时,MySQL 在严格 SQL 模式(STRICT_TRANS_TABLES)下会直接抛出写入异常:
-- 尝试向 utf8mb3 列写入 Emoji
INSERT INTO user_profile (nickname) VALUES ('开发者😊');
-- ERROR 1366 (HY000): Incorrect string value: '\xF0\x9F\x98\x8A' for column 'nickname' at row 1
utf8mb4 每个字符使用 1 到 4 个字节变长存储,覆盖了全部辅助平面字符。面向终端用户的通用文本列,使用 utf8mb4 能够彻底避免由于 Emoji 或生僻字符输入导致的写入中断。
磁盘存储与内存缓冲区的差异#
InnoDB 引擎在磁盘上存储 VARCHAR 类型时采用变长紧凑编码。对于 ASCII 范围内的字符(如数字、英文字符),utf8mb4 与 latin1 或 utf8mb3 一样只消耗 1 个字节;对于绝大多数常用中文字符,utf8mb4 与 utf8mb3 消耗相同的 3 个字节。将字段从 utf8mb3 转换为 utf8mb4,存量纯文本在磁盘物理页中的体积并不会翻倍膨胀。
额外的资源消耗主要发生在查询执行时的内存开销:
- 内存临时表与执行缓冲:MySQL 的执行引擎(如 Sort Buffer、Hash Join 缓冲以及内存临时表结构)在处理变长列时,部分路径会依据字符集的最大可能字节宽度预估单行宽度。
VARCHAR(255)在utf8mb4下按最大255 × 4 = 1020字节预留槽位,在utf8mb3下按765字节预留,在单字节字符集下仅按255字节预留。这会导致较宽的临时结果集更早触及temptable_max_ram上限,促使内存临时表溢写到磁盘。 - 内部行图像传递:在排序(
filesort)阶段,额外的字节宽度预留会增加每一行排序元组的内存消耗,降低单次内存排序容纳的行数,增加磁盘归并段(Sort_merge_passes)出现的几率。
3072 字节索引前缀上限#
InnoDB 在 DYNAMIC 行格式下,单列索引的前缀长度上限为 3072 字节。计算前缀长度时,MySQL 必须按该字符集单字符所占的最大字节数进行折算:
utf8mb4 单字符最大占 4 字节:3072 / 4 = 768 字符
utf8mb3 单字符最大占 3 字节:3072 / 3 = 1024 字符
ascii / latin1 单字符最大占 1 字节:3072 / 1 = 3072 字符
如果表结构中存在 VARCHAR(1000) 类型的列,在 utf8mb4 下直接对其建立完整单列索引会触发错误:
CREATE TABLE token_registry (
token VARCHAR(1000) NOT NULL,
KEY idx_token (token)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- ERROR 1071 (42000): Specified key was too long; max key length is 3072 bytes
要为此类长字符串建立索引,要么使用前缀索引(如 KEY idx_token (token(255))),要么在确定字符仅包含 ASCII 时显式将该列定义为 CHARACTER SET ascii。
排序规则的选择:0900、通用规则与二进制比对#
排序规则的后缀直接标明了比较行为。四类典型规则在算法基准、空格处理以及大小写敏感度上存在明显分化:
utf8mb4_0900_ai_ci:现代默认规则与 NO PAD 语义#
MySQL 8.4 LTS 默认采用 utf8mb4_0900_ai_ci。该规则基于 Unicode Collation Algorithm (UCA) 9.0.0 规范,其中 ai(Accent Insensitive)表示比对时不区分重音标记(例如 e、é、ê 判定为相等),ci(Case Insensitive)表示大小写不敏感(A 与 a 判定为相等)。
与 MySQL 5.7 时代的排序规则相比,该规则的关键变化在于填充属性(PAD_ATTRIBUTE)为 NO PAD。
在 MySQL 5.7 及更早的传统排序规则中,填充属性均为 PAD SPACE,比对字符串时末尾空格会被自动忽略。而在 NO PAD 下,末尾空格作为实际字符参与比较与判重:
CREATE TABLE account (
username VARCHAR(32) NOT NULL,
UNIQUE KEY uk_username (username)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
INSERT INTO account (username) VALUES ('admin');
-- 在 NO PAD 规则下,以下插入会成功,两行共存:
INSERT INTO account (username) VALUES ('admin ');
-- 查询匹配必须带上空格,二者视为不同值:
SELECT * FROM account WHERE username = 'admin'; -- 返回 1 行
SELECT * FROM account WHERE username = 'admin '; -- 返回 1 行
如果业务系统在用户注册或登录时不执行输入修剪(Trim),NO PAD 会允许用户以带有空格的用户名注册成功,导致鉴权逻辑产生非预期的两份账号。
utf8mb4_0900_as_cs:区分重音与大小写的精确比对#
当字段需要严格区分大小写或重音符号时,应选用 utf8mb4_0900_as_cs。该规则的 as(Accent Sensitive)将 e 与 é 判定为不同字符,cs(Case Sensitive)将 A 与 a 判定为不同字符,填充属性同样保持 NO PAD。
这类规则适合用于存储兑换码、券码或区分大小写的英文密钥标识。与二进制排序规则相比,它依然遵循 UCA 9.0 规范,但保留了字符大小写和重音标记的敏感度。
utf8mb4_bin 与 utf8mb4_0900_bin:代码点与字节比对#
需要区分大小写时,另一种常见的做法是采用二进制排序规则:
SELECT COLLATION_NAME, PAD_ATTRIBUTE
FROM INFORMATION_SCHEMA.COLLATIONS
WHERE COLLATION_NAME IN ('utf8mb4_bin', 'utf8mb4_0900_bin');
返回结果展示了二者的核心差异:
| 排序规则 | 比较基准 | PAD_ATTRIBUTE | 适用场景 |
|---|---|---|---|
utf8mb4_0900_bin |
Unicode 9.0 代码点权重 | NO PAD |
现代表结构需要严格区分大小写且尾随空格敏感的文本列 |
utf8mb4_bin |
字符存储的原始字节值 | PAD SPACE |
兼容旧系统迁移,需要忽略尾随空格的精确比对字段 |
utf8mb4_0900_bin 不执行语言学权重扩展,性能高于语言学规则;但在存在尾随空格的场景下,它的判定结果与遗留的 utf8mb4_bin 不一致,混用两类表时需特别注意。
utf8mb4_general_ci:旧版默认规则的性能与兼容性#
utf8mb4_general_ci 是 MySQL 5.7 时代的常见默认值。它采用简化的查表比对算法,忽略了 Unicode 的复合字符扩展规则(例如德语中 ß 与 ss 的等价性不会被处理)。
在 MySQL 5.7 时代,该规则因为跳过了复杂的权重计算而拥有性能优势。但在 MySQL 8.0 及 8.4 中,Unicode 排序引擎经过重构,基于预生成的权重字符串直接进行内存对比,utf8mb4_0900_ai_ci 在绝大多数测试集下的执行性能已经追平或超越了 utf8mb4_general_ci。
在 MySQL 8.4 中新建表若继续指定 utf8mb4_general_ci,不会带来性能增益,反而会在与默认规则表关联时引入排序规则冲突。
专用字符集场景:BINARY、ascii 与 gb18030#
除面向终端用户的通用文本外,有三类结构化字段选用特化字符集或二进制类型能带来明确收益:
BINARY 与 VARBINARY:紧凑存储定长数据#
UUID、散列值、IPv6 等数据虽然习惯以十六进制字符串展示,但底层结构是定长的原始字节。
以 UUID 为例,标准的字符串格式为 36 字符(形如 550e8400-e29b-41d4-a716-446655440000)。若采用 CHAR(36) 存储,单行需要 36 字节。
MySQL 8.0 起内置了 UUID_TO_BIN() 与 BIN_TO_UUID() 函数,支持将 UUID 转换为 16 字节的二进制序列,并可通过调整时间戳高低位参数提升聚簇索引插入时的顺序性:
-- 将 UUID 字符串转换为 16 字节 BINARY,第二个参数 1 表示重排时间戳保持单调递增
CREATE TABLE orders (
id BINARY(16) NOT NULL,
user_id BIGINT UNSIGNED NOT NULL,
PRIMARY KEY (id)
) ENGINE=InnoDB;
INSERT INTO orders (id, user_id)
VALUES (UUID_TO_BIN(UUID(), 1), 10001);
SELECT BIN_TO_UUID(id, 1) AS id, user_id FROM orders;
采用 BINARY(16) 替代 CHAR(36) 的收益集中在存储密度上:单行主键缩减 20 字节。以一千万行的表为例,聚簇索引本体能省出约 200 MB 空间;二级索引叶子节点挂载主键值,每个二级索引同样减少数百兆。行宽收窄提高了单个 InnoDB 16KB 数据页容纳的行记录数,能直接提升 Buffer Pool 在高并发点查下的缓存命中率。
相应的代价是放弃了直接可读性。在命令行直接查询时输出的是原始十六进制字节序列,排查问题需通过 BIN_TO_UUID() 手动格式化;应用层也需要在实体映射中配置字节数组或自定义类型转换器。
对于 MD5(BINARY(16))、SHA-256(BINARY(32))以及 IPv4(BINARY(4),结合 INET_ATON() / INET_NTOA()),采用二进制类型均能获得同等的收益。
ascii 字符集:突破前缀长度与强校验#
当字段内容由纯 ASCII 字符构成(例如 ISO 3166-1 两位国家代码 country_code、三位货币代码 currency_code、短哈希标识)时,显式指定 CHARACTER SET ascii 具备明确的价值:
CREATE TABLE country_reference (
code CHAR(2) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
name VARCHAR(64) NOT NULL,
PRIMARY KEY (code)
) ENGINE=InnoDB;
ascii 字符集能够起到字段约束作用:存储引擎层会直接拒绝超出 0x00 到 0x7F 范围的字节,避免非法字符写入枚举与代码列。
另一项用途是绕开 InnoDB 单列索引 3072 字节的前缀上限。对于类似长 API 凭证或请求追踪令牌的字段,如果长度达到 VARCHAR(1000),在 utf8mb4 下按 4 字节折算需要 4000 字节,无法建立完整单列索引。在确认内容为纯 ASCII 字符后,声明为 CHARACTER SET ascii 单字符仅折算 1 字节,索引可以正常建立。
gb18030:纯中文海量归档存储#
GB18030 是中国国家强制标准字符集,MySQL 8.0.28 起完善了对 GB18030-2022 的支持,提供 gb18030_chinese_ci 与 gb18030_bin。
在存储层面上,绝大多数常用汉字在 GB18030 中编码为 2 字节,而在 UTF-8 中编码为 3 字节。
对于百亿规模的纯中文冷归档系统(如历史合同文书、全文日志留存、司法裁判文书库),将纯中文大文本字段采用 gb18030 存储,能够直接减少约 33% 的文本存储体积。
这一取舍的代价是系统边界的转码成本:现代微服务架构(RPC、REST、Kafka 序列化)默认基于 UTF-8 运作,应用与数据库通信时必须在网络驱动层或数据库接入层进行逐字符转码,消耗额外的 CPU 周期。若该字段需要与其他 utf8mb4 表执行关联操作,字符集不一致会导致严重的执行计划退化。
排序规则不匹配:隐式转换破坏索引#
在升级或混用的系统架构里,跨表关联也是最容易暴露字符集配置问题的环节。
关联字段排序规则冲突#
设想以下场景:系统从 MySQL 5.7 逐步演进至 MySQL 8.4。老业务表 legacy_users 建于早期,字段排序规则为 utf8mb4_general_ci;新业务表 user_tags 建于新版本,字段排序规则为默认的 utf8mb4_0900_ai_ci:
CREATE TABLE legacy_users (
user_code VARCHAR(64) NOT NULL,
user_name VARCHAR(64) NOT NULL,
PRIMARY KEY (user_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
CREATE TABLE user_tags (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
user_code VARCHAR(64) NOT NULL,
tag_id INT NOT NULL,
PRIMARY KEY (id),
KEY idx_user_code (user_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
当执行两表关联查询时:
SELECT *
FROM user_tags t
JOIN legacy_users u ON t.user_code = u.user_code
WHERE t.tag_id = 101;
在执行等值比较 t.user_code = u.user_code 时,两列的排序规则不同。MySQL 会评估两者的字符集强制性(Collation Coercibility)。由于两个字段都是显式字段列(Coercibility 为 2),MySQL 无法直接将一方隐式提升,直接执行可能触发以下报错:
ERROR 1267 (HY000): Illegal mix of collations (utf8mb4_0900_ai_ci,IMPLICIT) and (utf8mb4_general_ci,IMPLICIT) for operation '='
为了避免报错,应用往往在 SQL 中添加强制转换:
SELECT *
FROM user_tags t
JOIN legacy_users u ON t.user_code = CONVERT(u.user_code USING utf8mb4) COLLATE utf8mb4_0900_ai_ci
WHERE t.tag_id = 101;
索引失效与全表扫描#
添加转换虽然解决了比对报错,但代价直接反映在执行计划上。被转换的列包裹在函数与规则重算中,优化器无法继续对 legacy_users 的主键走 eq_ref 树形查找:
EXPLAIN
SELECT *
FROM user_tags t
JOIN legacy_users u ON t.user_code = CONVERT(u.user_code USING utf8mb4) COLLATE utf8mb4_0900_ai_ci
WHERE t.tag_id = 101;
| id | select_type | table | type | possible_keys | key | rows | Extra |
|---|---|---|---|---|---|---|---|
| 1 | SIMPLE | t | ref | idx_user_code | idx_user_code | 10 | Using index condition |
| 1 | SIMPLE | u | ALL | PRIMARY | NULL | 1000000 | Range checked for each record (index map: 0x1) |
legacy_users 的访问类型从预期的 eq_ref 退化为对一百万行数据的全表逐行动态运算(ALL 或 Range checked for each record)。单次查询耗时可能从 1 毫秒暴增至数秒。
彻底规避此类慢查询需要在表结构定义层面对齐排序规则:
ALTER TABLE legacy_users CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
连接层字符集的对齐配置#
应用与 MySQL 通信时,连接层协商的字符集若与表定义不一致,会引入额外的运行时转码开销。
MySQL 维护着五个字符集相关变量:
character_set_client:客户端发送语句使用的字符集。character_set_connection:服务端解析语句与字面常量时转换的目标字符集。character_set_results:服务端返回查询结果集时使用的字符集。character_set_database:当前连接上下文数据库的默认字符集。character_set_server:数据库实例全局级别的默认字符集。
客户端建立连接时如果未指定字符集,服务端会将连接变量设定为握手协商的结果。当连接字符集与表字符集存在偏差时,MySQL 在接收请求和发送结果时均需逐行转码,增加网络与执行线程的 CPU 占用。
在基于 Spring Boot 与 MySQL Connector/J 的应用中,推荐的配置实践如下:
- 服务端配置:在
my.cnf中明确设定实例全局默认值,消除依赖环境缺省值的歧义:[mysqld] character-set-server = utf8mb4 collation-server = utf8mb4_0900_ai_ci - 客户端 JDBC 连接串:对于 MySQL Connector/J 8.0.26 及以上版本,驱动默认会自动探测并与服务端的
utf8mb4对齐。避免在 JDBC URL 中保留过时的参数:# 推荐:保持简洁,由驱动自动协商 utf8mb4 spring.datasource.url=jdbc:mysql://localhost:3306/shop_db?useSSL=false&serverTimezone=UTC # 避免:老旧配置硬编码 UTF-8,可能导致驱动协商降级为 utf8mb3 # spring.datasource.url=jdbc:mysql://localhost:3306/shop_db?characterEncoding=UTF-8
字符集选型决策路径#
字符集与排序规则的整体选型路径如下:
紧凑存储,无字符集转码"] IsText -->|"是"| CheckScope{"是否包含多语言或 Emoji?"} CheckScope -->|"是(默认通用场景)"| UTF8MB4["采用 utf8mb4 字符集"] CheckScope -->|"否(固定英文字符)"| ASCIICheck{"是否需要突破 768 字符索引限制?"} ASCIICheck -->|"是"| ASCIIChoice["采用 ascii 字符集
单字符 1 字节,支持最长 3072 字节索引"] ASCIICheck -->|"否"| UTF8MB4 UTF8MB4 --> CheckCollation{"是否需要严格区分大小写/重音?"} CheckCollation -->|"是(密钥/代码标识)"| CSCollation["utf8mb4_0900_as_cs 或 utf8mb4_0900_bin
精确匹配"] CheckCollation -->|"否(通用业务文本)"| AIDefault["utf8mb4_0900_ai_ci(默认推荐)
注意 NO PAD 尾随空格语义"]
小结#
字符集与排序规则的选择直接决定了存储密度、比对语义与索引效率:
- 新业务统一以
utf8mb4与utf8mb4_0900_ai_ci为基准配置。utf8mb3在 MySQL 8.4 LTS 中已被废弃,存量库表应尽早规划替换。 - 注意
NO PAD填充属性带来的行为变更。从 5.7 的PAD SPACE迁移至 8.4 时,应用层应显式修剪用户输入,避免尾随空格导致唯一约束产生非预期数据。 - UUID、MD5、IPv6 等定长数据使用
BINARY存储。收窄主键行宽能提升二级索引叶子节点的记录密度,直接改善 Buffer Pool 的命中表现。 - 跨表关联字段必须严格统一字符集与排序规则。任何隐式转码都会使 B+Tree 索引失效,将常数级树形检索降级为全表动态扫描。