
去年排查线上对账任务时我遇到过一个非常典型的幽灵问题某张核心表里明明有数据SQL 结果集却是空的。没有任何报错不超时也没有慢查询记录日志里干干净净。把 EXPLAIN 拉出来Extra 列躺着一行英文Impossible WHERE noticed after reading const tables。当时第一反应是这什么诡异提示翻了半天资料才明白这是 MySQL 优化器在替我做逻辑校验——它用查询本身的条件互相推导发现这组条件根本不可能同时成立于是提前宣布空结果。熟悉 MySQL 的人都知道EXPLAIN 的 Extra 列经常出现各种英文短语但这一句的迷惑性特别强。它不是报错不是警告而是优化器在优化阶段做的一道逻辑推理题结论。如果你能在几秒钟内读懂它不仅会少踩很多查出来是空的坑还能把它变成排查 SQL 逻辑错误的高效工具。下面我先把它出现的时机和含义讲清楚再带大家用一组实验完整复现各种触发场景。1. 这一行英文不是报错是优化器在替你验算1.1 三条相似的 Extra 信息别搞混在 EXPLAIN 的 Extra 列里跟不可能相关的信息主要有三条。很多文章把它们混着讲实际排查时容易误判方向所以先把区别列出来Extra 信息触发时机真实含义Impossible WHEREWHERE 条件分析阶段WHERE 子句本身恒为 FALSE不需要读表就能判定Impossible WHERE noticed after reading const tables常量表读取之后通过主键/唯一索引读到一行真实数据再用这行值核对剩余条件时发现不成立no matching row in const table常量表查找过程按等值条件去主键/唯一索引里找结果一行都不存在第一条处理的是纯逻辑矛盾。比如WHERE id 1 AND id 2主键不可能同时等于两个不同的数优化器根本不需要碰表直接在条件分析阶段就把查询短路了。第三条是查无此行等于拿着一个不存在的 id 去索引里找连行都没读到自然谈不上后续判断。中间这条——本文的主角——比另外两条多了一步after reading const tables优化器确实在主键索引里找到了那一行但接下来用这行的真实列值去核对剩余条件发现对不上于是断言整个查询永远返回空。注意一个细节出现这条信息时EXPLAIN 里的访问类型通常是const它不代表只扫了一行而是代表连这一行之后的整个执行过程都不用跑了。1.2 const tables到底指什么常量表const tables是 MySQL 优化器的一种特殊表访问类型。当事务满足两个条件时会被归为常量表第一最多只能匹配一行第二这一行是通过主键或唯一索引的全部列、用等值条件定位到的。典型的例子就是WHERE id 1且id是主键。关键点是优化器不会等到执行阶段才去取这行而是在生成执行计划的过程中就先做了真实的索引点查把读到的行存起来。读进来之后这一行的所有列值——name、status、email 等等——都变成了可供推导的已知常量。接下来执行器对剩余条件的判断全部拿这组常量去套。WHERE 里只要存在和这些常量冲突的条件就会当场被判定为不可能成立。打个比方这就像你去找人之前先翻了一下档案室档案上写着该员工已离职那你就不用再跑去工位找了。MySQL 的档案室就是主键索引和唯一索引而after reading const tables这段话就是在说档案翻完了结论是不用找了。2. 一张表、六条 SQL完整复现这条信息的出现场景光看文字描述不够直观我准备了一张测试表把各种触发场景跑一遍。下面这些 SQL 在 MySQL 5.7 和 8.0 的主流版本上输出基本一致大家可以自己在本地实测。2.1 测试表与数据CREATE TABLE t_user ( id INT PRIMARY KEY AUTO_INCREMENT, email VARCHAR(100) NOT NULL, status TINYINT NOT NULL DEFAULT 1, name VARCHAR(50), UNIQUE KEY uk_email (email) ) ENGINEInnoDB; INSERT INTO t_user (id, email, status, name) VALUES (1, aliceexample.com, 1, Alice), (2, bobexample.com, 2, Bob), (3, carolexample.com, 1, Carol);数据就三行其中id1这行的status是 1。后面的所有场景都围绕这几行展开。2.2 场景一主键上写了两条互斥的等值条件EXPLAIN SELECT * FROM t_user WHERE id 1 AND id 2;主键不可能同时等于 1 又等于 2优化器在条件分析阶段就能得出结论。这条 SQL 的 EXPLAIN 结果里会出现Impossible WHERE而且通常连表结构都不需要深入访问。这个场景说明只要 WHERE 里对同一列出现互斥的等值条件MySQL 不需要任何行数据就能证明结果为空。2.3 场景二读到常量行却栽在剩余条件上EXPLAIN SELECT * FROM t_user WHERE id 1 AND status 99;这才是本文主角的典型出场方式。id1这一行的真实status是 1而查询要求status99。优化器在优化阶段先通过主键读到id1这行把status确定为 1然后拿这个 1 去核对status99发现等式永远不成立。EXPLAIN 结果里访问类型是constExtra 显示Impossible WHERE noticed after reading const tables。这里最容易误解的地方是你看着这条 SQL 只觉得它查不到数据但优化器的角度是它已经被证明不可能查到数据——这两个结论在性能上差着一个量级。2.4 场景三唯一索引也可以当常量表入口EXPLAIN SELECT * FROM t_user WHERE email aliceexample.com AND status 99;email上有唯一索引uk_email同样满足常量表的判定规则。优化器顺着唯一索引找到 alice 这行读到的status是 1再和status99比较结论依然是Impossible WHERE noticed after reading const tables。这里要专门注意常量表的入口不一定是主键唯一索引的全部列只要都被等值条件覆盖同样可以成为常量表。所以生产环境里很多走唯一键比如业务流水号、手机号的查询都会触发这条信息。2.5 场景四等值查询指向了不存在的行EXPLAIN SELECT * FROM t_user WHERE id 999;这张表里没有id999的行。常量表判定成立但实际查找空手而归Extra 列显示的是no matching row in const table。它和Impossible WHERE noticed...完全是两码事这里根本没读到行所以谈不上用真实值去核对条件仅仅是你要找的对象不存在。排查时如果把这两种情况混在一起很容易把数据真的缺失误判成查询逻辑错误方向就完全反了。2.6 场景五JOIN 条件推导出另一张表的矛盾EXPLAIN SELECT * FROM t_user u1 JOIN t_user u2 ON u2.id u1.id 1 WHERE u1.id 1 AND u2.id 5;u1是常量表id1。读取以后连接条件u2.id u1.id 1会被替换成u2.id 2。此时再和 WHERE 里的u2.id 5放在一起优化器发现u2的 id 不可能同时等于 2 和 5于是整个 JOIN 被判定为空。这个场景说明常量表机制不只影响单表查询。在 JOIN 推导里先读出来的常量值会被代入连接条件进而推导出其它表上的等值约束最终暴露出 WHERE 条件里的矛盾。这类问题在多表关联的报表 SQL 里尤其隐蔽因为单看每个表各自的条件都没问题放到一起才冲突。2.7 场景六没有常量表范围索引同样能证明不可能不是只有常量表才会触发这类消息。对带了索引的普通表范围优化器会计算 WHERE 里各项条件在索引上的区间交集如果两个区间完全没有重叠也会判定为Impossible WHERE。比如建一张带时间索引的表CREATE TABLE t_order ( id INT PRIMARY KEY, created_at DATETIME NOT NULL, KEY idx_created (created_at) ); EXPLAIN SELECT * FROM t_order WHERE created_at 2024-01-01 AND created_at 2023-01-01;created_at既要大于 2024 年 1 月 1 日又要小于 2023 年 1 月 1 日两个时间区间交集为空Extra 列同样会出现Impossible WHERE。这种情况不需要常量表索引的范围检查就能证明。所以看到短版本Impossible WHERE时可以把注意力放在两个条件的区间是否有交集上看到带const tables的长版本时则把注意力放在常量行真实值是否满足剩余条件上。排查方向完全不同。3. 优化器为什么敢在优化阶段做这种断言3.1 进入 const table 的三条硬性规则不是所有表都能被当成常量表。MySQL 官方文档里的判定规则总结下来是三条表最多只能有一行满足条件。如果可能匹配多行比如WHERE id 1就不可能是常量表。访问路径必须是主键或唯一索引的全部列并且所有列都用等号与常量比较。比如uk_email(email)只有一个列那么WHERE emailaliceexample.com就满足如果是联合唯一索引(a, b)则必须同时给出a常量 AND b常量。表本身的物理行数是 0 或 1 的时候也算常量表EXPLAIN 里对应system访问类型。如果等值条件缺失了唯一索引的任何一个列或者使用了范围条件、、BETWEEN就不满足常量表要求优化器会退化到其它访问路径也就不会出现const类型和后续的 const tables 判定。这也是为什么实践中经常有同学疑惑我明明查的是唯一键怎么 EXPLAIN 不是 const——多半是条件里带了范围或者唯一索引是多列但只给了部分列。3.2 常量替换把 WHERE 变成了一道算术题从代码路径来看优化器大致做这么几步先把 WHERE 做扁平化处理flatten把嵌套的 AND/OR 拆成最简形式同时做常量折叠。对每个符合条件的常量表执行一次真实的索引点查把结果行存入常量行数组。把常量行里的列值代入 WHERE 的剩余条件、JOIN 条件甚至 SELECT 列表。如果代入后某个条件被证明恒为 FALSE优化器就把整个查询标记为 impossible直接短路成返回 0 行。比如WHERE id1 AND status99在优化器眼中等价于先查到id1的行发现status1然后判断199发现是假命题于是不再安排任何扫描计划。整个过程完全发生在优化阶段执行器根本不用上场所以性能代价几乎为零。第 3 步里代入 JOIN 条件的能力是很多深层优化的基石。它不只会揭示 WHERE 内部的矛盾还会把常量值传播到其它表上缩小其它表的范围条件产生的收益远不止提前发现空结果这一件事。3.3 省掉一次扫描只是表面收益有人觉得反正结果也是空扫描就扫描呗能慢到哪去实际上这个判定的收益非常大。省掉的不是一次回表而是整个执行计划的构建和执行MySQL 在优化阶段就短路了执行器拿到一个空计划直接返回。在 JOIN 场景里它可能省掉一长串驱动表探测和嵌套循环在分区表上配合分区裁剪可以避开扫描所有分区。提示在慢查询日志里看到某条 SQL它对应的查询计划却带着Impossible WHERE这种情况通常不是慢查询而是秒回的空结果。真正需要警惕的是条件矛盾但优化器没发现的情况——比如条件被函数包裹或者矛盾藏在 OR 分支内部。这两种情况在第 4 节会详细讲。4. 排查实践中真正值钱的三件事4.1 区分没读到行与行对不上条件对业务逻辑正确性来说区分no matching row in const table和Impossible WHERE noticed after reading const tables非常有用。两者的含义不同排查方向自然也完全不同no matching row in const table很可能是数据没写入、被删了或者上游传过来的 ID 错位。这时候应该去查数据本身。Impossible WHERE noticed after reading const tables行确实存在但 WHERE 里有个条件和这行的真实值矛盾。这时候应该先查这行的真实值再回代码里找是哪个过滤条件写错了。我的第一步永远是执行一条去掉剩余条件的 SQL把常量行捞出来看真实值。比如把WHERE id1 AND status99改成SELECT * FROM t_user WHERE id1看一眼status到底是多少。这一步能在一分钟内确定问题方向比盯着复杂的 EXPLAIN 输出琢磨半天高效得多。另一个实用场景是 UPDATE/DELETE。MySQL 的 EXPLAIN 同样支持EXPLAIN UPDATE和EXPLAIN DELETE矛盾的 WHERE 会在计划阶段直接暴露出来结果是 0 行受影响。对批量清理类任务来说先在测试环境把 EXPLAIN 跑一遍比在生产上执行后再看 affected rows 安全得多。4.2 NULL、OR、函数包裹是三个漏网之鱼这条消息虽然强大但它的推理能力有边界。以下三种情况不会触发Impossible WHERE结果却依然是空排查时最容易绕路WHERE id NULL不会触发。等号加 NULL 得到的是 NULL未知MySQL 不会把未知直接折叠成 FALSE所以优化器不会在计划阶段断言空结果。执行阶段所有行都会被过滤掉结果依然是空。正确写法永远是IS NULL这条属于新手高频错误。OR 分支内部的矛盾经常逃过检测。比如WHERE (id 1 AND id 2) OR status 1优化器可能把括号里的矛盾分支简化掉而不会把整个查询标记为 impossible。从结果看确实能查出status1的行但括号里那半截逻辑已经废了属于典型的死代码。函数包裹条件会阻断常量传播。比如WHERE id 1 AND status ABS(-99)虽然ABS(-99)最终是常量 99但函数计算未必在优化阶段完成优化器不会拿它跟常量行做矛盾判定。这三个漏网之鱼反过来正是实际项目里最常见的坑结果明明为空逻辑看起来也说得通但不出现这条提示于是排查看不到抓手。下一次遇到空结果先别急着怀疑这条提示没生效而是自查是不是踩了这三个雷。4.3 把它变成 CI 里的一道自动校验大多数 SQL 逻辑错误都是在数据齐全之后、返回空结果时才暴露往往要等到联调甚至上线。其实可以在测试环境提前加一道检查对核心查询跑 EXPLAIN如果 Extra 里出现Impossible WHERE字样直接让流水线失败。做法很简单准备一个 SQL 文件把每条要保护的核心查询前面都加上EXPLAIN然后在 CI 脚本里判断输出mysql -h127.0.0.1 -utest -p*** db_test explain_queries.sql \ | grep -E Impossible WHERE|no matching rowgrep 到任何一条说明代码里出现了一对不可能同时成立的条件。这种 bug 越早暴露越好等数据量大了再靠人工发现成本高得多。我在两个项目中用过这个做法都抓到过真实的逻辑冲突。5. 版本差异、EXPLAIN ANALYZE 与我的排查套路5.1 5.7、8.0 的差异和 JSON 解析要点这条 Extra 信息在 MySQL 5.7 和 8.0 上都能看到语义基本一致。细微差别主要在于8.0 对常量替换和范围推导的代码路径做过不少重构某些在 5.7 里直接判定为Impossible WHERE的情况在 8.0 里可能变成常量表读取后判定或者反过来另外 8.0 新增的EXPLAIN FORMATTREE和EXPLAIN ANALYZE展示方式完全不同。如果写脚本做 CI 检查不建议直接对文本 grep因为消息文本在不同版本可能有措辞差异。更稳的做法是用EXPLAIN FORMATJSON解析输出中的 message 字段mysql -N -e EXPLAIN FORMATJSON SELECT ... \ | jq .. | objects | .message? // empty用 jq 的递归下降扫描把所有 message 字段捞出来再在里面搜 impossible 关键字。这样不管 message 挂在哪个节点下都能抓到比固定字段路径健壮得多。5.2 EXPLAIN ANALYZE 把矛盾过程演给你看EXPLAIN ANALYZE是 MySQL 8.0.18 引入的分析工具它会真实执行查询并打印每步耗时。对本文这种情况EXPLAIN ANALYZE 不会保留Impossible WHERE noticed...字样因为查询已经执行完毕结果是 0 行耗时也接近 0。但它会以另一种形式把过程暴露出来输出大致是这样的- Filter: ((t_user.status 99) and (t_user.id 1)) (cost... rows1) (actual time... rows0) - Index lookup on t_user using PRIMARY (id1) (cost... rows1) (actual time... rows1)注意看内层Index lookup返回rows1外层Filter返回rows0。这就把行存在但条件不满足的完整链条演出来了索引找到了这一行但 Filter 把它拦在了外面。两种工具有各自的使用场景想快速判断结果是否为空用传统 EXPLAIN 的 Extra 列最直观想看清楚每一行数据如何被过滤、卡在哪一步用 EXPLAIN ANALYZE 更有效。5.3 五步排查法把前面这些内容收敛一下我总结了一套五步排查法遇到Impossible WHERE相关消息时按顺序执行跑 EXPLAIN先看 Extra 列有没有Impossible字样。区分长版本带const tables和短版本纯Impossible WHERE。如果是长版本执行去掉剩余条件后的 SQL把常量行捞出来看真实列值。把 WHERE 条件逐条列成 AND 列表找出互相矛盾的两个条件或与常量行真实值冲突的条件。修正后重新 EXPLAIN确认 Extra 列恢复正常再实际执行验证。第五步看起来多余实际很必要。优化器的判定依赖当前的常量表内容和索引结构修改条件后有可能引入新的执行计划问题所以修完再看一眼 EXPLAIN应该成为习惯动作。最后再分享一个个人习惯。现在我写完一条比较复杂的查询不急着执行先顺手在前面加个 EXPLAIN 扫一眼 Extra 列。这个动作看似多余但它的价值不在于看索引用没用上而在于让优化器先替我做一轮逻辑校验。有一次同事的定时任务一直报待处理数据为空我看了下他的 SQLEXPLAIN 直接给出Impossible WHERE noticed after reading const tables。原因很搞笑代码里把已处理和未处理两个状态参数拼到了同一个查询里。优化器比业务代码更快发现了这个自相矛盾。从那以后我对这行看似晦涩的英文提示就多了几分敬意——它不是麻烦而是 MySQL 免费送你的逻辑检查器。