group by本身不产生笛卡尔积,问题根源在join阶段未控基数导致中间结果爆炸;explain出现using temporary/filesort或rows远超count(distinct)即为典型信号;left join过滤条件须写on中,索引需覆盖where+group by字段顺序,避免函数破坏索引。

GROUP BY本身不产生笛卡尔积,但你在它前面写的JOIN如果漏条件、写错位置或没控制基数,中间结果早就炸开了——这时候再GROUP BY,只是在错误的数据上徒劳聚合。
为什么EXPLAIN里看到Using temporary + filesort就该警觉
这说明数据库被迫把海量中间行拉进内存(甚至磁盘)做分组和排序。根本原因不是GROUP BY太重,而是JOIN阶段已经生成了远超预期的行数。比如orders × order_items × customers三张表连在一起,若没限制order_items.status或customers.is_active,单个订单可能对应几十个无效组合。
-
EXPLAIN中rows列数值远大于COUNT(DISTINCT order_id),就是典型信号 - PostgreSQL里
HashAgg后紧跟External sort,基本等于内存不够扛原始宽表 - MySQL 5.7默认对
GROUP BY隐式加ORDER BY,没索引就会触发Using filesort
ON里写过滤条件,别塞到WHERE里
LEFT JOIN中,右表的过滤条件放WHERE会把本该保留的NULL行干掉,等效变INNER JOIN;更糟的是,它会让数据库先全量JOIN再过滤,中间过程白跑且放大笛卡尔风险。
- 错:
LEFT JOIN customers c ON o.customer_id = c.id WHERE c.status = 'active'→ 先拉所有客户,再筛 - 对:
LEFT JOIN customers c ON o.customer_id = c.id AND c.status = 'active'→ 只关联活跃客户,JOIN结果集更小 - 如果只是要客户名,别JOIN整张
customers,改用子查询:(SELECT id, name FROM customers WHERE status = 'active')
用子查询提前收拢“一”侧数据
当主表和明细表是1:N关系,而你只关心主表粒度(如每个订单的总金额),就别让明细表直接参与JOIN——先按主键聚合明细,再JOIN回来。
- 避免:
orders o JOIN order_items i ON o.id = i.order_id→ 每个订单项都带一遍订单字段,膨胀明显 - 推荐:
orders o 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)筛出单条,再JOIN
索引必须覆盖WHERE过滤 + GROUP BY字段顺序
光有user_id索引不够,如果查询是WHERE shop_id = 123 GROUP BY user_id,最优索引是(shop_id, user_id):前段快速定位数据块,后段让该块内数据天然有序,省掉排序开销。
- 别在
GROUP BY里套函数,如GROUP BY DATE(created_at)→ 改用created_at >= '2026-01-01' AND created_at 配合索引 -
COUNT(column)会跳过NULL,若该列无NOT NULL约束,就无法走索引覆盖,考虑补约束或换COUNT(*) - 分组后结果集超过10万行?先
SELECT COUNT(DISTINCT ...)看粒度,比盲目建索引有用得多
最容易被忽略的是:问题往往不出现在GROUP BY那一行代码,而出现在它上面三行JOIN里——检查每一对表之间有没有最小粒度的ON条件,比调优聚合本身重要十倍。











