WHERE中对列使用MONTH()函数会导致索引失效,引发全表扫描;正确写法是将函数移至右侧,如created_at >= '2024-05-01' AND created_at < '2024-06-01'。

WHERE 中用 MONTH() 会丢掉索引
直接写 WHERE MONTH(created_at) = 5 看似简单,但 MySQL 无法用上 created_at 字段的索引——因为函数作用在列上,优化器没法做范围扫描。实际执行是全表扫描,数据一过百万就明显变慢。
- 正确做法是把函数移到右边,让左边保持裸列:用
WHERE created_at >= '2024-05-01' AND created_at - 注意用
而不是 <code>,避免跨月重复或遗漏(尤其字段含时间部分时) - 如果
created_at是DATE类型(无时分秒),安全;若是 <code>DATETIME或TIMESTAMP,必须带时间,否则会漏掉'2024-05-31 23:59:59'
查“2024年5月”别只写 YEAR() + MONTH())
有人写 WHERE YEAR(created_at) = 2024 AND MONTH(created_at) = 5,逻辑没错,但同样走不了索引,而且比单用 MONTH() 更慢——两个函数叠加,计算开销翻倍,还多一次函数调用。
- 坚持用范围查询:
created_at BETWEEN '2024-05-01' AND '2024-05-31 23:59:59'可读性好,但不如开区间清晰 - 更推荐:
created_at >= '2024-05-01' AND created_at ——语义明确、索引友好、时区兼容性也更好 - 如果表里有分区(比如按月分区),这种写法还能命中对应分区,
YEAR/MONTH函数反而可能绕过分区裁剪
时区问题会让 MONTH() 返回意外结果
MySQL 的 MONTH() 按 session 时区解释时间值。如果你的服务器用 UTC,应用写入时没显式转时区,而用户期望的是东八区的“5月”,那 MONTH('2024-05-01 00:00:00') 在 UTC 里其实是 4 月 30 日晚,结果就是查不到。
- 确认时区设置:
SELECT @@time_zone, @@session.time_zone; - 稳妥做法:统一在应用层把时间转成目标时区再拼条件,或在 SQL 中用
CONVERT_TZ()显式转换:WHERE MONTH(CONVERT_TZ(created_at, '+00:00', '+08:00')) = 5 - 但注意:
CONVERT_TZ()同样让索引失效,所以优先级还是:先用范围查询 + 服务端统一时区处理
想动态查“上个月”?别硬套 MONTH(NOW())-1
WHERE MONTH(created_at) = MONTH(NOW()) - 1 在 1 月会返回 0,查不出任何数据;而且跨年问题(12 月 → 1 月)完全没处理。
- 用日期运算:
WHERE created_at >= DATE_SUB(DATE_FORMAT(NOW(), '%Y-%m-01'), INTERVAL 1 MONTH) AND created_at - 这个写法能自动处理年份切换,且左右都是确定日期字面量,索引照常生效
- 如果字段类型是
DATE,可简化为:WHERE created_at >= LAST_DAY(NOW()) + INTERVAL 1 DAY - INTERVAL 1 MONTH,但可读性稍差
事情说清了就结束。真正卡住人的往往不是语法,而是索引失效和时区错位——这两个点不提前想清楚,查着查着就发现慢得离谱,或者数据对不上。


















