对null日期调用year()或month()结果必为null而非0或报错;where中直接使用会漏掉null记录,应显式判断is null或改用范围查询以走索引。

YEAR() 和 MONTH() 函数怎么用才不踩空值坑
直接说结论:对 NULL 日期字段调用 YEAR() 或 MONTH(),结果一定是 NULL,不是 0,也不是报错。这点很多人在写条件查询时误以为能用 YEAR(col) = 2024 安全过滤,结果漏掉所有 NULL 记录。
常见错误现象:SELECT * FROM orders WHERE YEAR(created_at) = 2024 —— 如果 created_at 有空值,这些行不会被返回,但你可能根本没意识到它们本该参与业务逻辑(比如未填时间的待补录单)。
- 使用场景:统计、分组、WHERE 过滤、生成分区键
- 参数差异:
YEAR()返回 4 位整数(如2024),MONTH()返回 1–12 的整数(不是字符串) - 实操建议:需要包含空值逻辑时,显式判断:
WHERE created_at IS NULL OR YEAR(created_at) = 2024 - 性能影响:这两个函数本身开销极小,但一旦出现在 WHERE 子句左侧(如
YEAR(created_at) = 2024),就无法走created_at字段上的索引——应改写为范围查询:created_at >= '2024-01-01' AND created_at
YEAR() 在 GROUP BY 中为什么有时结果不对
问题不在函数本身,而在 MySQL 的 SQL 模式和隐式类型转换。当 created_at 是 DATETIME 类型,而你在 GROUP BY YEAR(created_at) 后又 SELECT 其他非聚合字段(比如 id 或 title),MySQL 5.7+ 默认开启 ONLY_FULL_GROUP_BY 会直接报错:Expression #2 of SELECT list is not in GROUP BY clause。
更隐蔽的问题是:关掉这个模式后,MySQL 会随机选一条记录的 id 返回,看起来“分组成功”,实际数据不可靠。
- 实操建议:明确聚合意图。要按年统计数量,就写
SELECT YEAR(created_at), COUNT(*) FROM logs GROUP BY YEAR(created_at) - 如果还要查某年的最新一条记录,别用
GROUP BY YEAR()+ 非聚合字段,改用窗口函数(MySQL 8.0+)或关联子查询 - 兼容性注意:MySQL 5.6 及更早版本不支持窗口函数,
YEAR()分组后混选字段的行为更危险,且无报错提示
用 YEAR/MONTH 做分区字段?先看存储引擎限制
MySQL 的原生分区(PARTITION BY RANGE / LIST)不接受函数表达式作为分区键,所以 PARTITION BY RANGE (YEAR(created_at)) 是语法错误。你只能分区到具体列,比如 created_at 本身,再配合 YEAR() 在查询中做逻辑切分。
常见错误现象:建表时报错 This partition function is not allowed,尤其在尝试用 TO_DAYS() 或 YEAR() 直接写进 PARTITION BY 时。
- 实操建议:想按年分区,用
PARTITION BY RANGE (TO_DAYS(created_at))(兼容性好),或 MySQL 8.0+ 支持PARTITION BY RANGE COLUMNS(created_at),但需手动定义每个年份的起止日期边界 - 性能影响:函数分区不可用,不代表不能优化。把
created_at单独建索引,并在 WHERE 中用范围条件(而非YEAR()),才能真正触发分区裁剪 - 容易忽略的一点:分区表的
created_at列必须是 NOT NULL,否则分区插入可能失败,而YEAR()对 NULL 返回 NULL,这和分区约束冲突
替代 YEAR/MONTH 的更安全写法
纯提取年月信息没问题,但一旦涉及比较、排序或索引利用,硬编码 YEAR() 就成了性能瓶颈和语义模糊点。MySQL 8.0+ 提供了 EXTRACT(YEAR FROM created_at) 和 DATE_FORMAT(created_at, '%Y-%m'),前者语义等价,后者可读性更强,但都逃不开索引失效问题。
真正关键的替代思路,是把“年月”作为冗余字段存下来,比如加一列 year_month CHAR(7) GENERATED ALWAYS AS (DATE_FORMAT(created_at, '%Y-%m')) STORED,然后在这个字段上建索引并查询。
- 实操建议:新表设计优先考虑生成列 + 索引;老表改造可加普通列,用 UPDATE 批量填充,再建索引
- 为什么这样做:避免每次查询都计算函数,让
WHERE year_month = '2024-03'走索引,也方便用BETWEEN做区间扫描 - 注意点:生成列的表达式必须是确定性的(
DATE_FORMAT是,NOW()不是),且 MySQL 5.7+ 才支持 STORED 生成列
函数本身很简单,难的是它在查询计划里藏得有多深、在 NULL 和索引之间绕得有多远。写完 YEAR() 别急着跑,先看下执行计划里有没有 Using filesort 或 type: ALL。











