原创

MySQL 中如何获取时间字段的年份和月份

很多业务表都会有 created_atupdated_atpay_timelogin_time 这类字段。取年份、月份这件事本身不难,难的是别把一个简单统计写成慢查询。MySQL 提供了 YEAR()MONTH()DATE_FORMAT()EXTRACT() 等日期时间函数,官方文档也明确把它们归在 Date and Time Functions 下:YEAR() 用来返回年份,MONTH() 用来返回月份,DATE_FORMAT() 用来按格式输出日期字符串,EXTRACT() 用来提取日期的一部分。([MySQL开发者专区][1])

准备一张示例表

先用一张订单表举例。字段不多,但足够覆盖实际开发里最常见的查询场景。

CREATE TABLE orders (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    order_no VARCHAR(64) NOT NULL,
    user_id BIGINT UNSIGNED NOT NULL,
    amount DECIMAL(10, 2) NOT NULL,
    pay_time DATETIME NOT NULL,
    created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (id),
    KEY idx_pay_time (pay_time),
    KEY idx_created_at (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

插入几条测试数据:

INSERT INTO orders (order_no, user_id, amount, pay_time, created_at) VALUES
('A001', 1001, 99.90,  '2024-01-15 10:20:30', '2024-01-15 10:21:00'),
('A002', 1002, 188.00, '2024-02-03 09:10:00', '2024-02-03 09:11:00'),
('A003', 1001, 59.90,  '2024-02-20 18:30:00', '2024-02-20 18:31:00'),
('A004', 1003, 299.00, '2025-01-08 14:05:00', '2025-01-08 14:06:00');

用 YEAR() 获取年份

最直接的写法是 YEAR(时间字段)

SELECT
    id,
    order_no,
    pay_time,
    YEAR(pay_time) AS pay_year
FROM orders;

结果大概是这样:

id  order_no  pay_time             pay_year
1   A001      2024-01-15 10:20:30  2024
2   A002      2024-02-03 09:10:00  2024
3   A003      2024-02-20 18:30:00  2024
4   A004      2025-01-08 14:05:00  2025

YEAR(date) 返回日期中的年份。按照 MySQL 8.4 官方文档,它返回的年份范围通常是 10009999,如果是 “zero date” 会返回 0,传入 NULL 时返回 NULL。([MySQL开发者专区][1])

用 MONTH() 获取月份

获取月份用 MONTH()

SELECT
    id,
    order_no,
    pay_time,
    MONTH(pay_time) AS pay_month
FROM orders;

MONTH(date) 返回 112,分别表示一月到十二月;如果日期里月份部分为 0,例如某些不完整日期,可能返回 0;传入 NULL 时返回 NULL。([MySQL开发者专区][1])

如果你只是展示月份,MONTH() 足够。如果你要做年月组合统计,单独取月份通常不够,因为不同年份的 2 月会混在一起。

同时获取年份和月份

常见写法有两种。

第一种是分别取年和月:

SELECT
    YEAR(pay_time) AS pay_year,
    MONTH(pay_time) AS pay_month,
    COUNT(*) AS order_count,
    SUM(amount) AS total_amount
FROM orders
GROUP BY YEAR(pay_time), MONTH(pay_time)
ORDER BY pay_year, pay_month;

这种结果适合给后端继续处理:

pay_year  pay_month  order_count  total_amount
2024      1          1            99.90
2024      2          2            247.90
2025      1          1            299.00

第二种是用 DATE_FORMAT() 拼成 yyyy-MM

SELECT
    DATE_FORMAT(pay_time, '%Y-%m') AS pay_month,
    COUNT(*) AS order_count,
    SUM(amount) AS total_amount
FROM orders
GROUP BY DATE_FORMAT(pay_time, '%Y-%m')
ORDER BY pay_month;

DATE_FORMAT(date, format) 会根据格式字符串格式化日期。比如 %Y 表示四位年份,%m 表示两位数字月份,所以 DATE_FORMAT(pay_time, '%Y-%m') 会得到类似 2024-02 的字符串。([MySQL开发者专区][1])

这里有个小细节:DATE_FORMAT() 返回的是字符串。用 %Y-%m 排序通常没问题,因为 2024-012024-022025-01 这种格式天然符合时间顺序。但如果写成 %m-%Y,字符串排序就不再等于时间排序了。

用 EXTRACT() 获取年月

EXTRACT() 是更通用的写法:

SELECT
    EXTRACT(YEAR FROM pay_time) AS pay_year,
    EXTRACT(MONTH FROM pay_time) AS pay_month
FROM orders;

它也可以直接提取 YEAR_MONTH

SELECT
    EXTRACT(YEAR_MONTH FROM pay_time) AS pay_year_month,
    COUNT(*) AS order_count
FROM orders
GROUP BY EXTRACT(YEAR_MONTH FROM pay_time)
ORDER BY pay_year_month;

结果类似:

pay_year_month  order_count
202401          1
202402          2
202501          1

MySQL 文档里的例子也展示了 EXTRACT(YEAR FROM '2019-07-02') 返回 2019EXTRACT(YEAR_MONTH FROM '2019-07-02 01:02:03') 返回 201907。([MySQL开发者专区][1])

这类写法适合做报表统计,尤其是你希望年月是一个数字值,而不是字符串值的时候。

查询某一年、某一月的数据

很多人会这么写:

SELECT *
FROM orders
WHERE YEAR(pay_time) = 2024
  AND MONTH(pay_time) = 2;

能查,语义也直观。

但在数据量大时,这种写法不太建议直接放在线上高频查询里。问题不在函数本身,而在你对列做了函数计算。pay_time 上虽然有索引,但条件变成了 YEAR(pay_time)MONTH(pay_time) 的计算结果,优化器很难像普通范围条件那样直接利用 idx_pay_time 做高效范围扫描。

更稳的写法是时间范围查询:

SELECT *
FROM orders
WHERE pay_time >= '2024-02-01 00:00:00'
  AND pay_time <  '2024-03-01 00:00:00';

查 2024 年全年:

SELECT *
FROM orders
WHERE pay_time >= '2024-01-01 00:00:00'
  AND pay_time <  '2025-01-01 00:00:00';

这个写法看起来没那么“优雅”,但它通常更适合索引。线上查询里,时间字段有索引时,优先考虑这种左闭右开的范围条件。

统计每月数据时怎么写更合适

如果只是离线报表、后台管理页、数据量不大,下面这种写法够用了:

SELECT
    DATE_FORMAT(pay_time, '%Y-%m') AS month,
    COUNT(*) AS order_count,
    SUM(amount) AS total_amount
FROM orders
GROUP BY DATE_FORMAT(pay_time, '%Y-%m')
ORDER BY month;

如果是大表,并且经常按月份统计,可以考虑两种优化方向。

第一种,在查询条件里仍然使用时间范围,减少扫描数据量:

SELECT
    DATE_FORMAT(pay_time, '%Y-%m') AS month,
    COUNT(*) AS order_count,
    SUM(amount) AS total_amount
FROM orders
WHERE pay_time >= '2024-01-01 00:00:00'
  AND pay_time <  '2025-01-01 00:00:00'
GROUP BY DATE_FORMAT(pay_time, '%Y-%m')
ORDER BY month;

第二种,给年月单独建字段。比如订单写入时直接存一个 pay_month

CREATE TABLE order_month_stats (
    id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
    pay_month CHAR(7) NOT NULL COMMENT '格式:yyyy-MM',
    order_count INT NOT NULL DEFAULT 0,
    total_amount DECIMAL(12, 2) NOT NULL DEFAULT 0.00,
    PRIMARY KEY (id),
    UNIQUE KEY uk_pay_month (pay_month)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这不是为了“省一个函数”,而是为了让查询模型更明确。报表类系统里,明细表和统计表分开,往往比在明细表上临时扫全量更可靠。

生成列和函数索引可以用吗

可以,但要看版本和场景。

MySQL 支持生成列,生成列的值可以通过列定义中的表达式计算出来。官方文档给出的定义就是:generated column 的值由列定义里的表达式计算。([MySQL开发者专区][2])

比如可以给订单表加一个年月生成列:

ALTER TABLE orders
ADD COLUMN pay_year_month INT
    GENERATED ALWAYS AS (EXTRACT(YEAR_MONTH FROM pay_time)) STORED,
ADD KEY idx_pay_year_month (pay_year_month);

之后查询某个月可以写:

SELECT *
FROM orders
WHERE pay_year_month = 202402;

MySQL 的 CREATE INDEX 语法里,索引的 key part 可以是列名,也可以是表达式;文档语法中写的是 key_part: {col_name [(length)] | (expr)} [ASC | DESC]。([MySQL开发者专区][3])

所以在支持表达式索引的版本里,也可以考虑类似这样的写法:

CREATE INDEX idx_pay_year_month_expr
ON orders ((EXTRACT(YEAR_MONTH FROM pay_time)));

不过实际项目里我更偏向先用范围查询。如果业务确实高频按年月检索,再考虑生成列或表达式索引。原因很简单:索引不是免费的,写入、更新、存储空间都会跟着变。

几种写法怎么选

如果只是展示年份,直接用:

YEAR(pay_time)

如果只是展示月份,直接用:

MONTH(pay_time)

如果要展示成 2024-02

DATE_FORMAT(pay_time, '%Y-%m')

如果要得到数字型年月:

EXTRACT(YEAR_MONTH FROM pay_time)

如果要查某个月的数据,优先写范围条件:

WHERE pay_time >= '2024-02-01 00:00:00'
  AND pay_time <  '2024-03-01 00:00:00'

这才是实际开发里最容易被忽略的地方:取年月的函数很简单,真正影响性能的是你把它放在了 SELECTGROUP BY,还是放在了 WHERE 条件的时间列上。

正文到此结束
评论插件初始化中...
Loading...