EXPLAIN中rows值远超实际数据量是笛卡尔积的铁证,主因是子查询与外层表缺少关联约束,常见于漏写JOIN条件、IN子查询退化、GROUP BY未建索引及EF Core多级Include等场景。

EXPLAIN 中 rows 值爆炸就是笛卡尔积的铁证
执行计划里某张表的 rows 值远超它真实的数据量,比如表 orders 实际只有 2000 行,EXPLAIN 却显示 rows=1800000,这基本等于告诉你:数据库正在做全量组合。根本原因不是数据多,而是没拦住——子查询和外层表之间缺少可下推的关联约束。
常见诱因包括:
- 显式
JOIN忘写ON条件,或老式逗号语法(FROM a, b)漏掉WHERE a.id = b.a_id - 子查询带
GROUP BY或聚合后,外层仍试图用原表主键直接JOIN,而没改用WHERE x IN (SELECT y ...)或EXISTS -
LEFT JOIN套了一个未加过滤的子查询,导致中间结果集被提前物化成静态集合,再跟外层逐行配对
MySQL 的 IN (subquery) 在 5.7 及更早版本里很危险
WHERE id IN (SELECT user_id FROM logs WHERE status = 'error') 看起来干净,但在 MySQL 5.7 及之前,只要子查询返回空、含 NULL、或外层有多个可匹配字段,优化器大概率放弃半连接(semi-join),退化为嵌套循环 + 全表扫描——效果等同于手动制造笛卡尔积。
更稳的做法是:
- 换成
WHERE EXISTS (SELECT 1 FROM logs WHERE logs.user_id = outer.id AND logs.status = 'error') - 子查询内部必须把过滤条件(如
status = 'error')写进去,不能留到外层再筛 - 避免
SELECT *,只查SELECT DISTINCT user_id;否则某些版本会多加一层临时表,进一步放大中间结果
子查询里 GROUP BY 后再 JOIN,主键对齐就失效了
假设你写了 LEFT JOIN (SELECT order_id, COUNT(*) cnt FROM items GROUP BY order_id) i ON o.id = i.order_id,本意是补上每单的项数。但若 items 表中没有 order_id 索引,或者 GROUP BY 前没去重,i.order_id 就可能重复出现——导致一条订单匹配多行,COUNT(*) 失真,rows 暴涨。
关键检查点:
- 子查询中的
GROUP BY字段是否已建索引?没索引的话,ON条件无法驱动高效查找 - 是否误把
SELECT *当成“安全默认”?实际它会让优化器无法判断输出列唯一性,被迫加临时表 - 统计主表独立行数时,别依赖
COUNT(*),改用COUNT(o.id);要统计“有明细的订单数”,用COUNT(i.order_id)(自动忽略 NULL)
EF Core 的 Include 多级导航本质也是笛卡尔积温床
在 EF Core 中写 context.Orders.Include(o => o.Customer).Include(o => o.Items).ThenInclude(i => i.Product),生成的 SQL 是单条多 JOIN 查询。一旦一个订单有 5 个商品、每个商品又属于不同供应商,主表字段(如 Order.Id)就会在结果集中重复 5 次甚至更多——数据库层面就是笛卡尔积,只是 ORM 隐藏了膨胀过程。
这不是语法错误,而是设计限制:
- 深嵌套 + 一对多关系 = 必然重复主实体数据
- 内存里还要去重合并对象,CPU 和 GC 压力同步上升
- 真正解法不是调优 SQL,而是拆成多个独立查询(如先查订单,再查 Customer IDs 批量查客户,再查 Items IDs 批量查明细)
最常被忽略的一点:笛卡尔积未必报错,它往往静默发生——查询跑得慢、内存 OOM、监控里 rows 异常高,但日志里连个警告都没有。靠 EXPLAIN 主动盯 rows 和 Extra 列,比等用户投诉更有效。

















