非等值连接在子查询中易被误判为相关子查询,导致N×M复杂度;根本原因是外层字段引用使优化器无法物化或下推,须配合等值驱动字段和正确索引顺序(如user_id在前)才能高效执行。

非等值连接在子查询里不是“难优化”,而是容易被误用成 correlated subquery,导致执行计划退化为逐行调用——每查外层一行,就完整跑一遍内层查询,复杂度直接变成 N × M。
为什么子查询里的非等值 JOIN 会触发 correlated 执行
MySQL 对子查询的处理有天然限制:只要子查询引用了外层表字段(比如 WHERE l2.user_id = u.id AND l2.created_at > u.last_login),它就被判定为相关子查询。此时优化器无法提前物化结果,也不能把连接下推到存储引擎层做索引合并扫描。
- 即使内层表有
(user_id, created_at)联合索引,也只对单次子查询有效;外层 10 万行用户,就要重复走 10 万次索引查找+范围扫描 -
LATERAL是个例外,但仅限 MySQL 8.0.14+,且要求驱动条件必须是等值(如l2.user_id = u.id),否则索引依然失效 - 用
IN或NOT IN包裹非等值逻辑(如id IN (SELECT id FROM t WHERE ts > ?))会强制生成临时表,若结果集超tmp_table_size还会落盘
EXISTS 替代后为什么还不快
很多人改用 EXISTS 就以为能走索引,结果发现 EXPLAIN 仍显示 type=ALL 或 Using join buffer。根本原因是:EXISTS 的性能取决于能否把外层字段“下推”进内层,并命中索引最左前缀。
- 如果内层写成
SELECT 1 FROM logins l WHERE l.user_id = u.id AND l.created_at > u.registered_at,而索引是(created_at, user_id)—— 等于白建,因为范围字段created_at在前,user_id的等值过滤无法利用索引 - 正确索引必须是
(user_id, created_at),且查询中user_id必须参与等值匹配(不能是IS NOT NULL或函数包装) - 若外层
u.registered_at是 NULL 常见值,优化器可能干脆放弃该索引,转而全表扫内层
真正有效的绕过方式:预计算 + JOIN
当业务逻辑明确需要“每个用户最近一次满足某时间条件的登录记录”,硬扛非等值子查询不如拆成两步:先用窗口函数或派生表拿到候选集,再用等值 JOIN 关联。
- 先建物化中间结果:
SELECT user_id, ip, created_at FROM ( SELECT user_id, ip, created_at, ROW_NUMBER() OVER (PARTITION BY user_id ORDER BY created_at DESC) rn FROM logins WHERE created_at > DATE_SUB(NOW(), INTERVAL 30 DAY) ) ranked WHERE rn = 1
- 再和
users表等值 JOIN:JOIN ... ON u.id = l.user_id,此时可走user_id索引,完全规避非等值连接 - 注意:窗口函数需 MySQL 8.0+;若版本低,可用
GROUP BY user_id配合MAX(created_at),再二次关联取完整行
最易被忽略的一点:非等值条件本身不致命,致命的是它出现在子查询的 WHERE 中,又没配对的等值驱动字段来锚定索引查找起点。一旦漏掉这个前提,建再多索引、换再多样式,都只是反复验证“为什么又慢了”。











