group by 1导致全表扫描,因其使用常量而非索引列作为分组键,无法利用索引有序性,迫使mysql执行全表扫描并创建临时表聚合,explain中显示type: all和using temporary。

GROUP BY 1 这种写法为什么等于全表扫描
因为 GROUP BY 1(或任何常量、表达式)无法提供分组键的有序性或可索引性,MySQL 只能放弃所有索引,老老实实扫完整张表,再用临时表做聚合——EXPLAIN 里必然出现 type: ALL 和 Extra: Using temporary。
常见错误现象:
- 写
SELECT COUNT(*), 'total' AS tag FROM orders GROUP BY 'total',以为能“强行归成一行”,结果比直接SELECT COUNT(*)还慢 - ORM 自动生成
GROUP BY 1(比如 Django 的.values()配合聚合时没传字段),查百万行表要好几秒 - 误以为
GROUP BY 1是语法糖,实际它等价于按第一列的值分组,但“1”不是列名,而是字面量常量,引擎无法映射到任何索引列
真正起作用的,是分组字段是否对应索引的**可遍历前缀**。常量不指向任何物理列,自然没有顺序可言,也谈不上“复用索引排序”。哪怕表只有 10 行,优化器也会选全表扫描路径,因为它压根没别的选择。
GROUP BY 字符串字面量(如 GROUP BY 'abc')的执行逻辑
MySQL 在解析阶段就确认 GROUP BY 'abc' 的结果恒为单一分组,但它不会因此跳过数据扫描——聚合函数如 COUNT(*) 或 SUM(amount) 仍需遍历所有行来累加。这和 SELECT COUNT(*) FROM orders 本质一样,但多了额外开销:构建单桶临时表、维护聚合状态、处理 NULL 边界。
关键区别在于:
-
SELECT COUNT(*) FROM orders:优化器可能走index_merge或直接读聚簇索引统计信息(InnoDB 的TABLE_ROWS是估算值,但COUNT(*)实际执行仍要扫) -
SELECT COUNT(*), 'abc' FROM orders GROUP BY 'abc':强制触发Using temporary,且无法被松散索引扫描(Loose Index Scan)优化,哪怕orders有完美索引也没用
更隐蔽的问题是:如果语句里还带了 HAVING(比如 HAVING COUNT(*) > 1000),优化器无法提前剪枝,必须等全表扫完、聚合完,才能判断是否满足条件。
哪些“看似分组”的写法其实安全
真正能避免全表扫描的,是那些让优化器识别出“分组键可由索引顺序天然支撑”的写法。以下几种情况可以走索引:
-
GROUP BY user_id→ 有单列索引(user_id)或联合索引以user_id开头 -
GROUP BY user_id, status→ 有联合索引(user_id, status),且 WHERE 条件只含等值(如WHERE deleted = 0) -
GROUP BY DATE(created_at)不行,但GROUP BY created_at+WHERE created_at BETWEEN ...可以,前提是索引包含created_at且顺序匹配
注意:GROUP BY (user_id + 0)、GROUP BY COALESCE(user_id, 0) 同样失效——只要对列做了任何包装,索引就断连。
如何快速发现这类问题
别靠猜,用 EXPLAIN FORMAT=TREE(MySQL 8.0+)或传统 EXPLAIN 看三处:
-
key列是否为NULL:是 → 没走索引 -
Extra是否含Using temporary:是 → 已掉进最慢路径 -
rows是否接近表总行数:比如表有 200 万行,rows=1982341→ 基本就是全扫
最容易被忽略的是:开发测试时数据少,GROUP BY 'xxx' 看不出慢;一上生产,表涨到千万级,临时表落盘、磁盘 I/O 打满,才暴露问题。这种写法在 SQL 层面“合法”,但性能上毫无容错余地。










