起因是运营同学反馈在测试环境线上环境配置了一套相同的数据,但该数据在测试环境可以正常显示,线上却无法正常显示。而当时测试环境与线上环境的代码已经同步,初步检查数据后也确实没有发现配置错误或异常日志。

但整个代码筛选过滤条件也不多,一个个看也没发现任何一个地方会导致过滤失效的。这时同事敏锐地发现配置中有一个空项,其值并非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_languagesupport_language = ''' ' = ''LENGTH(support_language)
101

如你所见,support_language字段长度为1,包含一个空格,MySQL认为其等价于'',但在SQL中写下的空格并不认为' '''是一个东西。那么线上环境无法筛出该数据的原因也呼之欲出了,线上环境执行该SQL得到的support_language = ''结果为false,那么这是为什么呢。

两个环境部署的MySQL版本均为8.0.x几乎可以排除由MySQL版本更新带来的策略不同问题。

经过反复的测试和对AI的拷打,同事从一大堆答案中找到了一句可靠的差异:排序差异

在 MySQL 中,字符比较的行为取决于排序规则COLLATE),不同的规则对字符的等价性有不同定义。你提供的两个查询结果差异源于 utf8mb4_general_ciutf8mb4_0900_ai_ci 对空格的权重处理不同。

验证实验SQL:

SELECT
  HEX(WEIGHT_STRING(' ' COLLATE utf8mb4_general_ci)),   -- 返回空(权重被忽略)
  HEX(WEIGHT_STRING(' ' COLLATE utf8mb4_0900_ai_ci));   -- 返回非空(明确权重)

结论似乎可以揭晓了。

最终结论

排序规则权重值 (HEX)说明
utf8mb4_general_ci0020空格的权重被定义为低优先级(直接使用 Unicode 码点 U+0020)
utf8mb4_0900_ai_ci0209空格的权重经过算法生成(符合 Unicode 9.0 标准,更精准)

1. 两个环境support_language = ''不一致的原因
utf8mb4_general_ci 在比较时会自动忽略尾随空格(非精确比较)。如果比较 'a ''a',也会返回 trueutf8mb4_0900_ai_ci严格遵循 Unicode 标准,权重的生成和比较逻辑一致,空格 ' ' 的权重(0209)与空字符串 ''(权重为空)明确不同。

2. 同一环境中,空字段与空格直接比较不一致的原因
那么对于support_language = ''true的测试环境,' ' = ''false,这又是为什么。问题出在字段取值和字面量比较上。

  • 字段比较:当比较字段值(即从表中读取)时,MySQL 会在 collation 的作用下自动去除尾随空格 —— 因此字段值中' ' = ''被视为相等。
  • 字面量比较:写在 SQL 里的两个字符串字面量,字面量比较不走字段比较路径,在实际比较时不会进行这种“尾随空格忽略”处理,因此直接比较' ' = ''返回了false

参考阅读