日期计算几乎是所有业务系统都绕不开的需求:统计近七天的订单、计算用户年龄、获取本月第一天、判断会员是否到期,这些场景都需要对日期进行加减和比较。不少开发者习惯把日期取出来放到应用程序里用代码计算,再拼回SQL查询,结果SQL越写越复杂,性能也随之下降。其实MySQL本身提供了非常完善的日期计算函数,绝大多数需求一行SQL就能解决。本文将系统梳理这些函数的用法,并结合实际业务场景给出可直接套用的示例。

一、日期加减:DATE_ADD与DATE_SUB
DATE_ADD和DATE_SUB是MySQL中做日期加减最核心的两个函数。DATE_ADD用于在日期上增加一个时间间隔,DATE_SUB则用于减去一个时间间隔。两个函数的语法完全一致,只是方向相反。
它们的第一个参数是日期值,第二个参数是一个INTERVAL表达式,格式为INTERVAL 数量 单位。单位非常丰富,包括SECOND、MINUTE、HOUR、DAY、WEEK、MONTH、QUARTER、YEAR,甚至还有组合单位如DAY_HOUR、YEAR_MONTH等。下面看几个典型用法:
-- 当前时间加3天
SELECT DATE_ADD(NOW(), INTERVAL 3 DAY);
-- 指定日期减2个月
SELECT DATE_SUB('2024-08-15', INTERVAL 2 MONTH);
-- 加1年零3个月
SELECT DATE_ADD('2024-01-31', INTERVAL '1-3' YEAR_MONTH);
-- 计算七天的截止时间
SELECT DATE_ADD(CURDATE(), INTERVAL 6 DAY);
需要注意一个细节:当对月末日期做加减时,MySQL会自动处理溢出问题。例如DATE_ADD('2024-01-31', INTERVAL 1 MONTH)的结果是2024-02-29,因为2024年是闰年,MySQL会将结果截断到该月最后一天,而不是报错或产生3月1日。这个行为在大多数业务中是符合预期的,但如果涉及账单周期这类对边界敏感的逻辑,务必提前测试。
另外,MySQL支持用+ INTERVAL 3 DAY这种简写形式替代DATE_ADD,例如SELECT NOW() + INTERVAL 3 DAY,效果完全相同,写起来更简洁。但为了代码可读性和跨版本兼容性,建议在正式项目中统一使用标准函数写法。
二、时间差计算:DATEDIFF、TIMESTAMPDIFF与TIME_TO_SEC
计算两个日期之间相差多少,是另一个高频需求。MySQL提供了几个侧重点不同的函数,用错会得到意想不到的结果。
DATEDIFF(date1, date2)返回两个日期之间相差的天数,只比较日期部分,忽略时分秒。它的计算逻辑是date1减date2,参数顺序不能搞反。比如DATEDIFF('2024-08-10', '2024-08-01')返回9,而反过来则是-9。这个函数非常适合判断两个日期是否在同一天、相差几天这类场景。
如果需要更精确的时间差,比如相差多少小时、多少分钟,就要用TIMESTAMPDIFF(unit, datetime1, datetime2)。它的第一个参数指定返回结果的单位,可以是SECOND、MINUTE、HOUR、DAY、MONTH、YEAR等。注意它的参数顺序与DATEDIFF相反,是后面减前面。看下面的对比:
-- 返回相差天数(忽略时间部分)
SELECT DATEDIFF('2024-08-10 23:00:00', '2024-08-01 00:30:00'); -- 9
-- 返回相差的小时数
SELECT TIMESTAMPDIFF(HOUR, '2024-08-01 08:00:00', '2024-08-02 10:30:00'); -- 26
-- 返回相差的秒数
SELECT TIMESTAMPDIFF(SECOND, '2024-08-01 08:00:00', '2024-08-01 08:05:30'); -- 330
-- 计算年龄:满周岁才算1年
SELECT TIMESTAMPDIFF(YEAR, '1995-06-15', CURDATE());
这里要特别提醒TIMESTAMPDIFF的一个特性:它返回的是完整单位的数量。也就是说,用YEAR单位计算年龄时,只有过了生日才会进位,这正好符合日常生活中满周岁才算几岁的习惯。而如果用YEAR函数相减来算年龄,就会出现生日未到也算大一岁的问题,这是很多新手容易踩的坑。
此外还有一个PERIOD_DIFF(P1, P2)函数用于计算两个年月之间相差的月数,参数格式是YYYYMM或YYMM,例如PERIOD_DIFF(202408, 202401)返回7,在按月统计的场景偶尔会用到。
三、日期提取与格式化:YEAR、MONTH、DAY与DATE_FORMAT
做日期计算之前,往往需要先从日期中提取出年、月、日、星期等部分。MySQL提供了一组提取函数:YEAR()返回年份,MONTH()返回月份数字,DAYOFMONTH()返回几号,DAYOFWEEK()返回星期几(1表示周日),WEEKDAY()也返回星期几(0表示周一),DAYOFYEAR()返回一年中的第几天。这些函数组合起来可以应对大多数提取需求。
DATE_FORMAT(date, format)则是格式化输出的利器。它按照指定的格式字符串把日期转成想要的样式,常用格式符包括%Y四位年份、%m两位月份、%d两位日期、%H小时、%i分钟、%s秒、%W完整英文星期名等。通过它还能巧妙地实现一些计算需求,比如获取本月第一天:
-- 获取本月第一天
SELECT DATE_FORMAT(CURDATE(), '%Y-%m-01');
-- 获取上个月的第一天和最后一天
SELECT DATE_FORMAT(DATE_SUB(CURDATE(), INTERVAL 1 MONTH), '%Y-%m-01') AS first_day,
LAST_DAY(DATE_SUB(CURDATE(), INTERVAL 1 MONTH)) AS last_day;
-- 查询本周的数据(周一为一周开始)
SELECT * FROM orders
WHERE YEARWEEK(create_time, 1) = YEARWEEK(NOW(), 1);
-- 按月分组统计订单量
SELECT DATE_FORMAT(create_time, '%Y-%m') AS month, COUNT(*) AS total
FROM orders
GROUP BY month
ORDER BY month;
上面的例子中出现了LAST_DAY()函数,它返回指定日期所在月的最后一天,配合DATE_ADD可以算出下个月第一天的日期:DATE_ADD(LAST_DAY(CURDATE()), INTERVAL 1 DAY)。YEARWEEK()则返回年份加周数的组合数字,第二个参数传1表示以周一为每周第一天,用来查询本周数据非常方便。
四、实战场景:常见业务日期查询模板
掌握了基础函数之后,来看几个可以直接套用的业务查询模板。第一个是统计近七天的数据,注意这里的近七天应该包含今天,所以条件是大于等于六天前的零点:
-- 近七天(含今天) SELECT * FROM orders WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 6 DAY) AND create_time < DATE_ADD(CURDATE(), INTERVAL 1 DAY); -- 今天的数据 SELECT * FROM orders WHERE create_time >= CURDATE() AND create_time < DATE_ADD(CURDATE(), INTERVAL 1 DAY);
这里有一个重要的性能技巧:不要对字段使用函数。如果把条件写成WHERE DATE(create_time) = CURDATE(),虽然结果一样,但对create_time列使用函数会导致索引失效,全表扫描。正确做法是把计算转移到常量一侧,让字段保持裸列状态,这样才能走索引。这个原则适用于所有日期查询。
第二个场景是判断时间是否在某个区间内,例如查询正在进行中的活动:
SELECT * FROM activities WHERE NOW() BETWEEN start_time AND end_time;
BETWEEN语法在日期比较中天然可用,因为日期和日期时间在MySQL中都是可以比较的类型。但要避免用字符串直接拼接比较,比如把日期格式化成字符串后再用LIKE匹配,这种写法既低效又容易出错。
五、日期计算中的常见陷阱
最后总结几个容易出错的地方。第一是日期格式问题:MySQL默认接受的日期字符串格式是YYYY-MM-DD或YYYY-MM-DD HH:MM:SS,如果插入中文格式的日期字符串如2024年8月1日,会直接报错或产生警告。存入数据库前务必保证格式正确。
第二是NULL值的处理。所有日期函数遇到NULL参数都会返回NULL,如果在做日期加减时字段可能为空,要么在查询前用IFNULL()做默认值兜底,要么在WHERE条件中排除NULL行,否则可能得到错误的对账结果。
第三是时区问题。NOW()和CURDATE()返回的是服务器时区的时间,如果应用服务器与数据库服务器时区不一致,或者业务需要跨时区支持,就要考虑使用UTC时间存储,或者通过SET time_zone统一会话时区,避免统计口径出现偏差。
第四是零值日期。在严格模式下,0000-00-00这样的零值日期无法直接插入或参与计算,遇到历史数据中存在零值的情况,需要先清洗数据再做日期运算,否则查询会直接报错。
总的来说,MySQL的日期函数体系相当完善,从简单的加减、差值计算到格式化、区间判断都有对应方案。写日期相关SQL时记住两个原则:能用数据库算的就不要搬到应用层算,能用常量计算的就不要对字段套函数。把这两点落实到位,日期查询的代码量和执行效率都会有明显改善。