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é”会被认为重复。这个行为在业务上到底合不合理,完全取决于你怎么设计字段的语义。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 排序规则不影响显示,但会影响这些核心行为
2.1 唯一索引为什么这么容易中招
回到最开头的用户注册事故。账号表结构大概是这样的:
sql复制CREATE TABLE `user` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(32) NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
在这个规则下,数据库判断 username 的唯一性时,会先按 general_ci 的规则做等价折叠。于是 “Tom”、“tom”、“TOM” 全被折叠成同一个比较键,谁先插入谁占位,后面的全被判为重复。如果业务上想区分大小写,这个建表方案本身就是错误的。
我见过不少团队把“用户名唯一”理解成“字符串完全一致才唯一”,于是天真地以为建个唯一索引就可以了,结果线上被花式注册搞到头大。通用解法有两条路。一条是把该字段的排序规则改成 utf8mb4_bin 或 utf8mb4_0900_bin,让比较变成大小写敏感;另一条是在应用层强制做归一化,比如注册时统一转成小写再存,比较时也统一小写后再查。两条路各有利弊:第一条是真的在数据库层区分大小写,但用户在登录时必须精确输入大小写,否则查不到;第二条是业务上把大小写视为等价,但存储时又只保留一种写法,既满足了唯一性,也照顾了用户登录体验。
就我个人的项目经验来说,绝大多数面向普通 C 端用户的系统,更合理的方案是第二条:用户名先小写化再落库,展示时可以保留原始大小写,但注册时把归一化后的值单独存一列,或者直接让 username 字段存小写。这样既避免了数据库排序规则这类底层细节对业务语义造成干扰,也让唯一约束怎么走都不会突然撞车。
2.2 字符集排序规则不一致可能让索引直接失效
比起唯一索引撞车,更容易让人头疼的另一类问题,是 join 或 where 比较时,两个字段明明都是 utf8mb4,却因为排序规则不同,导致索引用不上。MySQL 在做字符串比较时,如果两边排序规则不同,它会尝试找到一个兼容的排序规则。如果找不到,就可能做隐式转换,把其中一个字段临时转成另一种排序规则再做比较。一旦发生这种转换,字段上的索引往往就没法用了,执行计划会从索引查询变成全表扫描。
举个例子。订单表 user_id 关联用户表 id,两个表都用了 utf8mb4_general_ci 时,一切正常,关联能命中主键索引。但如果用户表是从老库迁移过来的,字段还是默认的 latin1 或者 utf8mb4_general_ci,而新的订单表建成了 utf8mb4_0900_ai_ci,那么在 join 时,MySQL 会认为两个字段的排序规则不一致,需要做隐式转换。这时候你用 EXPLAIN 看执行计划,会发现驱动表明明很小,但被驱动表却显示 type=ALL,全表扫描,扫描行数直接膨胀。
这种情况在平时开发环境很难发现,因为数据量小,全表扫描也就几十毫秒,看不出区别。等线上千万级数据一跑,接口就瞬间慢到超时。所以我有两条建议:一是建表规范里必须明确统一字符集和排序规则,避免出现同一套库不同表各用各的规则;二是 join 的字段类型要一致,varchar 不要和 char 关联,长度、字符集、排序规则尽量保持同一套配置。排查这类问题时,EXPLAIN 看到 type=ALL 或者 Extra 里出现 Using where; Using index condition 但不合理的,先检查两边的 collation 是否一致。
2.3 排序结果:general_ci 不会帮你做精细的本地化排序
排序规则影响的不只是等值比较,还有 order by。在英文场景下,utf8mb4_general_ci 和 utf8mb4_unicode_ci 排序出来的结果差别不大,大小写统一被忽略,A 到 Z 的顺序和直觉一致。但一旦字段内容包含来自不同语言的字符,比如德语、法语、西班牙语的重音字母,general_ci 的“通用但不精确”就暴露出来了。
举个例子,德语里有一个字符 ß(sharp s),等于 ss 的合体。在 utf8mb4_general_ci 里,它的处理方式和一个完整支持 UCA 的排序规则很不一样。部分版本中,ß 可能会被折叠成 s,这会导致排序时出现比较奇怪的位置,比如 “Straße” 被当成 “Strasse” 来排还能理解,但如果某些实现把 ß 当 s,就可能导致字典顺序错乱。相比之下,MySQL 8.0 默认的 utf8mb4_0900_ai_ci 是基于 Unicode 9.0 的默认权重表构建的,它对多语言排序的精细程度更高。
所以业务选型时要想清楚一个问题:你的排序结果对用户有多重要?如果只是商品列表按名称排、后台用户列表按昵称排,那 general_ci 完全够用,而且性能通常还更稳定。但如果你的产品是面向多语言市场的,需要按当地语言习惯展示词条、联系人、标签,比如德语区用户希望看到 ä、ö、ü 按照德语规则排列在合适位置,那 general_ci 就满足不了,需要上 unicode 族或 0900 族规则。
2.4 general_ci、unicode_ci、0900_ai_ci 到底差在哪
对比排序规则,不能只看名字。我整理过一个对照维度,列在这里方便查:
| 对比项 | utf8mb4_general_ci | utf8mb4_unicode_ci | utf8mb4_0900_ai_ci |
|---|---|---|---|
| 基于的算法 | MySQL 早期简化比较逻辑 | Unicode Collation Algorithm 老版本 | Unicode Collation Algorithm 9.0 权重表 |
| 是否区分大小写 | 不区分 | 不区分 | 不区分 |
| 是否区分重音 | 基本不区分 | 不区分 | 不区分(ai 结尾) |
| 是否支持补充字符 | 支持 | 支持 | 支持 |
| 多语言排序精细度 | 较低 | 中等 | 较高 |
| 版本支持 | MySQL 5.5+ | 老版本默认之一 | MySQL 8.0 默认规则 |
| 比较性能 | 早期实现精简,老版本略快 | 老版本略慢 | 新版本经过优化,通常不差 |
从实际使用的角度讲,utf8mb4_general_ci 最大的特点是“简单直接”,在 MySQL 5.7 及以下版本它是建表时最常见的默认规则之一;utf8mb4_unicode_ci 虽然在语言排序上更精细,但老版本里基于 UCA 的比较开销相对高一些,在高频短字符串查询场景,两者差距仍然可能体现出来;而 utf8mb4_0900_ai_ci 是 8.0 之后的默认选项,它同时吸收了 Unicode 官方排序算法的精细度,并且在新版本里性能也做过头了,不再需要像以前那样为了性能牺牲正确性。
需要注意的是,字符排序规则的选择如果能不混合就尽量不混合,因为互相比较时可能触发隐式转换。比如你有一个老库存量表用 general_ci,新表用了 0900_ai_ci,两边做关联查询或 union 时,很容易出现 collation mismatch 错误,或者性能骤降。
3. 线上查改排序规则的完整实操
3.1 怎么快速查出当前库、表、列用的排序规则
查排序规则前,先明确它能出现在 MySQL 的四个层级:server 层、database 层、table 层、column 层。越往下的设置优先级越高,column 层如果单独指定了 collate,那它比表级、库级都优先。所以排查问题时,光看库的默认规则没用,得看真实字段上的规则。
查 server 层默认值:
sql复制SHOW VARIABLES LIKE 'collation_server';
SHOW VARIABLES LIKE 'character_set_server';
查某个库的默认规则:
sql复制SELECT DEFAULT_CHARACTER_SET_NAME, DEFAULT_COLLATION_NAME
FROM information_schema.SCHEMATA
WHERE SCHEMA_NAME = 'your_db';
查某张表:
sql复制SELECT TABLE_COLLATION
FROM information_schema.TABLES
WHERE TABLE_SCHEMA = 'your_db' AND TABLE_NAME = 'your_table';
查具体列的规则:
sql复制SELECT COLUMN_NAME, CHARACTER_SET_NAME, COLLATION_NAME
FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = 'your_db' AND TABLE_NAME = 'your_table'
AND COLUMN_NAME IN ('username', 'email');
我平时排查隐式转换问题时,习惯直接用上面最后一个语句,把关联查询涉及的两张表的 join 字段列出来,对比它们的 CHARACTER_SET_NAME 和 COLLATION_NAME 是否完全一致。很多时候问题一眼就暴露了:一边是 utf8mb4_general_ci,另一边是 utf8mb4_unicode_ci,就差一行设置,执行计划就天上地下。
还有一个快捷方法:直接看建表语句。
sql复制SHOW CREATE TABLE your_table;
它会明确显示表级的 DEFAULT CHARSET 和 COLLATE,但需要留意,这只是表默认规则。如果某些列单独指定了 COLLATE,那 SHOW CREATE TABLE 里列定义也会带上 COLLATE 字样,排查时重点看列定义。
3.2 修改列排序规则时,锁表、重建、数据长度都要考虑
修改排序规则不是改个字段属性那么简单,它可能触发整张表的重建。拿最常见的操作来说:
sql复制ALTER TABLE your_table CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
这条语句会把表内所有字符类型列都转成新的字符集和排序规则。注意,它做的是“转换”,不是“只看 collation”。如果你表里有的列本来就是 utf8mb4、有的列可能混着 latin1,这条语句会把所有列统一成 character set utf8mb4,这会改变存储编码,触发全表的数据重写,表越大,耗时越长,产生的 binlog 也越大。
如果只是想把某张表的默认排序规则改掉,但已有的列不处理,可以用:
sql复制ALTER TABLE your_table DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;
这条语句只影响之后新建列或者没显式指定字符集的列,已经存在并且显式继承旧规则的表列不会自动变更。想改单独某一列,需要逐个列操作:
sql复制ALTER TABLE your_table MODIFY COLUMN username varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL;
这里有几个必须在操作前确认的坑:
第一,改排序规则导致的表重建,在 InnoDB 里通常会加排他锁,至少需要拿到 MDL(metadata lock),具体锁粒度与算法版本有关。如果是亿级大表,一个 ALTER 上去可能锁表几小时,业务直接停摆。遇到这种情况,我的建议是别直接在生产执行原生 ALTER,优先用 pt-online-schema-change(pt-osc)或者 gh-ost 这类在线改表工具。它们通过触发器或者 Binlog 同步方式,先在临时表上完成结构变更,然后增量追数据,最后切换,降低对线上读写的影响。
第二,varchar 的长度不要只看字符数量。utf8mb4 一个字符最多占 4 字节,如果原列是 latin1 的 varchar(255),转成 utf8mb4 后最多占 1020 字节,可能超过单列索引 767 字节或 3072 字节的上限,导致建索引失败或需要调整长度。所以把整表 CONVERT 到 utf8mb4 前,一定要先检查哪些索引列可能会超长,尤其那种一个索引包含多个 varchar 大字段的组合。
第三,改完排序规则后,外键、生成的列、视图这些依赖列类型的对象也可能受影响。外键关联的两列,字符集和排序规则必须一致,否则 MySQL 直接拒绝执行 ALTER。我见过一个案例,改表前没检查外键,ALTER 报错 ERROR 3780,就是因为父表和子表外键列的类型排序规则不统一。操作前先查一下这张表是否被外键关联,并且把两边列的规则一起规划好。
3.3 新表怎么做才不会有历史包袱
建新表时,把所有规则写在明面上,不要依赖服务器全局默认值。全局默认值可能在测试库、生产库、灾备库之间不一样,一旦环境之间做数据同步或对比,差异非常难查。一个比较稳妥的建表模板是这样的:
sql复制CREATE TABLE `user` (
`id` bigint unsigned NOT NULL AUTO_INCREMENT,
`username` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL,
`nickname` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`email` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci DEFAULT NULL,
`created_at` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
如果团队用的 MySQL 版本统一是 8.0,没有历史包袱,新库可以直接把默认规则定为 utf8mb4_0900_ai_ci;但如果团队内部同时存在 5.7 和 8.0 实例,或者有数据要从 5.7 迁移到 8.0,那新表也建议沿用 utf8mb4_general_ci,防止跨版本同步时排序规则出现奇怪的兼容问题。底层逻辑很简单:与其在一个点位上追求“最优规则”,不如先保证整个技术栈里所有实例的排序规则完全一致,一致性才是第一优先级。
4. 常见问题与排查技巧实录
4.1 为什么 EXPLAIN 显示全表扫描,索引明明存在
这类问题我在第三部分已经提到过,这里再展开一个真实常见场景。假设有用户表和登录日志表,登录日志表存了 username 冗余字段,查询时要 join 用户表。两张表都用 utf8mb4,但用户表是老库迁移来的,列上 COLLATE utf8mb4_general_ci,登录日志表是后来新项目建的,默认变成 utf8mb4_0900_ai_ci。
sql复制SELECT u.id, l.login_time
FROM login_log l
LEFT JOIN user u ON l.username = u.username
WHERE l.login_time >= '2024-01-01';
执行计划里,user 表明明有 uk_username 唯一索引,但 EXPLAIN 显示的却是 type=ALL,并且 Extra 里常出现 Using where。因为 MySQL 需要把 l.username 和 u.username 两个字符串比较,两者排序规则不同,它会对其中一列做隐式排序规则转换。这种转换一旦发生,索引就基本失效。解决方式并不是去 SQL 里写 FORCE INDEX,而是检查两边的 collation,统一成同一个规则。
具体排查可以用:
sql复制EXPLAIN SELECT ...
SHOW WARNINGS;
如果看到类似 “Cannot use range access on index 'uk_username' due to type or collation conversion on field 'username'” 的提示,那就坐实了排序规则不一致引起的隐式转换。
4.2 改完规则后,原本正常的查询反而报错或者变慢
有次我帮一个团队做字符集标准化,把一张表的 collation 从 utf8mb4_general_ci 改成 utf8mb4_0900_ai_ci,结果业务方反馈,某个后台查询开始报错,提示 Illegal mix of collations。原因很简单:这张表单表改了规则,但它关联的另一张表还是旧的 general_ci,两边在做字符串比较时,MySQL 找不到一个能同时兼容的排序规则,直接抛错。
这种问题在规范化过程中几乎必然出现,不能只改一张表,得把所有关联表一起纳入改动范围。做法是先查清所有通过外键、join、union 与目标表发生字符串比较的字段,形成一份清单,统一升级或统一回退。改完以后,最好在测试环境造一些真实数据,把核心查询语句的 EXPLAIN 全部跑一遍,确认执行计划没有劣化。
4.3 “排序结果不对”怎么确认是不是 general_ci 的锅
如果客户端反馈某些名称排序结果不对,先别急着改排序规则,先做两个小验证。用 SELECT 直接对比两个字符串,看数据库认为谁大谁小:
sql复制SELECT STRCMP('ä', 'z') AS cmp_az;
在 utf8mb4_general_ci 下,ä 的处理可能接近 a,所以和 z 比较时会认为 ‘ä’ < ‘z’;但如果是纯字母序,一般也是这个结果。真正出问题的是当字符集里包含德语、北欧语字符时。此时建议换个方式,直接用 HEX 看字符编码:
sql复制SELECT HEX('ä'), HEX('z');
如果字段里存的是 Unicode 码点范围内但带组合标记的字符串,或类似全角、半角字符混在一起,排序表现不一定符合直觉。在做排序规则选型时,我建议先拿业务真实数据样本,在两套不同排序规则下分别执行 ORDER BY,导出结果对比。把直观结果给业务方确认,多数时候比看文档更管用。
4.4 迁移、同步后主从排序规则不一致
数据库迁移工具很多会自动同步表结构,比如从 5.7 迁移到 8.0 时,源库的 utf8mb4_general_ci 会被原样带过去,但如果迁移脚本里有人手工改过建表语句,或者中间经过了其他平台的 schema 同步,出现排序规则漂移并不意外。结果就是主库和从库返回的排序结果不一致,或者在主从切换后,应用某个依赖特殊排序的 SQL 行为发生变化。
我的建议是,在做版本升级、迁移演练时,把所有库的 default collation 和所有字符列的 collation 拉出来做一次镜像对比,可以写个临时脚本查询 information_schema.COLUMNS,输出一张全表列清单,再用 diff 工具对比源端和目标端。这一步虽然琐碎,但能在正式割接前就把隐性差异拦截在外。
5. 排序规则选型,我实际用下来的几条心得
5.1 版本是绕不开的前置条件
如果你的 MySQL 还是 5.7,那 utf8mb4_general_ci 就是最稳妥的日常选择。5.7 版本里虽然也有 utf8mb4_unicode_ci,但它的排序权重依赖老版 UCA,性能上也不比 general_ci 好多少,多语言排序的收益并不明显。如果业务场景并没有严格的多语言排序需求,没必要为了“听起来更标准”去选 unicode_ci。
如果你的环境已经是 8.0,而且所有业务代码都运行在新实例上,完全可以把默认规则从 general_ci 换成 utf8mb4_0900_ai_ci。这个规则在性能和精确度之间平衡得更好,而且作为官方默认值,社区生态、工具链对它的支持也最完善。但前提是,你也要接受一次规则统一带来的改动成本,不能只换库不换代码。
5.2 唯一性字段的排序规则要单独设计
不能因为表默认是 utf8mb4_general_ci,就让它把所有字符串字段的唯一性语义也给定死了。像用户名、邮箱这类字段,到底要不要区分大小写,这是业务规则问题,不应该由数据库默认值悄悄决定。我的做法是在设计表结构时,单独给这类字段指定显式 COLLATE,并在建表评审时把规则写清楚。
比如需要严格区分大小写的邮箱,字段可以写成:
sql复制`email` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL
但也要注意,utf8mb4_bin 会启用大小写敏感,在存储层意味着 “Tom” 和 “tom” 能同时存在。如果应用层登录时还把用户输入原样传给数据库查询,那用户必须每次精确输入大小写。正确做法通常是应用层先做归一化,比如邮箱转小写后再比较。数据库的排序规则只是一个安全兜底,不能替代业务规则的实现。
5.3 多数项目不需要纠结排序规则的性能差异
早期总有人拿 MySQL 5.6、5.7 的性能基准说 utf8mb4_unicode_ci 比 utf8mb4_general_ci 慢,然后得出结论说 general_ci 更快。这个结论放在当时有一定道理,因为 mdbt; Unicode 排序规则涉及更复杂的权重表。但到了 8.0 之后,排序规则比较性能的差距已经很小,实际瓶颈更多集中在表连接方式、索引命中率和数据量本身。
我记得有一次排查一个列表接口慢查询,一开始怀疑是排序规则太复杂,后来用 profile 看瓶颈,发现是 SQL 里对某个函数字段做了排序,导致索引失效。所以遇到性能问题,先不要急着归因到 collation,而应该先看是否隐式转换、是否函数参与比较、是否驱动表过大。排序规则只有在跨规则比较时,才可能通过“让索引失效”的方式对性能产生显著影响。
5.4 统一规范比选择哪个规则更重要
从工程管理角度看,utf8mb4_general_ci 和 utf8mb4_0900_ai_ci 之间的差距,远小于“同一套业务里两种规则混用”带来的维护成本。你从建表模板、数据迁移、代码生成器、ORM 映射这些环节把它固定下来,让开发人员不再关心 collation,才是真正省心的事。
我这些年维护过的数据库里,出问题的从来不是“选错了某一种规则”,而是“有的表用 A 规则,有的表用 B 规则,线上 join 时整个执行计划就乱了”。MySQL 的排序规则设计里,一致性本身就是一套强大的规则——只要你全链路统一,绝大多数隐含问题都能从源头规避。
最后再分享一个操作细节。我在做数据库巡检时,总会顺手执行这句 SQL,把所有“字符集是 utf8mb4 但排序规则不是 utf8mb4_general_ci 也不是 utf8mb4_0900_ai_ci”的列揪出来:
sql复制SELECT TABLE_SCHEMA, TABLE_NAME, COLUMN_NAME, COLLATION_NAME
FROM information_schema.COLUMNS
WHERE CHARACTER_SET_NAME = 'utf8mb4'
AND COLLATION_NAME NOT IN ('utf8mb4_general_ci', 'utf8mb4_0900_ai_ci');
任何不属于“团队统一规则白名单”的列,基本就是隐藏隐患的入口,早点发现,早做收敛计划。实际执行时别一股脑 ALTER,按列整理成迁移工单,分批改,每批观察监控,最后再做一次全量比对。数据库修改这件事,慢就是快。
