
【MySQL】一次SQL空格引发的“灵异事件”的排查解决
起因是运营同学反馈在测试环境和线上环境配置了一套相同的数据,但该数据在测试环境可以正常显示,线上却无法正常显示。而当时测试环境与线上环境的代码已经同步,初步检查数据后也确实没有发现配置错误或异常日志。
但整个代码筛选过滤条件也不多,一个个看也没发现任何一个地方会导致过滤失效的。这时同事敏锐地发现配置中有一个空项,其值并非NULL
,而是空格,这在数据库开发中算常见的错误了。于是尝试直接在线上环境将其置为NULL后查看接口返回数据,正常了。
问题就这么简单吗?
首先要强调,将空值统一定义为NULL或空数据结构是必要且科学的,可以避免上述情况的出现。但是,问题就在但是后,测试环境同样是空格,对于同一套代码,为什么测试环境可见?
抛开宇宙射线爆发导致比特翻转、玄学力量导致机魂不悦等重大因素后,疑点落到了SQL查询上。DEBUG发现,这套SQL在测试环境和正式环境返回了不同的结果!
在测试环境下执行以下SQL:
SELECT '' = ' '; -- 返回 0 (false)
select support_language, support_language = '' , ' '='', LENGTH(support_language) from mind_craft_characters where id=57; -- 返回值见下表
-- 补充嫌疑字段的DDL定义
`support_language` varchar(255) DEFAULT '' COMMENT '支持语言 [''zh'',''ja'',''en''],空表示都支持'
support_language | support_language = '' | ' ' = '' | LENGTH(support_language) |
---|---|---|---|
1 | 0 | 1 |
如你所见,support_language
字段长度为1,包含一个空格,MySQL认为其等价于''
,但在SQL中写下的空格并不认为' '
和''
是一个东西。那么线上环境无法筛出该数据的原因也呼之欲出了,线上环境执行该SQL得到的support_language = ''
结果为false
,那么这是为什么呢。
两个环境部署的MySQL版本均为8.0.x
几乎可以排除由MySQL版本更新带来的策略不同问题。
经过反复的测试和对AI的拷打,同事从一大堆答案中找到了一句可靠的差异:排序差异。
在 MySQL 中,字符比较的行为取决于排序规则(
COLLATE
),不同的规则对字符的等价性有不同定义。你提供的两个查询结果差异源于 utf8mb4_general_ci
和 utf8mb4_0900_ai_ci
对空格的权重处理不同。
验证实验SQL:
SELECT
HEX(WEIGHT_STRING(' ' COLLATE utf8mb4_general_ci)), -- 返回空(权重被忽略)
HEX(WEIGHT_STRING(' ' COLLATE utf8mb4_0900_ai_ci)); -- 返回非空(明确权重)
结论似乎可以揭晓了。
最终结论
排序规则 | 权重值 (HEX) | 说明 |
---|---|---|
utf8mb4_general_ci | 0020 | 空格的权重被定义为低优先级(直接使用 Unicode 码点 U+0020) |
utf8mb4_0900_ai_ci | 0209 | 空格的权重经过算法生成(符合 Unicode 9.0 标准,更精准) |
1. 两个环境support_language = ''
不一致的原因
utf8mb4_general_ci
在比较时会自动忽略尾随空格(非精确比较)。如果比较 'a '
和 'a'
,也会返回 true
。utf8mb4_0900_ai_ci
严格遵循 Unicode 标准,权重的生成和比较逻辑一致,空格 ' '
的权重(0209
)与空字符串 ''
(权重为空)明确不同。
2. 同一环境中,空字段与空格直接比较不一致的原因
那么对于support_language = ''
为true
的测试环境,' ' = ''
为false
,这又是为什么。问题出在字段取值和字面量比较上。
- 字段比较:当比较字段值(即从表中读取)时,MySQL 会在 collation 的作用下自动去除尾随空格 —— 因此字段值中
' ' = ''
被视为相等。 - 字面量比较:写在 SQL 里的两个字符串字面量,字面量比较不走字段比较路径,在实际比较时不会进行这种“尾随空格忽略”处理,因此直接比较
' ' = ''
返回了false
。