DATE(created_at)函数导致索引必然失效,因B+树只存原始值,无法匹配计算结果;应改用冗余字段created_date并建索引,或先范围过滤再分组。

直接对 DATE(created_at) 这类函数表达式做 GROUP BY,性能差不是“可能慢”,而是“必然慢”——索引完全失效,MySQL 只能全表扫描后逐行计算日期值,再分组。
为什么 DATE() 函数会让索引失效
MySQL 的 B+ 树索引只存储原始字段值,不存储函数计算结果。哪怕 created_at 上有索引,DATE(created_at) 仍需对每一行执行函数调用,无法利用索引的有序性或范围查找能力。执行计划里一定会出现 type: ALL 或 Using temporary; Using filesort。
- 索引字段是
created_at DATETIME,但GROUP BY DATE(created_at)实际比较的是'2024-01-01'这样的 DATE 值,类型和值都不匹配 - MySQL 5.7 不支持函数索引;MySQL 8.0+ 虽支持
INDEX(DATE(created_at)),但DATE()表达式在某些版本中仍被判定为“非确定性”,实际不生效 - 即使建了函数索引,优化器也可能因统计信息不准而弃用它,不如显式冗余字段可靠
替代方案:用冗余字段 + 索引代替函数分组
高频按天/月/年统计时,加一个生成列(STORED)并建索引,是最稳、最易维护的做法。
- 添加冗余字段:
ALTER TABLE logs ADD COLUMN created_date DATE AS (DATE(created_at)) STORED; - 建索引:
CREATE INDEX idx_created_date_user_id ON logs(created_date, user_id); - 改写查询:
SELECT created_date, COUNT(*) FROM logs WHERE created_date BETWEEN '2024-01-01' AND '2024-01-31' GROUP BY created_date;
注意:STORED 是必须的,VIRTUAL 列无法建索引(MySQL 8.0.12+ 支持虚拟列索引,但生产环境兼容性和稳定性不如 STORED)。
低频或灵活时间粒度查询怎么办
如果只是偶尔按小时、周、季度分组,不值得加一堆冗余字段,那就靠前置过滤 + 小数据集上做函数分组。
- 先用时间范围大幅缩小数据量:
WHERE created_at >= '2024-06-01' AND created_at - 再在过滤后的结果上用函数:
GROUP BY HOUR(created_at)或GROUP BY YEARWEEK(created_at) - 确保该范围内数据量可控(比如 ≤ 10 万行),否则仍会触发磁盘临时表
别指望加 SQL_SMALL_RESULT 提示能救回来——如果输入行数没压下来,哈希聚合照样爆内存。
容易忽略的关键点
很多人试过加索引、换函数、调参数,还是慢,问题常出在两个隐性环节:
-
WHERE条件没走索引,导致分组前就已全表扫描——查EXPLAIN看key_len是否合理、rows是否远大于预期 -
SELECT里带了未被索引覆盖的字段(比如MAX(content)),引发大量回表,让本可毫秒完成的分组卡在 IO 上


















