在 SQL 中处理日期数据时,经常需要从完整的日期值里单独拿到年份或月份,比如统计某一年注册的用户数、按月汇总订单金额,或者判断某个日期是否属于目标月份。YEAR() 和 MONTH() 就是完成这类任务最直接的两个标量函数。它们通常接收 DATE、DATETIME、TIMESTAMP 类型的表达式,也可以接收能隐式转换为日期类型的字符串,返回结果分别是四位整数年份和一到两位整数月份。本文会结合 MySQL、SQL Server 等常见数据库,把这两个函数的用法、差异以及容易踩到的坑一次讲清楚。

YEAR 和 MONTH 的基础语法与数据库差异
在 MySQL、MariaDB 和 SQL Server 中,YEAR() 和 MONTH() 的语法非常相近:YEAR(日期表达式) 返回该日期对应的四位年份,MONTH(日期表达式) 返回一到十二之间的月份整数。以 MySQL 为例,下面的查询可以直接把字符串日期拆成年和月。
-- MySQL / MariaDB 基础用法
SELECT YEAR('2024-03-15') AS year_value,
MONTH('2024-03-15') AS month_value;
执行后会得到 year_value 为 2024,month_value 为 3。对于 DATETIME 或 TIMESTAMP 类型的列,两个函数同样适用。假设 orders 表中存在 created_at 列,就可以在 SELECT、WHERE、GROUP BY、ORDER BY 中直接使用它们。需要理解的是,这类函数返回整数,不保留前导零,例如 3 月返回的是 3 而不是 03,因此在报表展示时如果要求月份固定两位,通常还需要做补零处理。
SQL Server 的写法基本一致,可以直接对日期列调用 YEAR 和 MONTH。此外 SQL Server 还提供了 DATEPART(year, created_at) 和 DATEPART(month, created_at),效果等价。PostgreSQL 和 Oracle 则更偏向标准 SQL 的 EXTRACT 表达式,虽然有些环境也可能通过扩展支持 YEAR 函数,但在跨数据库场景下建议优先使用 EXTRACT。
-- SQL Server 写法
SELECT YEAR(created_at) AS order_year,
MONTH(created_at) AS order_month
FROM orders;
-- PostgreSQL / Oracle 推荐写法
SELECT EXTRACT(YEAR FROM created_at) AS order_year,
EXTRACT(MONTH FROM created_at) AS order_month
FROM orders;
如果习惯使用 PostgreSQL 的 DATE_PART,也可以写成 DATE_PART('year', created_at),效果与 EXTRACT 类似。至于 SQLite,虽然不直接支持 YEAR 函数,但可以用 STRFTIME('%Y', 日期字段) 实现相同目的。了解这些差异后,在切换数据库或者阅读不同项目代码时就不容易混淆。
筛选、分组与报表中的实战用法
只提取年月往往不是最终目的,更有价值的是把提取结果用于筛选和聚合。例如要查询 2024 年创建的所有订单,最直观的写法是把 YEAR(created_at) 直接放进 WHERE 子句。
-- 按年份筛选订单 SELECT order_id, customer_id, created_at FROM orders WHERE YEAR(created_at) = 2024;
这种写法可读性好,但如果 created_at 列上建立了索引,函数作用在列上会导致优化器很难利用索引进行范围扫描。数据量较大时,不建议把它作为第一选择。更稳妥的方式是用明确的日期范围条件,让查询保持可索引。
-- 用范围条件替代函数筛选,更利于索引 SELECT order_id, customer_id, created_at FROM orders WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01';
在报表统计中,按月分组几乎是 YEAR 和 MONTH 的经典组合。下面按用户注册年月统计数量,很多后台看板都是这种输出形式。分组时需要注意,SELECT 中出现没有包含在聚合函数里的列,必须同时出现在 GROUP BY 中,否则严格模式下会直接报错。
-- 按年和月统计注册用户数
SELECT YEAR(reg_time) AS reg_year,
MONTH(reg_time) AS reg_month,
COUNT(*) AS user_count
FROM users
GROUP BY YEAR(reg_time), MONTH(reg_time)
ORDER BY reg_year, reg_month;
如果希望月份显示为两位数字,MySQL 可以借助 LPAD 处理,例如 LPAD(MONTH(reg_time), 2, '0'),其他数据库也都有对应的补零函数。需要生成“2024-03”这种年月组合时,可以用 CONCAT 或字符串拼接。
-- MySQL 生成 2024-03 格式的年月 SELECT CONCAT(YEAR(created_at), '-', LPAD(MONTH(created_at), 2, '0')) AS month_key FROM orders;
这种年月键值在制作折线图、柱状图或月度对比表时非常实用,尤其是配合 ORDER BY 排序后,前端可以直接使用稳定格式的月份标识。
常见陷阱:字符串格式、空值和对索引的影响
YEAR 和 MONTH 虽然简单,但在实际项目里仍然有几个容易出错的地方。第一个是字符串格式问题。MySQL 对日期字符串的解析能力并不稳定,标准格式 yyyy-mm-dd 通常可以正确转换,但用斜杠分隔的字符串在部分配置或严格模式下可能返回 NULL。
-- MySQL 中不同日期格式的解析差异
SELECT YEAR('2024-03-15') AS y1,
MONTH('2024-03-15') AS m1,
YEAR('2024/03/15') AS y2,
MONTH('2024/03/15') AS m2;
如果查询结果里 y2、m2 是 NULL,说明字符串没有被识别为有效日期。处理办法是使用 STR_TO_DATE 明确转换,例如 STR_TO_DATE('2024/03/15', '%Y/%m/%d'),然后再套用 YEAR、MONTH。这样无论原始字符串是什么分隔方式,都能得到稳定的年份和月份。
第二个是空值问题。任何标量函数遇到 NULL 输入基本都会返回 NULL,如果不加处理,报表里可能出现空白值。可以在 SELECT 阶段用 CASE 或数据库提供的 COALESCE 函数兜底。
-- 空值处理示例 SELECT COALESCE(CAST(YEAR(created_at) AS CHAR), '未知年份') AS order_year FROM orders;
第三个是索引失效问题,前面已经提到。如果必须保留索引效率,同时又希望使用函数式写法,可以考虑在表中增加生成列,或者使用函数索引。MySQL 8、SQL Server 支持生成列,PostgreSQL、Oracle 支持函数索引,这些方案都能在保证查询可读性的同时减少性能损耗。
-- MySQL 8 通过生成列优化年份筛选 ALTER TABLE orders ADD COLUMN order_year INT GENERATED ALWAYS AS (YEAR(created_at)) STORED, ADD INDEX idx_order_year (order_year);
总之,YEAR 和 MONTH 是日期处理中最基础也最常用的两个函数。理解它们的适用数据库、返回类型以及对查询计划的影响,能帮助你在日常开发中避开很多低效 SQL 和隐性错误。当遇到跨数据库需求时,善用 EXTRACT 和 DATE_PART 也能保持 SQL 的可移植性。