全角括号藏在半角里:MySQL 说相等,JS 说不认识
某后台管理系统的级联选择器突然出现大批「不在枚举」红色标记。用户打开下拉,肉眼可以清楚地看到对应选项就在列表里,但系统就是死认为没有匹配。复制粘贴、清缓存、换浏览器——全部无效。
症状像是幽灵:数据库里有,前端选项有,就是匹配不上。
现象
前端使用严格相等做孤儿检测:
const knownValues = new Set(options.map(o => o.value))
const isOrphan = Boolean(current) && !knownValues.has(current)
// ^^^^^^^^^^^^^^^^^^^^^^^^
// 返回 true → 标红「不在枚举」options 来自枚举接口,current 来自另一张业务快照表的冗余字段。两边的字符串看上去一模一样,但 knownValues.has(current) 就是 false。
查 MySQL,直接 WHERE name = '信息技术(信息安全)' 能命中枚举表里的那条行;而业务快照里存的是 信息技术(信息安全)——用的全角括号 ()。
根因
中文输入法(IME)在中文输入状态下会把 ( 打成 (,在英文状态下则是 (。两个人在不同时间分别录入了「同一条」数据,一个用半角,一个用全角,外观几乎无差异,却是完全不同的 Unicode 码位:
| 字符 | 码位 | UTF-8 字节 |
|------|------|-----------|
| ( | U+0028 | 28 |
| ( | U+FF08 | EF BC 88 |
| ) | U+0029 | 29 |
| ) | U+FF09 | EF BC 89 |
JS / Python / Go 的字符串比较是字节严格的,( 和 ( 绝对不等。
然而 MySQL 的默认排序规则 utf8mb4_0900_ai_ci(MySQL 8.0 起的默认值)是宽度无感知(width-insensitive)的——这是 Unicode CLDR 规范的一部分。所以 MySQL 里 ( 和 ( 在 = 和 LIKE 运算下相等,数据库管理工具和终端都正常显示,看不出差异。
用 WHERE name LIKE '%(%' 来定位半角括号行,MySQL 会同时把含全角 ( 的行也返回进来(因为排序规则宽度无感知),让你以为范围更大、问题更复杂。
实际上这条 LIKE 查出了 97 行,真正含半角括号的只有 8 行。必须加 BINARY 前缀才能区分:
-- 只返回真正含半角括号的行(8 行)
SELECT id, name FROM majors
WHERE name LIKE BINARY '%(%' OR name LIKE BINARY '%)%';正解
第一步:用 HEX() 确认根因,不要相信肉眼
-- 枚举表(SSoT)
SELECT id, name, HEX(name) FROM majors
WHERE name LIKE '%信息技术%';
-- 快照表(消费方)
SELECT title_major, HEX(title_major) FROM snapshot_table
WHERE title_major LIKE '%信息技术%';对比字节。枚举表里是 ...28...29...(半角),快照表里是 ...EFBC88...EFBC89...(全角)。根因确认。
第二步:决定哪一边是 SSoT,修异常那边的源头数据
不要在前端或服务层加正则替换——那只是掩盖问题,每个新增点、每次导出都会继续混入错误形式。
统计两边各有多少行用哪种形式,以多数为准。这里全角是正确的中文排版惯例,把枚举表里那 8 行半角括号统一成全角:
UPDATE majors
SET name = REPLACE(REPLACE(name, '(', '('), ')', ')')
WHERE name LIKE BINARY '%(%' OR name LIKE BINARY '%)%';REPLACE 是字节严格的,传入半角 ( 不会误改全角 (。
第三步:用 BINARY 验证清零
-- 必须返回 0
SELECT COUNT(*) FROM majors
WHERE (name LIKE BINARY '%(%' OR name LIKE BINARY '%)%')
AND is_deleted = 0;然后在 UI 里刷新页面——不需要任何代码部署,「不在枚举」标记全部消失。
一次 SQL UPDATE 统一 SSoT 的存储形式,JS 的严格 === 逻辑本来就是正确的,出问题的是数据。前端/后端代码零改动,所有语言(JS、Python、Java)的比较自动对齐。
一句话外卖
MySQL 的默认排序规则会把全角和半角当同一字符,诊断时永远加
LIKE BINARY;「看起来一样但比较不等」的中文字符串,先查字节(HEX()),再修数据源头,不要在消费层加正则兜底。