日期函数与日期处理专题,一篇彻底掌握 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/15MySQL 中常见的日期格式符如下。
| 格式符 | 含义 |
|---|---|
%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 高频题,讲解它在实际开发中的各种应用。