2605 字
约 8 分钟
15
S08-A17 · 排序规则与比较语义:为什么"大小写不同"算重复

注册接口拒绝了一个手机号,理由是"已被占用"——而数据库里查出来的那条记录,手机号是带空格的。原因是表用了 utf8mb4_general_ci 排序规则,而 ci 表示"不区分大小写",同时它把尾随空格视为相等,于是 "13800138000 " 与 "13800138000" 撞了唯一索引。

排序规则(collation)看起来是建表时的一个选项,实际它决定了字符串如何比较、如何排序、以及什么算唯一。这个代价有个具体形态:改排序规则会重建整张表与该列上所有索引,而唯一的"无损"方式是新建列、回填、切换——于是它变成一个项目,而不是一次 DDL。
而它一旦选定,改动的代价远高于"改一个字段"。


1. 排序规则决定三件事

决定 影响
比较是否区分大小写 唯一约束、查询匹配
是否忽略尾随空格 唯一约束、等值查询
字符的排序权重 ORDER BY 结果、索引顺序

第二项最容易被忽略:很多 _ci 规则的 PAD SPACE 语义把尾随空格视为无意义,于是"看起来不同"的两个值在索引层面相同。

第三项影响索引复用:不同排序规则的列无法共用索引,联表时可能退化为全扫。第二项还有个别名:PAD SPACE 与 NO PAD。MySQL 8 的部分规则改为 NO PAD,于是"带空格与不带空格"不再相等——所以同一段代码在升级后行为可能变化。

2. 常见规则的含义

规则 含义
utf8mb4_general_ci 不区分大小写,较简单的权重
utf8mb4_unicode_ci 不区分大小写,更符合 Unicode 排序
utf8mb4_0900_ai_ci MySQL 8 默认,不区分大小写与重音
utf8mb4_bin 按字节比较,完全区分
utf8mb4_general_cs 区分大小写

第四项最"严格":它是唯一能保证"字符串完全相同才相等"的规则,适合做唯一标识(如 token、哈希、邮箱的规范化后形式)。

第五项(_cs)在 MySQL 里并不常见,实际项目更多用 _bin 来表达"严格比较"。所以"区分大小写"这件事,实践上等于"用 _bin"。
第三项的 ai 表示"忽略重音":café 与 cafe 被视为相等。这对多语言输入是便利,对唯一标识是风险。

-- 查看列的实际排序规则(排查的第一步)
SELECT column_name, character_set_name, collation_name
FROM information_schema.columns
WHERE table_schema='shop' AND table_name='users';
-- 典型意外:业务以为区分大小写,而列是 _ci

3. 唯一索引 + _ci 的三种后果

后果 表现
大小写不同的值撞唯一 用户注册被拒
尾随空格撞唯一 无法插入"看起来不同"的值
重音不同的值撞唯一 国际化场景冲突

第一项在"邮箱大小写"上最常见:[email protected] 与 [email protected] 被当成同一个。有些业务希望这样(邮箱通常不区分大小写),有些则不希望(用户名)。

第三项在多语言产品里麻烦:"José" 与 "Jose" 撞唯一,而用户认为这是两个人。

-- 决策:唯一标识到底是"字节相同"还是"语义相同"
-- 方案一:唯一标识用 _bin(严格)
ALTER TABLE users MODIFY email VARCHAR(255) COLLATE utf8mb4_bin;
-- 方案二:唯一标识用"规范化后的值"(更稳)
-- 存 email_normalized(小写+去空格),唯一索引加在它上面

第二项的额外好处是"可审计":规范化规则可以写成代码、写进测试,而排序规则是数据库配置,代码里看不见。所以前者的行为更容易被人理解。
第二项通常更稳:把"规范化"显式化成数据处理步骤,索引只在规范化值上做严格比较。这样"业务语义"与"字节比较"分开,改动可控。

"规范化"这一步有三个常被漏掉的规则,值得写成代码而不是靠记忆:

// 规范化:三类字段,三套规则(显式、可测)
def norm_email(s: str) -> str:
    return s.strip().lower()                      # 邮箱:去空格 + 小写
def norm_username(s: str) -> str:
    return s.strip()                              # 用户名:只去空格,保留大小写
def norm_phone(s: str) -> str:
    return "".join(ch for ch in s if ch.isdigit())  # 手机号:只留数字
// 关键:规范化函数有测试;索引建在规范化列上,排序规则用 _bin

4. 排序规则与索引

场景 问题
联表两列规则不同 索引无法使用
查询用字面量 隐式转换,索引失效
规则变更 需重建索引

