1. utf8mb4_general_ci 到底管什么:从一次线上事故说起
先讲个我实际经手的工单。当时业务方反馈,用户系统里一个新注册用户怎么也插入不了记录,报错是 duplicate entry,明明我看了一眼数据库,根本没有同名用户。反复核对后才发现,库里已经有一个“Tom”,新用户叫“tom”,而账号字段上的唯一索引用的正是 utf8mb4_general_ci 排序规则。在它下面,“Tom”和“tom”被 MySQL 认为是同一个字符串,于是唯一索引直接挡了驾。
这类问题说大不大,但特别容易踩。很多人对 utf8mb4_general_ci 的认知停留在“这是 MySQL 建表时的默认字符集排序规则”,平时建表复制粘贴一行 CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci 就完事了,压根没想过它会参与字符串比较、索引匹配、排序输出这些底层行为。实际上,排序规则(collation)在 MySQL 里的影响范围远比表面看起来大,它决定的不只是 order by 时的字母顺序,还有等值比较、唯一约束、索引能否命中,甚至 join 能不能走索引。
这次我打算把 utf8mb4_general_ci 从头到尾拆一遍:它跟字符集有什么关系,ci 后缀到底意味着什么,它和 unicode_ci、0900_ai_ci、bin 这些规则有什么本质差别,线上怎么查、怎么改、改了会踩什么坑。如果你是后端开发或者 DBA,对字符串比较、用户唯一性这类需求比较敏感,这篇文章可以帮你省掉不少排查时间。
1.1 字符集和排序规则不能混为一谈
先说一个最常见的概念混淆。utf8mb4 是字符集(character set),utf8mb4_general_ci 是排序规则(collation),它俩不是一个层面的东西。字符集决定的是“这个字段能用哪些字符、每个字符怎么编码存储”,比如 utf8mb4 相比老 utf8,能用四个字节编码完整的 Unicode 字符,包括 Emoji 和很多生僻汉字。而排序规则决定的是“在比较两个字符串时,哪些字符算相等、排序时谁先谁后”。
打个比方,字符集是货架,排序规则是货架的管理制度。货架决定能放什么尺寸的商品,管理制度则决定“可口可乐”和“可口可樂”算不算同一个商品、货架上应该按什么顺序陈列。假如你在 MySQL 里看到一个字段类型是 varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci,那说明这个字段既能存 Emoji,又在比较时采用了 general_ci 这套“怎么看待字符相等”的逻辑。
这里有个很容易被忽略的点:很多人在排查乱码、字符串截断问题时,习惯把目光放在字符集上,比如确认 client_character_set、表的 charset 是不是 utf8mb4。但一旦遇到“字符串看起来不同,数据库却认为相同”这类诡异问题,真正的主角其实是排序规则。比如大小写不同的两个用户名、全角半角字符、带重音符号的字母,在不同排序规则下是否相等,结果可能完全不一样。如果一开始就搞混字符集和排序规则,排查方向很容易跑偏。
1.2 为什么 utf8mb4 会出现 general_ci 这种“通用但不精确”的规则
从历史角度来看,MySQL 很早就支持了 utf8 字符集,但早期那个 utf8 最多只能存 3 字节的字符,意思是基本平面的字符没问题,一旦遇到 Emoji、部分 CJK 扩展汉字,就存不进去,轻则变成问号,重则直接报错。后来 MySQL 推出了 utf8mb4,等于把 UTF-8 的完整编码都纳入支持。这个“mb4”就是 max bytes 4 的意思,单字符最多占四个字节。
字符集变大了,配套的排序规则自然也要跟上。但 Unicode 的排序规则本身是个巨复杂的体系,不同语言的字母顺序、大小写对应的等价关系、重音符号的处理方式,要做到国际化应用级别的精确,需要大量映射表和权重数据。在早期 MySQL 版本里,为了在“支持 Unicode 字符”和“保持足够快的比较性能”之间找平衡,就出现了 general 这一族排序规则。
general_ci 的实现思路更偏工程化,不太追求语言学的精确度。它把一些常见拉丁字母的变体直接做了折叠,比如很多带重音的字符会被映射到基础字母上,比较时忽略差异;同时它也不用 Unicode Collation Algorithm(UCA)那套精细的权重计算,而是用相对简单的字节映射和规则。好处是快,尤其早期 CPU 性能没那么强的时候,这种设计对高并发查询很友好。坏处也明显:它不保证符合特定语言或 Unicode 官方的排列习惯,某些边缘字符的比较结果在专业场景下会显得“粗糙”。
1.3 理解后缀:ci、ai、as、bin 分别代表什么
排序规则的名字并不是随便起的,里面每个缩写都携带明确语义。utf8mb4_general_ci 末尾的 ci 是 case insensitive 的缩写,意思是在比较字符串时忽略大小写,A 和 a 被视为同一个字符。所以前面提到的 “Tom” 和 “tom” 才会撞唯一索引。
类似的常见后缀还有 as(accent sensitive)和 ai(accent insensitive)。as 代表区分重音,比如 é 和 e 会被视为不同字符;ai 代表不区分重音,é 和 e 等价。还有个特殊的 bin,它不是 insensitive/sensitive 那一套,而是直接按字符的二进制编码值比较,规则最简单、最严格,大小写一个字节都不能差。
这里要特别提醒一点,很多人以为“只要选了 ci 就是忽略大小写而已”,其实 ci 除了大小写不敏感,在很多实现里同时也会忽略重音、甚至某些字符的变体等价关系。这是因为语言排序规则里,大小写只是其中一个维度,重音、全半角等维度通常一起被处理了。utf8mb4_general_ci 对重音的处理方式比较隐蔽,它会把带重音的拉丁字符折叠到基础字母,比如 á 和 a 在它眼里是相等的。这意味着如果你在用户真实姓名这种字段上建了唯一索引,又用 general_ci,那么“Jose”和“José”会被认
