笛卡尔积必然发生于JOIN条件缺失、错误或失效时;常见原因包括ON漏写、逗号语法缺WHERE关联、LEFT JOIN后WHERE过滤导致退化为INNER JOIN、字段类型不一致致索引失效、子查询未控粒度等。

笛卡尔积不是“可能出错”,而是只要JOIN条件缺失、写错或隐式失效,它就一定发生——而且往往在你看到结果前,数据库已经把内存和IO耗光了。
ON子句漏写或写成WHERE,是最常见的触发点
老式逗号语法(FROM a, b)必须靠WHERE补关联;新式JOIN语法则必须用ON。两者混用或位置放错,直接导致中间结果膨胀。
-
LEFT JOIN b ON a.id = b.a_id WHERE b.status = 'active':先全量LEFT JOIN(含NULL),再过滤,b.status IS NULL的行被干掉,等效于INNER JOIN,还白跑一遍空匹配 - 正确写法是
LEFT JOIN b ON a.id = b.a_id AND b.status = 'active':只拉符合条件的右表行,不生成无效组合 - 如果用的是
FROM a, b,那WHERE里必须有a.id = b.a_id,缺一个字段等于没写
关联字段类型不一致,索引直接失效
比如t1.id是BIGINT,t2.t1_id是VARCHAR,即使值看起来一样,MySQL也会对其中一列做隐式转换,导致无法走索引——于是退化为全表扫描+嵌套循环,笛卡尔积就坐实了。
- 用
EXPLAIN看type列:出现ALL或index且rows远大于表实际行数,基本就是这问题 - PostgreSQL里看执行计划中的
Nested Loop节点下actual rows,如果比左表行数高一两个数量级,八成是类型不匹配或没索引 - 修复方式只有统一字段类型,或显式
CAST(但不如改表结构干净)
子查询参与JOIN时,未控制其输出粒度
子查询本身返回多行,又没加DISTINCT或聚合,再跟外层表JOIN,很容易按行数相乘爆炸。尤其LEFT JOIN (SELECT * FROM b)这种写法,等于把b表原样“摊开”再连。
- 如果只是要b表某几个字段,别写
SELECT *,只查id, name这类必要字段 - 一对多关系下,优先在子查询里先聚合:
(SELECT order_id, SUM(amount) FROM order_items GROUP BY order_id),再JOIN,避免扩散 - 需要明细但只取最新一条?用
ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY created_at DESC)筛,别让引擎自己猜 - MySQL 8.0.14+ 或 PostgreSQL 可考虑
LATERAL,让子查询能引用外层变量,避免提前物化整个结果集
用LIMIT快速验证是否已膨胀,但别依赖它修复
线上不敢跑全量?加LIMIT 100不是为了“先看看”,而是为了秒级确认问题规模:如果LIMIT 100都卡住或返回重复主键,说明中间结果早已失控。
-
SELECT COUNT(*) FROM a JOIN b ON a.id = b.a_id LIMIT 10—— 这条没意义,COUNT会强制算完再截断 - 应该写
SELECT a.id, b.id FROM a JOIN b ON a.id = b.a_id LIMIT 100,看结果里a.id是否大量重复 - 如果发现
a.id重复10次,而b表平均每个a_id对应10条记录,那大概率是JOIN前b没去重或条件没生效
真正难的不是加ON或建索引,而是意识到:JOIN的每一步都在放大行数,而GROUP BY、COUNT、ORDER BY这些操作,都是在那个已经被放大的结果集上干活——所以问题永远出在聚合之前,而不是之后。