第二项很隐蔽:WHERE name = 'X' 中字面量的规则由连接或列决定,两侧不一致时会做转换,从而无法用索引。

第三项的成本:改建排序规则等于重建整张表的索引,大表上是数小时的 DDL。第一项(联表两列规则不同)的表现是"同一个查询,单独跑很快,联表就慢"。因为联表时的比较无法用索引,优化器只能选全扫加哈希。

-- 检查是否因排序规则导致索引失效
EXPLAIN SELECT * FROM users WHERE email = '[email protected]';
-- 关注 key 是否为 NULL;若 NULL 且列有索引 → 可能是规则不一致
SHOW WARNINGS;   -- 有时会提示"排序规则混用,无法使用索引"

还有一个"连接层"的坑:连接串里可以设 collation / characterEncoding,而它会成为字面量的比较规则。于是"同一段 SQL 在不同连接上结果不同",而代码完全一致。

// 连接层影响比较的三个参数(MySQL 示例)
characterEncoding=utf8mb4      → 字符集
connectionCollation=utf8mb4_bin → 字面量的比较规则(关键)
-- 若不设,继承服务端默认,可能与列不一致
// 症状:应用查不到,客户端手工查得到

5. 跨库与跨系统的坑

场景 坑
应用与数据库规则不同 应用层判重与库不一致
分库后规则不同 同一查询不同结果
数据同步到 ES 分词与比较语义不同
导出到文件再导入 默认规则覆盖

第三项(同步到 ES)的差异不只是分词:ES 的 keyword 字段默认区分大小写,而 MySQL 的 _ci 不区分,于是"库里有 1 条、索引里 2 条",而两边各自都"正确"。
第一项最典型:Java 的 equals 区分大小写,而数据库不区分,于是"应用认为不重复、数据库报重复"。这类错误只在真实数据出现大小写混用时暴露。

第三项在搜索场景里常见:数据库按 _ci 判重,ES 按分词后匹配,两侧的"相同"定义不同。

排查这类问题的第一个动作应当固定下来:把两个值都 HEX() 出来对比。因为"看起来一样"经常不一样——尾随空格、零宽字符、全角半角、不同 Unicode 归一化形式,全都在肉眼上不可见。

-- 两个"看起来相同"的值,字节是否真的相同
SELECT HEX('13800138000 '), HEX('13800138000');
-- 前者多一个 20(空格),这就是唯一冲突的原因

-- 找出表里带尾随空格的记录
SELECT id, HEX(email) FROM users WHERE email LIKE '% ';

6. 排错:五种"字符串比较不符预期"

现象 成因 处理方向
大小写不同判重复 _ci 规则 改 _bin 或规范化
带空格判重复 PAD SPACE 规范化去空格
重音不同判重复 ai 规则 改 _bin 或保留重音
查询用不上索引 规则混用 统一规则
应用与库判断不一致 双侧语义不同 显式规范化

第五项是"系统性"问题:两套判重逻辑必然不一致第四项(规则混用)还有一个"跨库"版本:分库后不同分片用了不同规则(因为建表时间不同),于是同一查询在不同分片返回不同结果——这类问题只有在数据量大到跨分片时才出现。
,因为"字符串相等"在两个系统里定义不同。解法是"只保留一套判重"(通常在数据库),应用层不做预判。

7. 边界:什么情况不必关心排序规则

场景 为什么 做法
纯数字/ID 列 无字符串比较 不必
内部临时表 生命周期短 不必
仅用于展示的文本 不参与比较 不必
已统一规范化 比较语义明确 不必

最后一条与"迁移"有关的边界:跨数据库迁移时排序规则几乎必然要重定(MySQL 与 PostgreSQL 的默认不同)。所以"迁移后唯一约束失效/变严"是常见事故,应当在迁移前专门核对每个唯一索引的语义。
判断口径一句话:这个字段会不会参与"相等判断"或"唯一约束"。会,排序规则就是业务规则的一部分;只用于展示,它只是存储细节。


8. 自测

  1. 排序规则决定的三件事分别是什么?为什么"尾随空格"会导致唯一冲突?
  2. utf8mb4_bin 与 utf8mb4_0900_ai_ci 的核心差别是什么?各自适合什么字段?
  3. 为什么"存规范化后的值 + 唯一索引"比"改排序规则"更稳?
  4. 排序规则混用如何导致索引失效?怎么验证?
  5. 为什么"应用层判重 + 数据库判重"必然不一致?解法是什么?
  6. 判断"是否需要关心排序规则"的那个问题是什么?
S08-A17 · 排序规则与比较语义:为什么"大小写不同"算重复
https://www.clxhxhhr.top/posts/6509/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。