说实话,很多来看这个话题的人都是被标题里的“吃”字吸引过来的。我先把结论放在前面:utf8mb4_general_ci 和 utf8mb4_bin 最核心的区别,一句话就是“一个不区分大小写,一个区分大小写”。但如果你以为只有这一个区别,直接在业务里乱用,后面踩的坑大概率会让你想把自己吃下去重来一次。
用户名在 general_ci 下可能“撞车”,优惠码在 bin 下排序排得让人一头雾水,两个表的排序规则不一致直接抛 Illegal mix of collations 错误,甚至唯一索引在 general_ci 下会拦下你本来想放进去的数据……这些真实发生过的生产问题,都藏在这两个排序规则的行为差异里。这篇文章我会从原理到实测 SQL,再到选型建议和排错经验,把这两个排序规则彻底掰开揉碎讲清楚,保证你看完不需要吃任何东西。
1. 从一条“诡异”的登录 SQL 说起
假设你有一个用户表,创建的时候用了默认的 utf8mb4_general_ci,里面存了一个用户名为Admin的账号。然后用户在前端输入admin去登录,执行下面这条 SQL:
SELECT * FROM users WHERE username = 'admin';结果这个账号居然被查出来了。如果你还加了唯一索引,更麻烦的情况是:想注册admin的用户会被告知“用户名已被占用”,哪怕你数据库里根本没有小写的admin。
这就是 general_ci 的“ci”在做怪。它把A和a当成同一个字符看待。
那换成 bin 呢?
SELECT * FROM users WHERE username = 'admin' COLLATE utf8mb4_bin;这时候查询结果为空,因为Admin和admin在 bin 规则下是两个完全不同的字符串。
对很多程序猿来说,第一次踩到这种坑的时候,第一反应是“卧槽我的 SQL 写错了”,第二反应才是去查排序规则。所以我觉得有必要先把最基础的概念讲清楚,后面所有差异都是由这个概念延伸出来的。
1.1 先搞清楚:字符集和排序规则是两码事
很多人会把字符集(charset)和排序规则(collation)混在一起说,其实它们是两个层级的配置。
字符集解决的是“字符怎么存的”问题。utf8mb4 是一种字符集,它把每个字符映射成 1 到 4 个字节存储在数据库中。你可以把字符集理解成仓库里的货架,货架的形状决定了你能放什么尺寸的货物。utf8mb4 能放下 emoji、生僻汉字、各种特殊符号,因为它支持 4 字节字符。
排序规则解决的是“字符怎么比”的问题。同样是两个字符串,怎么判断它们相等?怎么排先后顺序?这就是 collation 干的事。拿仓库来类比,货架都有了,你还得定规矩:是按保质期先后来上架,还是按商品编码大小来上架?不同规矩,同一个仓库里商品的摆放顺序完全不一样。
所以字符集决定了你“能存什么”,排序规则决定了你“查出来的结果怎么比、怎么排”。
如果你在建表的时候只指定了字符集,没指定排序规则,MySQL 会用一个默认值。在 5.7 时代,这个默认值往往是utf8mb4_general_ci;在 8.0 时代,默认变成了utf8mb4_0900_ai_ci。很多人根本不知道自己的表里用的是什么排序规则,这是后面出问题的根源。
查看当前表用的什么排序规则,非常简单:
SHOW TABLE STATUS LIKE 'users';结果里有一列Collation,会直接告诉你这个表用的规则。
1.2 数据库层面设 utf8mb4,到底在设什么
你可能会说:“我建库的时候直接写了CHARACTER SET utf8mb4,不就行了?”
其实你只设了一半。完整的写法通常是:
CREATE DATABASE mydb CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;如果你只写了CHARACTER SET utf8mb4,MySQL 会用字符集对应的默认排序规则。问题在于不同 MySQL 版本默认值不一样,这就导致迁移环境的时候,可能本地是 general_ci,线上却变成了 0900_ai_ci,行为立刻变得不一致。
更值得注意的一点是,MySQL 里字符集和排序规则是一对多的关系。同一个utf8mb4字符集下面挂着一堆排序规则,常见的有:
utf8mb4_general_ciutf8mb4_binutf8mb4_unicode_ciutf8mb4_0900_ai_ciutf8mb4_0900_bin
所以“用 utf8mb4”和“用哪个 utf8mb4 规则”是两件事。搞清楚这个前提,我们再来看 general_ci 和 bin 的区别。
2. 核心机制:ci 和 bin 到底差在哪
很多人对这两个规则的认知停留在“ci 不区分大小写,bin 区分大小写”,这个方向没错,但远远不够。因为字符串比较的行为,除了大小写,还包括重音符号、尾随空格、排序顺序等多个维度。而且 general_ci 的“不敏感”并不是你想象的那种全方位不敏感。
2.1 general 的“不敏感”和你以为的不一样
先说 ci 的全称:case-insensitive,即大小写不敏感。general_ci 在处理英文字母时,会把A-Z和a-z折叠成同一组字符来比较,所以在它的规则下MySQL、mysql、MYsql都是同一个字符串。
但这里有个容易误解的地方:general_ci 不等于“所有字符都不敏感”。它对重音是敏感的。
什么意思?在utf8mb4_general_ci下,é和e是两个不同的字符,Å和A也不相等。这一点和 8.0 的utf8mb4_0900_ai_ci不一样,0900_ai_ci里的ai全称是 accent-insensitive,重音不敏感,在那种规则下é就能等于e。
还有更微妙的:MySQL 的字符串比较对于带尾随空格的情况,默认是忽略的。这两个排序规则都属于 PAD SPACE 族,也就是说'account'和'account '在一般等值比较下是相等的。这又是另一个容易埋雷的细节。
所以我们可以把 general_ci 的行为总结成一个矩阵:
| 行为维度 | utf8mb4_general_ci | utf8mb4_bin |
|---|---|---|
| 大小写是否敏感 | 不敏感 | 敏感 |
| 重音是否敏感 | 敏感 | 敏感 |
| 尾随空格是否忽略 | 忽略(PAD SPACE) | 忽略(PAD SPACE) |
| 比较方式 | 先大小写折叠,再按规则映射比较 | 直接按 UTF-8 字节或 Unicode 码点比较 |
| 排序结果 | 大小写混排、偏向人类习惯 | 严格按编码值排,大写集中在前 |
| 唯一索引行为 | Admin和admin会冲突 | 互不冲突,可同时存在 |
bin 的全称就是 binary,它做的事情非常简单粗暴:比较的时候直接看字节序。因为 utf8mb4 的编码和 Unicode 码点是单调映射的,所以 bin 规则下比较两个字符串,约等于按 Unicode 码点一个个字符去比。
2.2 关键差异一览:大小写、重音、尾随空格、排序
上面那张表信息量已经不小,但我觉得还是有必要把每个维度再拆细一点,因为你实际写 SQL 的时候,这些差异会直接暴露出来。
第一个维度是大小写。这是两个规则最直观的区别:
SELECT 'mysql' = 'MySQL' COLLATE utf8mb4_general_ci AS result;结果是 1,表示相等。
SELECT 'mysql' = 'MySQL' COLLATE utf8mb4_bin AS result;结果是 0,表示不相等。
第二个维度是重音。前面说过,general_ci 虽然是ci,但它只对大小写不敏感,对重音仍然敏感。拿法语字符试一下:
SELECT 'é' = 'e' COLLATE utf8mb4_general_ci AS result;结果是 0,é和e不相等。
这一点对多语言业务很重要。如果你的系统将来要放法语、德语这类带重音字符的内容,又希望重音和不带重音的字符等价,那么utf8mb4_general_ci根本不能满足你,你得用utf8mb4_0900_ai_ci。
第三个维度是尾随空格。这个坑特别隐蔽,平时大家几乎不会注意到。PAD SPACE 的意思是,比较两个字符串时,如果长度不一致,MySQL 会把较短字符串的末尾自动补一些空格,让两者长度对齐后再比较。所以:
SELECT 'mysql' = 'mysql ' COLLATE utf8mb4_general_ci AS result; SELECT 'mysql' = 'mysql ' COLLATE utf8mb4_bin AS result;这两个查询结果都是 1。
看到没有,bin 在这个场景下也会“容忍”尾随空格。这其实不符合很多人对 binary 的直觉,但 MySQL 对utf8mb4_bin的实现就是 PAD SPACE,而不是 NO PAD。如果你想要那种连一个空格都严格区分的效果,得用 MySQL 8.0 的utf8mb4_0900_bin,那个才是 NO PAD。
第四个维度是排序。同样一批数据,用不同排序规则得到的顺序完全不同。这个对列表页、分页查询影响很大,后面实测部分我会用 SQL 展示。
2.3 另外一个容易忽略的版本变化:8.0 的 0900 系
说实话,如果你用的 MySQL 是 8.0 及以上,你大概率已经在用utf8mb4_0900_ai_ci而不是utf8mb4_general_ci了,只是你没意识到。
MySQL 8.0 把默认字符集改成了 utf8mb4,默认排序规则改成了utf8mb4_0900_ai_ci。这个 0900 系列基于 Unicode 9.0 标准的排序算法,对多语言的支持比 general_ci 完善得多,重音不敏感,还改成了 NO PAD 行为。
这里有个现实问题:很多老项目是从 5.7 迁移到 8.0 的,表是以前建的,排序规则还停留在 general_ci。新表呢?默认已经变成 0900_ai_ci。于是同一个数据库里,不同表用着不同规则,联结查询的时候就会出幺蛾子。
这也是我为什么一直建议团队在建表的时候显式写明COLLATE,不要让默认值给你暗中做决定。
3. 实测演示:用 SQL 把两者“打回原形”
光讲理论是很虚的,我们直接建表、插入数据,看这两个排序规则在真实 SQL 里的表现。
3.1 准备数据:同一张表,两个排序规则
先建两张结构完全一样、只有排序规则不同的表:
CREATE TABLE t_general_ci ( name VARCHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci ); CREATE TABLE t_bin ( name VARCHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin );然后插入同一批数据:
INSERT INTO t_general_ci VALUES ('Apple'), ('apple'), ('Banana'), ('banana'), ('admin'); INSERT INTO t_bin VALUES ('Apple'), ('apple'), ('Banana'), ('banana'), ('admin');现在我们对这两张表分别查询等值条件和排序结果,差异马上就出来了。
3.2 等值查询和排序结果实测
先看等值查询。在 general_ci 表里执行:
SELECT * FROM t_general_ci WHERE name = 'APPLE';你会看到结果返回了Apple和apple两行。
同样的查询在 bin 表里:
SELECT * FROM t_bin WHERE name = 'APPLE';结果为空,因为APPLE是全大写的,和Apple、apple都不同。
然后看排序。执行:
SELECT * FROM t_general_ci ORDER BY name; SELECT * FROM t_bin ORDER BY name;general_ci 的结果大概是这样:大小写被折叠对待,apple和Apple会排在一起,整体视觉上符合人对英文字母表的认知。
bin 的结果就完全不一样了。因为大写字母的 Unicode 码点(65-90)比小写字母的码点(97-122)小,所以所有以大写开头的字符串会全部排在小写开头的字符串前面。你会看到像Apple、Banana先出现,然后是apple、banana。
这个排序差异在列表页也许无所谓,但如果你的业务依赖数据库排序做分页,换一个排序规则可能导致同样的数据被重复翻出来或者漏掉,对用户体验影响很大。
3.3 唯一索引下的“隐形合并”
再来做一个实验:在 general_ci 字段上建唯一索引,然后尝试插入Admin和admin。
CREATE TABLE users_general ( username VARCHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci UNIQUE ); INSERT INTO users_general VALUES ('Admin'); INSERT INTO users_general VALUES ('admin');第二条语句会直接报错,提示Duplicate entry。
原因很清晰:general_ci 认为Admin和admin是同一个字符串,因此唯一索引判定冲突。
同样的操作在 bin 字段上就没任何问题:
CREATE TABLE users_bin ( username VARCHAR(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin UNIQUE ); INSERT INTO users_bin VALUES ('Admin'); INSERT INTO users_bin VALUES ('admin');两条都能插入成功。
这个特性你说它是 bug 还是 feature?看业务。如果你希望用户名不允许大小写重复注册,那 general_ci 正好帮你省了代码层的逻辑。如果你希望Admin和admin是两个完全不同的账号,那你必须用 bin。
4. 性能与索引:bin 真的更快吗
网上一直有一种说法,说 bin 比较效率更高,推荐凡事都上 bin。这话有依据,但也很容易误导人。我们需要把比较成本、索引行为、报错现象分开来看。
4.1 比较成本拆解:字节比较 vs 规则比较
从算法角度说,bin 的比较确实更快。因为它的逻辑非常简单:拿出两个字符串的 UTF-8 字节序列,一个字节一个字节地比大小,本质上就是memcmp一类的操作。计算机对这类操作是非常高效的,而且几乎不需要额外的状态转换。
general_ci 就不一样了。它要先做大小写折叠,每个字符可能还要查映射表,然后才进入比较逻辑。虽然 MySQL 对这块做了很多性能优化,但不管怎么优化,计算步骤都比直接字节比较多。
所以在纯 CPU 密集、大量字符串比较的场景下,bin 有优势。但实际业务系统里,查询的瓶颈很少出现在字符串比较这一步,更多时候是磁盘 IO、索引回表、网络传输。你用一个带索引的等值查询,数据库通过 B-Tree 定位记录,比较次数非常有限,bin 和 general_ci 的差异完全可以忽略。
只有当你需要对超大结果集做ORDER BY,或者在大表上频繁做字符串排序时,才能感受到一点点性能差别。但为了这点差别去牺牲业务语义,完全不值得。
4.2 排序规则不一致导致的 Illegal mix 问题
这个坑我几乎每年都要帮人排查好几次,报错就是那句经典的:
ERROR 1267 (HY000): Illegal mix of collations (utf8mb4_general_ci,IMPLICIT) and (utf8mb4_bin,IMPLICIT) for operation '='什么叫 illegal mix?举个简单的例子。你有两张表,一张t_general_ci的字段是 general_ci,另一张t_bin的字段是 bin。你做联结查询:
SELECT * FROM t_general_ci g JOIN t_bin b ON g.name = b.name;MySQL 一看,两个字段的排序规则不同,不知道按谁的规则来比较,直接给你报错。
解决办法有几种。最简单的是在比较的时候显式指定规则:
SELECT * FROM t_general_ci g JOIN t_bin b ON g.name = b.name COLLATE utf8mb4_bin;或者反过来:
SELECT * FROM t_general_ci g JOIN t_bin b ON g.name COLLATE utf8mb4_bin = b.name;也可以用CONVERT转换:
SELECT * FROM t_general_ci g JOIN t_bin b ON CONVERT(g.name USING utf8mb4) COLLATE utf8mb4_bin = b.name;临时语法能解决问题,但长期来看还是得统一表的排序规则。比较合理的方式是:同库同表同一个排序规则,除非某个字段有特别明确的使用理由。
4.3 索引长度与 utf8mb4 的 191 字符魔咒
顺便说一个和排序规则一起出现的高频问题:utf8mb4 下建索引,字段长度稍微一长就直接报错。
utf8mb4 一个字符最多占 4 字节,老版本 InnoDB 的索引键最大限制是 767 字节,算下来单个索引列最多只能存 191 个字符。所以 5.6、5.7 时代,你给一个VARCHAR(255)字段加索引,经常踩到Specified key was too long。
解决办法要么是用前缀索引:
CREATE INDEX idx_name ON users (name(191));要么等 MySQL 8.0 用 DYNAMIC 行格式,索引键上限提升到 3072 字节,这个问题才有所缓解。
这跟 general_ci 还是 bin 没有直接关系,但只要你把字符集改成 utf8mb4,就一定会碰到。很多人在换了排序规则又发现索引失效或建不出来的时候,才会想起字节长度这个隐形的天花板。
5. 业务选型:哪些字段必须 bin,哪些适合 general_ci
讲完原理和实操,最后落到怎么选。我的观点非常简单:排序规则是一种业务语义的数据库表达,选哪个不是看“哪个更高级”,而是看你希望字符串之间怎么比较。
5.1 按业务语义一张表做选择
不同类型的字段,建议直接照下面这个表来。
| 字段类型 | 推荐排序规则 | 原因 |
|---|---|---|
| 用户名(登录账号) | general_ci / unicode_ci | 多数业务不希望出现Admin和admin同时存在,大小写不敏感更符合常规体验 |
| 邮箱 | general_ci / unicode_ci | 邮箱规范化时默认不区分大小写,能防止同一个邮箱注册多个账号 |
| 密码/哈希值 | bin | 哈希字符串大小写敏感,用 ci 会直接让不同哈希值互相匹配 |
| Token/API Key | bin | 这类值是精确匹配,任何字符差异都有意义 |
| 订单号/优惠码 | bin | 很多编码规则生成时区分大小写,排序也应该按精确码值 |
| 文章标题/标签 | general_ci 或 0900_ai_ci | 需要做模糊匹配和展示,大小写敏感反而影响体验 |
| 多语言内容(法语、德语) | 0900_ai_ci 优先 | 支持重音不敏感,排序也更符合 Unicode 标准 |
| 内部编码/状态码 | bin | 一般由系统生成,精确比较与排序更稳妥 |
需要注意,用户名选 general_ci 不代表一定安全。如果你还要求用户名里不能有大小写完全相同但视觉不同的字符,那就得配合应用层校验或者用更严格的规则。数据库排序规则只是第一道防线,不能解决所有问题。
5.2 迁移现有表:ALTER 或重建的注意事项
老项目想从 general_ci 改成 bin,或者反过来,操作本身不复杂:
ALTER TABLE users MODIFY username VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;也可以直接改表默认:
ALTER TABLE users DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;注意,MODIFY和DEFAULT的区别很大。改了默认值,只影响后续新增列,已有列不会变。想全表统一,就得逐列MODIFY,或者直接用:
ALTER TABLE users CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_bin;这个语句会转换整张表的字符集和排序规则,包括所有字符串列。
迁移前务必想清楚两个问题。
第一,排序规则的改变会影响索引比较。如果之前 general_ci 下建立的唯一索引,现在改成 bin,原本被拦住的大小写重复数据,之后可能就能插入了。应用逻辑没变的话,你可能在用户登录时突然出现“多个账号对应同一用户名”的诡异现象。
第二,大表ALTER TABLE会锁表或者消耗大量 IO。虽然 8.0 支持在线 DDL,但还是建议在低峰期操作,并先用小表演练一遍。我自己的习惯是先备份,然后导出一份到测试环境验证数据一致性,再在线上执行。
5.3 推荐配置:建表时就把规则写死
我见过太多团队把排序规则的决策完全交给数据库默认值,导致建出来的表五花八门。最好的方式是在建表 SQL 里显式写明COLLATE。
如果你希望用户名大小写不敏感,就写:
CREATE TABLE users ( username VARCHAR(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL );如果你希望密码哈希大小写敏感,就写:
CREATE TABLE user_auth ( password_hash VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL );同一个库中的字段,明确自己的语义,然后让代码和数据库的规则保持一致,这是最省心的做法。
6. 真实案例复盘与避坑清单
最后这部分,我想把自己踩过以及帮人排查过的几个典型问题复盘一下,都是真实发生过的事,供你对照排查。
6.1 案例一:用户名大小写锁号
有个朋友做社交 App,注册时允许用户自己输入用户名,表用的是 general_ci。某天客服收到一堆账号登录不上的投诉,一查发现是有用户注册了ALice,另一个人注册了Alice。因为 general_ci 的唯一索引判定这两个用户名相同,后注册的人直接被拒。
但问题出在登录侧:老用户 A 一直用ALice登录,某天改成了alice,系统按 general_ci 查,居然也能查到账号。看起来是“贴心”,可一旦账号走找回密码流程,验证逻辑对大小写敏感的字段做了严格匹配,就会出现数据库能查到但业务校验不通过的情况。
最后我们把用户名字段改成 bin,并且在应用层加了一道统一的用户名规范化处理:一律转成小写再注册。这样既防止了大小写撞车,又保持了用户输入的灵活性。
6.2 案例二:优惠码唯一索引报错
另一个场景是我们公司内部的营销系统,发优惠码时用随机生成的字符串,格式类似XK7a2BfQ。当时建表用的是 general_ci,并且给优惠码字段加了唯一索引。
上线第二天,运营反馈说系统提示优惠码重复。排查后发现,两个批次生成了xK7A2bFQ和XK7a2BfQ,这两串在 general_ci 下被判成同一个字符串,唯一索引直接拒绝第二条记录。
解决办法是把优惠码字段改成 bin,然后加一个普通索引(不需要唯一),唯一性在应用层生成的时候保证。从此再没出过这类问题。
6.3 避坑速查清单
我把平时最容易踩的点汇总成一张清单,你可以直接截图保存:
| 风险点 | 现象 | 规避方式 |
|---|---|---|
| 登录时大小写误判 | 输入大小写不同也能查到账号 | 业务侧明确用户名是否大小写不敏感,选用匹配的 collation |
| 唯一索引撞车 | 合法、不同的字符串被判定重复 | 对大小写区分的字段(令牌、码、hash)用 bin |
| 联结查询报 Illegal mix | join/比较时报错 | 两个字段统一 collation,或显式 COLLATE |
| 排序结果不符合预期 | 大写字母集中在前,分页数据错位 | 预期人类排序用 ci,预期精确二进制用 bin |
| 尾随空格被忽略 | 保存了带空格的值,查询时等效 | 理解 PAD SPACE 行为,需要严格比较用 0900_bin |
| 迁移后索引失效 | 改 collation 后部分 SQL 不能走索引 | MODIFY 前做 explain 检查执行计划 |
6.4 一个小技巧:用 COLLATE 临时验证问题
如果你怀疑线上某个字段的排序规则有问题,但不想立刻改表,可以用 COLLATE 在查询时临时指定规则,先验证行为是否符合预期。
打个比方,你要验证用户的输入在 bin 规则下是否唯一:
SELECT username, COUNT(*) FROM users GROUP BY username COLLATE utf8mb4_bin HAVING COUNT(*) > 1;这样你就可以快速知道,当前数据在更严格的比较规则下有没有隐藏的“重复”。注意这个写法在 GROUP BY 中可能出现索引失效,但作为一次性排查工具完全够用。
另外,千万不要在已经有线上流量的核心表上直接改排序规则。我见过有人觉得 bin 更“高级”,顺手把用户表全改了,结果应用层还能登录,但所有涉及大小写的唯一性逻辑全部失效,数据隔天就乱了。改之前先在测试环境完整模拟一遍。
我个人用到现在的体会是:collation 这个东西,平时看不见摸不着,一旦出问题,往往是那种“每条 SQL 看过去都没毛病,但整体行为就是不对”的疑难杂症。与其事后排查,不如在建表的时候多想两分钟,把每个字段的语义和排序规则对齐。USERNAME 要大小写不敏感,就大大方方用 general_ci;TOKEN、哈希、优惠码这一类机器生成且大小写敏感的值,就直接 bin。规则和业务语义保持一致,后面能省掉大把时间。
如果你现在正在追查一个莫名其妙的“字符相等但又不相等”的问题,先跑一句SHOW CREATE TABLE 表名;看看每个字段的 COLLATE 是什么,再对照这篇文章里的行为矩阵去排查,八成能定位到根因。