SuperAPI AI 工具集

sql-not-in-nullable-trap

编程与开发

查看项目

更新于 10-17 16:16

技能介绍

该条目关注 SQL 中 NOT IN 与可空子查询配合时的常见问题:语句可能永远匹配不到任何行,却不会抛出异常或记录错误,对 DELETE 这类操作来说,影响行数为零也可能是正常结果,因此这类失败没有任何可见症状。条目记录的一个示例是:一条清理播放列表的语句长期删除 0 行且未报错,原因是它过滤的列可为 NULL,且大部分值为 NULL。

原因

x NOT IN (a, b, c) 等价于 NOT (x = a OR x = b OR x = c)。任何值与 NULL 比较,结果都是 NULL,而不是 false。于是当列表中含 NULL 时:若 x 在列表中,TRUE OR NULL 得到 TRUE,取反为 FALSE,不会匹配(正确);若 x 不在列表中,FALSE OR NULL 得到 NULL,取反仍为 NULL,也不会匹配(错误)。本应排除的行仍被排除,本应匹配的行永远不会匹配,最终结果为零匹配,且不会出现明显错误或部分正确的结果。

陷阱

子查询中只要出现一个 NULL,整个语句都会受到影响,这里指所有外层行都不再匹配,并非只有包含 NULL 的行表现异常;即使某列有 99% 的值非空,也同样会导致该问题。IN 不受此影响,NOT IN 会受影响,这使得问题容易通过评审:正向 IN 在真正匹配时仍返回 TRUE,同一个 NULL 在其中无害;评审者曾多次用 IN 查询该列,因此没有理由怀疑。可空性是模式事实,并非数据事实,“目前生产环境没有 NULL”不是保障,下次插入 NULL 时就会出问题。条目建议根据模式做防护,并加注释说明为何未加防护;否则后续读者会以为遗漏。

规避

优先用 NOT EXISTS:它逐行关联比较,子查询中某行对应列为 NULL 时,该行自身比较失败并被忽略。示例:DELETE FROM playlist AS p WHERE NOT EXISTS (SELECT 1 FROM local_playlist AS l WHERE l.remotePlaylistId = p.id)。条目提醒,部分引擎(包括 SQLite)不接受不带 AS 的 DELETE FROM playlist p,需用 AS 别名形式。LEFT JOIN … WHERE joined.col IS NULL 同样对 NULL 安全。建议新代码优先用 NOT EXISTS,仅在列可证明为 NOT NULL 且列表较小时保留 NOT IN。条目还指出,该问题的本质是形状问题,并非 SQL 的怪癖:“永远不成立的条件 + 没有成功信号的代码路径”也适用于只为两条触发路径之一添加防护,或把平台实现留成空操作。任何发布后没有可观察差异的改动,在使其可观察之前都应视为未验证。

验证

相信 NOT IN 前应做三件事:(1)统计子查询列的 NULL 数量:SELECT COUNT(*) FROM local_playlist WHERE remotePlaylistId IS NULL; 结果大于零意味着当前所有针对该列的 NOT IN 都匹配不到任何行;(2)用同一数据分别执行 NOT IN 和等价的 NOT EXISTS,若计数不同即发现问题,相同则说明该列当前确实干净;(3)让语句报告结果:DAO 方法返回影响行数并记录日志,将各阶段影响行数全为零的清理视为失败信号,而不是“无事可做”,这是模式变化后仍有效的检测手段。