where中对列使用month()函数会导致索引失效,引发全表扫描;正确写法是将函数移至右侧,如created_at >= '2024-05-01' and created_at
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,但可读性稍差事情说清了就结束。真正卡住人的往往不是语法,而是索引失效和时区错位——这两个点不提前想清楚,查着查着就发现慢得离谱,或者数据对不上。












