日期函数与日期处理专题,一篇彻底掌握 SQL 日期计算
前置阅读
- 《08:LIKE 与 REGEXP,一篇彻底掌握 SQL 字符串匹配》
前言
如果说字符串处理考验的是对函数和正则表达式的理解,那么日期处理考验的就是数据分析能力。
在实际开发中,几乎所有业务系统都会涉及日期相关的统计。例如:
-
统计今天新增了多少用户;
-
查询最近 7 天的订单;
-
计算用户连续登录天数;
-
统计每个月的销售额;
-
判断用户是否在第二天再次登录。
这些需求看似不同,但背后使用的都是 SQL 的日期函数。
LeetCode SQL50 中,日期相关的题目数量并不算多,但难度普遍高于普通查询。很多题目真正困难的地方并不是 SQL 语法,而是如何把日期转换成可以计算的数据。
因此,这一篇文章不会只是介绍几个日期函数,而是结合 LeetCode 高频题,系统讲解日期处理的完整思路。读完之后,你不仅能够熟练使用 YEAR()、MONTH()、DATEDIFF() 等函数,还能够独立分析首次登录、连续登录、留存分析等经典问题。
一、SQL 中常见的日期类型
在开始学习日期函数之前,我们首先需要了解 MySQL 中最常见的三种日期类型。
| 类型 | 示例 | 是否包含时间 |
|---|---|---|
| DATE | 2025-07-15 | 否 |
| DATETIME | 2025-07-15 14:30:20 | 是 |
| TIMESTAMP | 2025-07-15 14:30:20 | 是 |
对于 LeetCode SQL 而言,最常见的是 DATE 类型。
例如:
2025-07-15
它只保存日期,不保存具体时间。
而很多实际业务,例如订单系统、聊天系统、支付系统,则更常使用 DATETIME 或 TIMESTAMP,因为需要精确到秒。
需要注意的是,虽然 DATETIME 和 TIMESTAMP 看起来很相似,但它们并不完全相同。
-
DATETIME保存的是输入的日期时间值,不受时区影响。 -
TIMESTAMP保存的是时间戳,会根据服务器时区进行转换。
对于 LeetCode 来说,这一区别几乎不会考察,因此本文主要讨论 DATE 类型。
二、获取当前日期与时间
MySQL 提供了几个非常常用的函数来获取当前日期和时间。
1. CURDATE()
返回当前日期。
SELECT CURDATE();
假设今天是 2025 年 7 月 15 日,那么结果为:
2025-07-15
注意,它不会包含时间。
因此:
WHERE order_date = CURDATE()
表示查询今天的数据。
2. NOW()
如果希望获得完整的日期和时间,则应该使用:
SELECT NOW();
结果类似:
2025-07-15 14:35:28
相比 CURDATE(),NOW() 多了时、分、秒。
一般来说:
-
判断今天的数据,用
CURDATE(); -
记录创建时间、更新时间,用
NOW()。
三、提取日期中的年、月、日
很多统计需求都需要按照年份、月份或者日期进行分组。
例如:
查询 2024 年的所有订单。
或者:
统计每个月的销售额。
这时候,就需要使用日期提取函数。
YEAR()
提取年份。
SELECT YEAR(order_date)
FROM Orders;
如果:
order_date = 2025-07-15
返回:
2025
因此:
SELECT *
FROM Orders
WHERE YEAR(order_date)=2025;
即可查询 2025 年的订单。
MONTH()
提取月份。
SELECT MONTH(order_date)
FROM Orders;
结果:
7
因此:
SELECT *
FROM Orders
WHERE MONTH(order_date)=2;
可以查询所有二月份的数据。
不过需要提醒一点,在真实项目中,这种写法可能会影响索引使用,更推荐使用日期范围查询,例如:
WHERE order_date >= '2025-02-01'
AND order_date < '2025-03-01'
这一点在数据量较大时性能差异会非常明显。
DAY()
提取日期。
SELECT DAY(order_date)
FROM Orders;
如果:
2025-07-15
返回:
15
同理,还有:
HOUR()
MINUTE()
SECOND()
分别用于提取小时、分钟和秒。
四、DATE_ADD() 与 DATE_SUB()
这是 MySQL 中最常用的一对日期计算函数。
很多初学者容易混淆它们,其实记住一句话即可:
-
DATE_ADD() 用于增加时间。
-
DATE_SUB() 用于减少时间。
例如:
SELECT DATE_ADD(
'2025-07-15',
INTERVAL 7 DAY
);
结果:
2025-07-22
表示在原来的日期基础上增加 7 天。
相反:
SELECT DATE_SUB(
'2025-07-15',
INTERVAL 7 DAY
);
结果:
2025-07-08
表示减少 7 天。
这里的 INTERVAL 表示时间间隔,除了 DAY,还可以写:
INTERVAL 1 MONTH
INTERVAL 1 YEAR
INTERVAL 12 HOUR
INTERVAL 30 MINUTE
因此,无论是计算未来日期还是历史日期,都可以通过这两个函数完成。
五、DATEDIFF():计算两个日期之间相差多少天
在所有日期函数中,DATEDIFF() 可以说是 LeetCode SQL 中出镜率最高的函数之一。很多首次登录、留存分析、连续登录等经典题目,本质上都离不开它。
DATEDIFF() 的作用非常简单,就是计算两个日期之间相差的天数,其语法如下:
DATEDIFF(date1, date2)
需要注意的是,它计算的是 date1 - date2 的结果,因此参数的顺序十分重要。
例如:
SELECT DATEDIFF('2025-07-15', '2025-07-10');
执行结果为:
5
表示 2025 年 7 月 15 日比 2025 年 7 月 10 日晚 5 天。
如果交换参数的位置:
SELECT DATEDIFF('2025-07-10', '2025-07-15');
得到的结果则是:
-5
因此,很多初学者第一次使用 DATEDIFF() 时都会因为参数顺序写反,导致计算结果出现负数。记住一个简单的方法即可:谁减谁,就把谁放在前面。
DATEDIFF() 在 LeetCode 中的三个经典应用
虽然 DATEDIFF() 看起来只是一个简单的日期函数,但它在 LeetCode 中几乎贯穿了所有日期分析题。总结下来,最常见的应用主要有以下三类。
1. 判断两个日期是否连续
例如,用户的两次登录日期分别为:
| 登录日期 |
|---|
| 2025-07-10 |
| 2025-07-11 |
可以直接计算:
SELECT DATEDIFF('2025-07-11', '2025-07-10');
结果为:
1
说明这两次登录之间正好相差一天,因此可以认为它们是连续登录。
如果结果大于 1,则表示中间存在间隔;如果结果为 0,则说明是同一天登录。
2. 判断是否属于次日留存
留存分析是互联网数据分析中最经典的场景之一。
假设某位用户第一次登录日期为:
2025-07-10
第二次登录日期为:
2025-07-11
只需要计算:
DATEDIFF(second_login, first_login)
如果结果等于 1 ,就说明该用户在首次登录后的第二天再次登录,也就是通常所说的次日留存用户。
事实上,大多数 LeetCode 留存分析题,本质上都是利用 DATEDIFF() 判断日期差值是否满足指定条件。
3. 计算两个业务事件之间的时间间隔
除了登录分析之外,DATEDIFF() 还经常用于计算两个业务事件之间的时间差。
例如,订单日期为:
2025-07-15
发货日期为:
2025-07-18
可以直接计算:
SELECT DATEDIFF(ship_date, order_date);
结果为:
3
表示从下单到发货一共经历了 3 天。
这种写法在订单分析、物流分析、项目管理等实际业务中都十分常见。
六、DATE_FORMAT():按照指定格式输出日期
前面介绍的日期函数主要用于计算日期 ,而 DATE_FORMAT() 的作用则是格式化日期。
很多时候,数据库中的日期格式并不适合直接展示。例如:
2025-07-15
在报表中,我们可能希望显示为:
2025-07
用于按月份统计;
或者显示为:
07/15
用于界面展示。
这时候就需要使用 DATE_FORMAT()。
例如:
SELECT DATE_FORMAT(order_date, '%Y-%m')
FROM Orders;
得到的结果为:
2025-07
如果希望显示为月/日格式:
SELECT DATE_FORMAT(order_date, '%m/%d')
FROM Orders;
则会得到:
07/15
MySQL 中常见的日期格式符如下。
| 格式符 | 含义 |
|---|---|
%Y |
四位年份,例如 2025 |
%y |
两位年份,例如 25 |
%m |
两位月份 |
%d |
两位日期 |
%H |
小时(24 小时制) |
%i |
分钟 |
%s |
秒 |
虽然 DATE_FORMAT() 在 LeetCode 中出现得并不算多,但在企业开发中却是生成报表时最常见的函数之一。
例如,公司希望统计每个月的销售额,通常都会写成:
SELECT
DATE_FORMAT(order_date,'%Y-%m') AS month,
SUM(amount)
FROM Orders
GROUP BY DATE_FORMAT(order_date,'%Y-%m');
这样得到的结果就是按月份汇总后的销售数据。
七、LeetCode 高频题型一:首次登录
首次登录几乎是所有日期分析题的起点,也是 LeetCode 中出现频率最高的经典题型之一。
假设有如下登录记录:
| player_id | event_date |
|---|---|
| 1 | 2025-01-01 |
| 1 | 2025-01-03 |
| 1 | 2025-01-08 |
| 2 | 2025-02-01 |
现在要求查询每位玩家第一次登录的日期。
很多初学者第一反应是按照日期排序,再取第一条记录。实际上完全没有必要,因为日期本身就可以比较大小。
对于这种需求,最直接的方法就是使用聚合函数 MIN():
SELECT
player_id,
MIN(event_date) AS first_login
FROM Activity
GROUP BY player_id;
这里最容易遗漏的地方就是 GROUP BY player_id。
因为我们需要的是每位玩家 的第一次登录,而不是所有玩家中最早的一次登录。如果不按照玩家分组,MIN(event_date) 只会返回整张表中最早的一条记录。
为什么不能使用 MIN(id)?
很多同学在刷题时会犯一个错误,他们认为:
第一条登录记录对应的一定是最小的 id。
这种想法其实并没有任何保证。
例如:
| id | event_date |
|---|---|
| 100 | 2025-01-01 |
| 2 | 2025-01-03 |
虽然第二条记录的 id 更小,但它对应的日期却更晚。
因此,只要题目涉及首次登录、第一次下单、最早访问等时间概念,都应该使用:
MIN(event_date)
而不是:
MIN(id)
这是 SQL 日期题中最容易出现的错误之一。
八、LeetCode 高频题型二:次日留存
除了首次登录之外,另一类非常经典的日期分析题就是次日留存(Retention)。
题目通常会描述为:
有多少玩家在首次登录后的第二天再次登录?
这类题虽然看起来复杂,但解题思路几乎是固定的。
第一步,统计每位玩家第一次登录的日期。这一步通常使用 MIN(event_date) 配合 GROUP BY player_id 即可完成。
第二步,将首次登录日期与原始登录记录进行关联。这样,每一次登录记录都能够对应到该玩家的首次登录日期。
第三步,利用 DATEDIFF() 计算当前登录日期与首次登录日期之间相差多少天:
DATEDIFF(event_date, first_login)
如果计算结果等于 1,说明当前登录发生在首次登录后的第二天,该用户属于次日留存用户;如果结果不是 1,则说明这次登录并不是首次登录后的第二天。
因此,这类题真正考察的并不是某一个日期函数,而是下面这一整套分析流程:
MIN() → GROUP BY → JOIN → DATEDIFF()
只要掌握了这一套思路,大多数留存分析题目都可以按照类似的方法解决,而不需要死记具体 SQL。
九、LeetCode 高频题型三:连续日期问题
相比首次登录和次日留存,连续日期问题的难度会更高一些,因为它不仅需要比较两个日期之间的关系,还需要识别一段连续的时间区间。
例如,下面是一位用户的登录记录:
| player_id | event_date |
|---|---|
| 1 | 2025-07-10 |
| 1 | 2025-07-11 |
| 1 | 2025-07-12 |
| 1 | 2025-07-15 |
| 1 | 2025-07-16 |
观察数据可以发现,前面三天构成了一段连续登录记录,而后面两天又构成了另一段连续登录记录。
很多初学者遇到这种题时,第一反应都是两两比较日期,例如计算:
DATEDIFF(current_date, previous_date)
如果结果等于 1,就认为两次登录是连续的;否则就认为连续登录中断。
这种方法虽然能够判断两条记录是否连续,但如果题目要求找出连续登录三天以上的用户 ,或者统计最长连续登录天数 ,仅仅依靠 DATEDIFF() 就远远不够了。
因此,LeetCode 中这类题通常都会结合窗口函数一起解决。
利用 LAG() 获取上一条记录
假设登录记录已经按照日期排序,我们可以利用 LAG() 获取上一条登录日期:
SELECT
player_id,
event_date,
LAG(event_date) OVER (
PARTITION BY player_id
ORDER BY event_date
) AS previous_date
FROM Activity;
查询结果如下:
| player_id | event_date | previous_date |
|---|---|---|
| 1 | 2025-07-10 | NULL |
| 1 | 2025-07-11 | 2025-07-10 |
| 1 | 2025-07-12 | 2025-07-11 |
| 1 | 2025-07-15 | 2025-07-12 |
| 1 | 2025-07-16 | 2025-07-15 |
有了上一条登录日期之后,只需要计算:
DATEDIFF(event_date, previous_date)
如果结果等于 1,就说明当前记录与上一条记录是连续的;如果结果大于 1,则说明连续登录已经被打断。
这种写法不仅逻辑清晰,而且在窗口函数专题中也会频繁出现,因此建议提前熟悉这种分析思路。
连续日期为什么经常和 ROW_NUMBER() 一起出现?
除了 LAG(),还有一种更加经典的解法,就是日期减去行号(Date - Row Number)。
这也是许多 LeetCode Hard 题以及互联网大厂 SQL 面试中最喜欢考察的方法。
假设数据如下:
| event_date |
|---|
| 2025-07-10 |
| 2025-07-11 |
| 2025-07-12 |
| 2025-07-15 |
| 2025-07-16 |
首先,为每条记录生成一个连续的编号:
| event_date | row_number |
|---|---|
| 2025-07-10 | 1 |
| 2025-07-11 | 2 |
| 2025-07-12 | 3 |
| 2025-07-15 | 4 |
| 2025-07-16 | 5 |
接着,分别计算:
event_date - row_number
得到:
| event_date | row_number | event_date - row_number |
|---|---|---|
| 2025-07-10 | 1 | 2025-07-09 |
| 2025-07-11 | 2 | 2025-07-09 |
| 2025-07-12 | 3 | 2025-07-09 |
| 2025-07-15 | 4 | 2025-07-11 |
| 2025-07-16 | 5 | 2025-07-11 |
你会发现,一个非常有趣的现象:
-
第一段连续日期计算后的结果完全相同;
-
第二段连续日期计算后的结果也完全相同。
因此,只需要按照 event_date - row_number 分组,就能够自动识别出每一段连续日期。
这也是 LeetCode 中解决连续日期问题最经典的技巧之一。
十、日期查询中的性能优化
日期函数不仅要会使用,还要注意性能。
很多初学者写 SQL 时,经常会写出下面这样的语句:
SELECT *
FROM Orders
WHERE YEAR(order_date) = 2025;
从功能上来看,这条 SQL 完全正确,但它存在一个潜在问题:索引可能失效。
原因在于,数据库需要先对每一条记录执行 YEAR(order_date),然后再进行比较,这意味着无法直接利用 order_date 上的索引。
更加推荐的写法是使用日期范围:
SELECT *
FROM Orders
WHERE order_date >= '2025-01-01'
AND order_date < '2026-01-01';
这种写法不仅逻辑更加清晰,而且数据库能够直接利用索引进行范围扫描,在数据量较大的情况下性能优势非常明显。
同样地,查询某个月的数据时,也建议使用范围条件,而不是:
MONTH(order_date) = 2
正确写法应当是:
WHERE order_date >= '2025-02-01'
AND order_date < '2025-03-01'
这也是企业开发中的一种最佳实践。
十一、面试高频问题总结
学习完日期函数之后,下面几个问题几乎是 SQL 面试中的高频考点。
1. DATEDIFF() 两个参数的顺序可以交换吗?
可以交换,但结果的正负号会发生变化,因此必须明确是谁减谁。
2. 为什么首次登录要使用 MIN(event_date),而不是 MIN(id)?
因为主键 ID 与时间没有必然联系,只有日期字段才能准确反映事件发生的先后顺序。
3. 查询某一年数据时,为什么不推荐使用 YEAR(order_date)=2025?
因为对字段使用函数可能导致索引失效,通常推荐使用日期范围查询。
4. 连续日期问题有哪些经典解法?
主要有两种:
-
LAG()+DATEDIFF(); -
ROW_NUMBER()+ 日期减行号分组。
前者更容易理解,后者则是 LeetCode Hard 题和大厂面试中最经典的方法。
本章总结
日期函数虽然数量不多,但却是 SQL 数据分析中最重要的知识点之一。
本章我们系统学习了 MySQL 中最常用的日期处理方法,包括:
-
CURDATE()与NOW()的区别; -
YEAR()、MONTH()、DAY()等日期提取函数; -
DATE_ADD()与DATE_SUB()的日期计算; -
DATEDIFF()的典型应用场景; -
DATE_FORMAT()的日期格式化; -
首次登录、次日留存、连续日期等 LeetCode 高频题型;
-
日期查询中的性能优化技巧。
掌握这些内容之后,你已经能够解决 SQL50 中绝大多数日期相关题目。
下一篇,我们将进入另一个高频专题------CASE WHEN 与条件判断 。我们不仅会介绍 CASE WHEN 的基本语法,还会结合条件聚合、统计分析和 LeetCode 高频题,讲解它在实际开发中的各种应用。