返回博客

全角括号藏在半角里: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 运算下相等,数据库管理工具和终端都正常显示,看不出差异。

坑:用 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()),再修数据源头,不要在消费层加正则兜底。