日期函数与日期处理专题一篇彻底掌握 SQL 日期计算前置阅读《08LIKE 与 REGEXP一篇彻底掌握 SQL 字符串匹配》前言如果说字符串处理考验的是对函数和正则表达式的理解那么日期处理考验的就是数据分析能力。在实际开发中几乎所有业务系统都会涉及日期相关的统计。例如统计今天新增了多少用户查询最近 7 天的订单计算用户连续登录天数统计每个月的销售额判断用户是否在第二天再次登录。这些需求看似不同但背后使用的都是 SQL 的日期函数。LeetCode SQL50 中日期相关的题目数量并不算多但难度普遍高于普通查询。很多题目真正困难的地方并不是 SQL 语法而是如何把日期转换成可以计算的数据。因此这一篇文章不会只是介绍几个日期函数而是结合 LeetCode 高频题系统讲解日期处理的完整思路。读完之后你不仅能够熟练使用YEAR()、MONTH()、DATEDIFF()等函数还能够独立分析首次登录、连续登录、留存分析等经典问题。一、SQL 中常见的日期类型在开始学习日期函数之前我们首先需要了解 MySQL 中最常见的三种日期类型。类型示例是否包含时间DATE2025-07-15否DATETIME2025-07-15 14:30:20是TIMESTAMP2025-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-102025-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_idevent_date12025-01-0112025-01-0312025-01-0822025-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。这种想法其实并没有任何保证。例如idevent_date1002025-01-0122025-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_idevent_date12025-07-1012025-07-1112025-07-1212025-07-1512025-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_idevent_dateprevious_date12025-07-10NULL12025-07-112025-07-1012025-07-122025-07-1112025-07-152025-07-1212025-07-162025-07-15有了上一条登录日期之后只需要计算DATEDIFF(event_date, previous_date)如果结果等于 1就说明当前记录与上一条记录是连续的如果结果大于 1则说明连续登录已经被打断。这种写法不仅逻辑清晰而且在窗口函数专题中也会频繁出现因此建议提前熟悉这种分析思路。连续日期为什么经常和 ROW_NUMBER() 一起出现除了LAG()还有一种更加经典的解法就是日期减去行号Date - Row Number。这也是许多 LeetCode Hard 题以及互联网大厂 SQL 面试中最喜欢考察的方法。假设数据如下event_date2025-07-102025-07-112025-07-122025-07-152025-07-16首先为每条记录生成一个连续的编号event_daterow_number2025-07-1012025-07-1122025-07-1232025-07-1542025-07-165接着分别计算event_date - row_number得到event_daterow_numberevent_date - row_number2025-07-1012025-07-092025-07-1122025-07-092025-07-1232025-07-092025-07-1542025-07-112025-07-1652025-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 高频题讲解它在实际开发中的各种应用。