group by本身不会产生笛卡尔积,问题一定出在它前面的join阶段;只要join缺on、on无效或一对多表未聚合就直接连接,中间结果即爆炸,group by仅对错误结果徒劳汇总。

GROUP BY 本身不会产生笛卡尔积,问题一定出在它前面的 JOIN 阶段——只要某次 JOIN 缺 ON、ON 无效、或一对多表没聚合就直接连,中间结果就已经爆炸了,GROUP BY 只是“照单全收”再徒劳汇总。
每个 JOIN 后必须跟有效 ON,不能靠 WHERE 补救
WHERE 是连接完成之后才执行的,它对 JOIN 过程中的行数膨胀毫无约束力。把右表过滤条件塞进 WHERE,等于先让数据库硬算完全部组合,再砍掉不要的行——性能更差,还掩盖了真正的问题。
-
LEFT JOIN customers c ON o.customer_id = c.id WHERE c.status = 'active'→ 先拉所有客户(含 inactive),再筛,中间结果可能翻倍 -
LEFT JOIN customers c ON o.customer_id = c.id AND c.status = 'active'→ 只关联活跃客户,JOIN 输出行数天然更小 - 用
INNER JOIN替代带 WHERE 的LEFT JOIN,如果语义允许,更清晰也更安全
一对多表必须先聚合,再 JOIN 主表
orders(1 行)→ order_items(5 行)→ payments(3 行),三者直接 LEFT JOIN,结果就是 1 × 5 × 3 = 15 行。这不是语法错误,是逻辑失控——你本只需要每个订单的总金额和最新付款时间,却让数据库生成了所有排列组合。
- 错:直接
JOIN order_items i ON o.id = i.order_id JOIN payments p ON o.id = p.order_id - 对:用子查询提前收拢明细,
JOIN (SELECT order_id, SUM(amount) AS total FROM order_items GROUP BY order_id) i ON o.id = i.order_id - 要最新一条付款记录?用
ROW_NUMBER() OVER (PARTITION BY order_id ORDER BY created_at DESC)筛出 rank = 1 再 JOIN
检查 EXPLAIN 中的 rows 和 Extra 字段
执行 EXPLAIN 后,如果某一步的 rows 值远大于你预期的主表行数(比如 orders 表 10 万行,但 JOIN 后 rows 显示 800 万),或者 Extra 出现 Using temporary; Using filesort,基本可以断定中间结果已笛卡尔化。
- MySQL 8.0+ 或 PostgreSQL 中,
HashAgg后紧跟External sort,说明内存扛不住原始宽表 - 索引必须覆盖
WHERE过滤字段 +GROUP BY字段的顺序,比如WHERE shop_id = 123 GROUP BY user_id,最优索引是(shop_id, user_id) - 避免在 JOIN 字段上用函数,如
ON UPPER(a.name) = UPPER(b.name),会破坏索引使用
最常被忽略的点是:把子查询当“安全区”——哪怕子查询内部做了 GROUP BY,一旦它作为右表参与 JOIN 时没写 ON,或者 ON 写成 ON 1=1,照样触发笛卡尔积。子查询只是临时表,不是免死金牌。










