GROUP BY 表达式(如 YEAR(created_at))必然比 GROUP BY created_at 慢,因其强制MySQL放弃索引、触发全表扫描及临时表分组;索引按原始列值有序存储,而表达式需运行时计算,无法对齐B+树结构。

GROUP BY 表达式(如 GROUP BY YEAR(created_at))必然比 GROUP BY created_at 慢,根本原因是它强制 MySQL 放弃所有索引,退化为全表扫描 + 内存/磁盘临时表分组。
为什么表达式会让索引完全失效
MySQL 无法将函数或表达式结果与 B+ 树索引的有序结构对齐。哪怕你建了 INDEX(created_at),只要写成 GROUP BY DATE(created_at),优化器就无法利用该索引做松散索引扫描(Loose Index Scan),只能逐行计算表达式值再分组。
- 索引本质是按原始列值排序存储的,
YEAR(created_at)是运行时计算,无法预排序 - 即使 MySQL 8.0+ 支持函数索引,也必须显式声明:
CREATE INDEX idx_created_year ON t ((YEAR(created_at)));普通索引INDEX(created_at)对此无效 - EXPLAIN 中会看到
type: ALL或key: NULL,且Extra必含Using temporary和Using filesort
哪些表达式写法最危险
以下写法在任何主流版本中都会触发全量计算和临时表,应绝对避免:
-
GROUP BY CONCAT(first_name, ' ', last_name):字符串拼接无法走索引,CPU 和内存开销双高 -
GROUP BY UPPER(email):隐式类型/字符集转换常导致索引失效,且大小写归一化需逐行处理 -
GROUP BY SUBSTRING(phone, 1, 3):前缀截取破坏索引有序性,除非建了生成列索引 -
GROUP BY created_at + INTERVAL 1 DAY:任意算术运算都使索引失效
怎么改写才能真正提速
核心思路是把“运行时计算”转为“存储时预计算”或“范围过滤后分组”:
- 高频固定粒度(如按天统计):加生成列
created_date DATE AS (DATE(created_at)) STORED,再建索引INDEX(created_date, user_id),查询用WHERE created_date = '2026-09-01' GROUP BY created_date - 低频灵活粒度(如按小时分析):先用范围缩小数据量,
WHERE created_at >= '2026-09-01' AND created_at ,至少减少 95% 扫描行数 - 需要多级分组(年+月+日):建联合生成列
(YEAR(created_at), MONTH(created_at), DAY(created_at))并索引,而非嵌套函数
容易被忽略的兼容性陷阱
不同数据库对表达式索引的支持差异极大,别假设写法通用:
- MySQL 5.7 不支持函数索引,
INDEX(DATE(created_at))语法直接报错 - PostgreSQL 要求函数必须标记为
IMMUTABLE才能建函数索引,TO_CHAR(created_at, 'YYYY-MM')默认不是 - SQL Server 的计算列必须设为
PERSISTED才能被索引,否则仍无法加速 GROUP BY - 即便建了函数索引,如果 WHERE 条件里用了不同精度(如
WHERE created_at >= '2026-09-01'但索引是YEAR(created_at)),下推失败,照样慢
真正起效的优化,从来不是调大 tmp_table_size 或加 SQL_BIG_RESULT 提示——而是让分组键本身变成索引可覆盖的、物理有序的列值。一旦写了表达式,你就主动放弃了索引加速的全部可能性。

















