MONTH()和YEAR()函数在WHERE中直接过滤会导致索引失效;应改用范围查询如created_at >= '2023-03-01' AND created_at < '2023-04-01',以保持索引可用性。

MONTH() 和 YEAR() 函数在 WHERE 中直接过滤无效?
直接写 WHERE MONTH(created_at) = 3 AND YEAR(created_at) = 2023 看似合理,但会导致索引失效——因为对字段施加函数后,数据库无法利用 created_at 上的 B-tree 索引。实际执行会全表扫描,百万级数据时响应可能从毫秒变秒级。
正确做法是把函数挪到右边,让左边保持裸字段:
- ✅ 推荐:
WHERE created_at >= '2023-03-01' AND created_at - ❌ 避免:
WHERE MONTH(created_at) = 3 AND YEAR(created_at) = 2023 - 注意:用
而不是 <code>,避免时区或毫秒截断误差
MySQL 8.0+ 可用 DATE_FORMAT 做安全的字符串匹配?
如果实在需要按年月字符串筛选(比如配合报表参数),DATE_FORMAT(created_at, '%Y-%m') 比 YEAR()+MONTH() 组合更简洁,但仍有索引问题。唯一安全用法是配合生成列(generated column)+索引:
ALTER TABLE transactions ADD COLUMN ym CHAR(7) STORED AS (DATE_FORMAT(created_at, '%Y-%m'));
再建索引:CREATE INDEX idx_ym ON transactions(ym);,之后就能高效查 WHERE ym = '2023-03'。
- ⚠️ 注意:STORED 生成列才支持索引,VIRTUAL 不行
- ⚠️ MySQL 5.7 不支持函数索引,必须用生成列;8.0+ 可直接
CREATE INDEX idx_f ON t((DATE_FORMAT(created_at, '%Y-%m')))
PostgreSQL 怎么等价实现?别用 EXTRACT
PostgreSQL 的 EXTRACT(YEAR FROM created_at) 和 EXTRACT(MONTH FROM created_at) 同样导致索引失效。应改用范围查询 + DATE_TRUNC:
- ✅ 推荐:
WHERE created_at >= DATE_TRUNC('month', '2023-03-01'::date) AND created_at - ✅ 更通用:
WHERE created_at >= '2023-03-01'::date AND created_at (无需函数) - ❌ 少用:
WHERE EXTRACT(YEAR FROM created_at) = 2023 AND EXTRACT(MONTH FROM created_at) = 3
PostgreSQL 对日期字面量类型推断很稳,'2023-03-01' 自动转为 date,不用显式 cast,但加上更明确。
SQL Server 的 DATEPART 要小心隐式转换
DATEPART(YEAR, created_at) 和 DATEPART(MONTH, created_at) 行为和 MySQL 类似,也会阻止索引使用。但 SQL Server 有个坑:如果 created_at 是 datetime2 类型,而你传入字符串如 '2023-03-01',SQL Server 可能因语言设置不同解析成 01/03/2023(日/月/年)导致逻辑错误。
- ✅ 安全写法:
WHERE created_at >= '20230301' AND created_at (ISO 格式,无歧义) - ✅ 或用
WHERE created_at >= DATEFROMPARTS(2023, 3, 1) AND created_at - ⚠️ 避免:
WHERE DATEPART(YEAR, created_at) = 2023 AND DATEPART(MONTH, created_at) = 3
真正容易被忽略的是时区:如果业务数据跨时区,created_at 存的是 UTC,而你要按本地月份筛选,得先 AT TIME ZONE 转换,否则凌晨写入的数据可能算错月。

















