看到执行计划里 nested loop (join filter: true) 就该立刻停止查询,说明数据库正对左表每行扫描右表全量,已产生爆炸性中间结果;应优先用 count(*) over (partition by) 验证膨胀程度,再检查 on 条件是否缺失、恒真或歧义。

看到执行计划里 Nested Loop (Join Filter: true) 就该立刻停
PostgreSQL 中只要 Nested Loop 节点下出现 Join Filter: true 或 Rows Removed by Join Filter: 0,说明数据库正在对左表每行做右表全量扫描——这不是慢,是已经在生成爆炸性中间结果。此时 CANCEL 查询比等它超时更安全,尤其当 actual rows 已比左表行数高两个数量级时。
用 COUNT(*) OVER 快速验证膨胀程度,不跑 SELECT *
别等全量结果返回,先加窗口函数看单主键拉出多少行:
SELECT u.id, COUNT(*) OVER (PARTITION BY u.id) AS cnt FROM users u LEFT JOIN orders o ON u.id = o.user_id;
如果最大 cnt 是几百上千,而业务预期是 0–5,基本确认连接失控。这个查询不锁表、不改逻辑,线上可秒级执行;ORDER BY cnt DESC LIMIT 5 能直接定位问题用户ID。
临时加 LIMIT 100 不是为取数据,是为确认是否已失控
LIMIT 在这种场景下不是优化手段,而是诊断开关:
- 如果
SELECT * FROM a JOIN b ON ... LIMIT 100仍卡住或返回大量重复a.id,说明中间结果早已失控 - 如果前 100 行里就有同一
a.id出现 20 次,不用查全表,连接逻辑已经错了 - 注意:MySQL 8.0+ 和 PostgreSQL 支持
LIMIT在子查询中生效,但老版本可能仍物化全部结果,慎用
别依赖 WHERE 过滤来“救” LEFT JOIN
写成 LEFT JOIN b ON a.id = b.a_id WHERE b.status = 'active' 等价于 INNER JOIN,且先生成全部 NULL 行再过滤——中间膨胀照旧。正确做法是把条件塞进 ON:
LEFT JOIN b ON a.id = b.a_id AND b.status = 'active'
同样,避免在 ON 里写恒真式,比如 ON u.id = u.id 或 ON o.order_id = order_id(缺别名),这类语句语法合法但实际失效。
真正危险的不是查得慢,是查得“看起来还行”——比如只返回 100 行但耗时 3 秒,背后可能已构造了 200 万行中间集再被裁剪。盯紧执行计划里的 rows 和 actual rows,而不是结果集大小。











