子查询未写关联条件会导致笛卡尔积爆炸,如1000×500=50万行中间结果;应改用exists、显式关联外层字段、避免select*、控制子查询粒度并贴合业务逻辑过滤。

子查询没写关联条件,直接爆炸
外层表 1000 行 × 子查询 500 行 = 50 万行中间结果,而你只想要匹配的几十行。这不是“可能慢”,是数据库真会把这 50 万行全算出来再过滤。
常见踩坑点:
-
WHERE x IN (SELECT y FROM t)看似安全,但 MySQL 5.7 及更早版本在子查询返回空、含NULL或外层有多个可匹配字段时,大概率放弃半连接优化,退化为嵌套循环+全表扫描 -
FROM a, (SELECT * FROM b) c这种老式逗号语法,必须在后续WHERE里补全a.id = c.a_id,漏一个就炸 - 用
EXPLAIN看执行计划:rows列显示 500000,但表b实际只有 1000 行?基本就是这问题
IN 改 EXISTS,语义稳、路径稳
WHERE x IN (SELECT y FROM t) 在 MySQL 中容易触发隐式笛卡尔扫描,尤其当子查询结果超 1000 行时,优化器大概率放弃哈希半连接。
换成 EXISTS 不仅语义清晰,MySQL 对它的执行路径选择也更可靠:
SELECT * FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id AND o.status = 'paid');
关键点:
-
EXISTS是“是否存在匹配”,不关心具体值,也不怕NULL - 子查询中必须显式引用外层字段(如
o.user_id = u.id),否则仍是静态集合 - 别写
SELECT *,SELECT 1足够,避免额外字段拖慢物化
LEFT JOIN 套子查询后 COUNT(*) 失真
典型错误写法:SELECT COUNT(*) FROM users u LEFT JOIN (SELECT * FROM orders WHERE status = 'paid') o ON u.id = o.user_id。本意是统计用户数,但结果可能远大于 COUNT(u.id)。
原因有两个:
- 子查询若为空,
COUNT(*)仍返回用户行数(LEFT JOIN保留左表) - 子查询若未去重或未聚合,一个
u.id匹配多条o记录,COUNT(*)就会放大
正确做法:
- 要统计用户总数:用
COUNT(u.id)或COUNT(1),别信COUNT(*)在 JOIN 后的结果 - 要统计“有已支付订单的用户数”:用
COUNT(o.user_id)(自动忽略NULL) - 子查询本身需控制粒度:比如改成
(SELECT DISTINCT user_id FROM orders WHERE status = 'paid')或先GROUP BY user_id
子查询带 GROUP BY 后,别当原表用
一旦子查询用了 GROUP BY、SUM、COUNT,它输出的就不是明细行集合,而是聚合宽表。外层再按 id 直接连,等于强行对齐两个不同结构的数据集。
例如:(SELECT order_id, SUM(amount) FROM order_items GROUP BY order_id) 返回的是每单总金额,没有原始 item_id;你不能拿它和 products 表按 item_id 关联。
安全做法:
- 外层关联必须基于子查询输出的字段,比如用
WHERE o.order_id IN (SELECT order_id FROM ...) - 如果需要明细但只取最新一条,用
ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY created_at DESC)筛,别让引擎自己猜 - MySQL 8.0.14+ 或 PostgreSQL 可考虑
LATERAL,让子查询能实时引用外层变量,避免提前物化整个结果集
真正难的不是写出子查询,而是判断哪张表该聚合、按什么字段聚合、是否要加状态过滤——这些都得贴着业务逻辑抠。一不留神,子查询里漏了个 AND deleted = 0,结果还是错的。











