☰
MySQL日期时间函数详解:格式化、周期计算与索引优化
2026/10/13 4:34:34 网站建设 项目流程

写业务统计的人,估计都逃不过一个课题:MySQL里的日期时间处理。无论是报表要按天、按月、按周汇总,还是数据清洗要筛出“近30天新增”,又或者接口返回要“把最后登录时间格式化成年月日”,绕来绕去,最后都会绕到MySQL的日期时间处理函数上。这一篇我打算把常用的函数拉出来过一遍,重点讲讲它们之间的差异、容易踩的坑,以及背后到底该怎么选。内容更适合正在写业务SQL、做数据报表或者维护后台系统的朋友,新手拿来当速查手册也好,老手看能不能帮你填几个记忆盲区。

我自己的感受是,MySQL的日期时间函数并不难背,难的是“场景判断”——同一个需求,用不同的函数组合,能得出不同的结果,选错了就是你排查半天的“灵异事件”。所以这篇文章不只列函数清单,更想讲清楚每个函数适合用在哪儿、为什么不建议那么用。

1. 动手之前先搞懂:日期时间类型选错,函数全白搭

1.1 DATETIME与TIMESTAMP:一字之差带来的连锁反应

很多新手根本没管字段到底是什么类型,上来就写函数。这就像不看锅是高压锅还是电饭锅就直接定时间,很容易翻车。MySQL里和你打交道最多的两种日期时间类型是DATETIME和TIMESTAMP,虽然都能存“年月日时分秒”,但内核差异很大。

DTATETIME存储范围是1000年到9999年,占用8个字节,和时区无关。你存进去几点就是几点,拿出来是什么就是什么。TIMESTAMP的存储范围只在1970年到2038年,占用4个字节,但它有一个特性:存储时会先转成UTC,取出来时再根据当前会话的时区设置转回去。换句话说,TIMESTAMP天然带时区属性,如果业务需要跨时区展示时间,它是更合理的选择。

但TIMESTAMP的2038年问题是真的。如果你要把日期时间给一个二十年后的业务用,比如某系统的“生效日期”选在2050年,用TIMESTAMP就直接报错。实际开发中,业务时间字段我一般默认用DATETIME,只有类似“记录行数据最后修改时间”这类跟系统操作强相关的场景,才会考虑TIMESTAMP配合自动更新。至于说TIMESTAMP占4个字节更省空间,在小表上根本无所谓,大表才值得较真。

1.2 三种常见书写格式:为什么你的字符串被当成日期

MySQL对日期时间的输入格式非常宽容,但宽容的另一面就是“猜”。你写一个字符串进去,它内部会尝试把字符串解析成日期,比如'2024-01-15'是合法的,'2024/01/15'也合法,'20240115'一样合法。这看起来很便利,一旦你的字符串不是标准格式,比如'2024.01.15'或者'15/01/2024',MySQL的解析规则就会产生歧义甚至直接报错。

所以在写SQL的时候,凡是涉及日期时间常量,我强烈建议只用'YYYY-MM-DD HH:MM:SS'这一种标准格式。这不是保守,这是避免让自己陷入“隐式转换”的泥潭。比如说WHERE create_time > '2024/01/15',在有的版本里能查到数据,在有的版本里就出现“Incorrect datetime value”警告,原因就是它按默认规则解析失败后走了别的路子。这个我后面还会再提到。

还有一类特殊的短日期写法:'2024-1-5',MySQL也能解析,但如果你的程序平台做了严格的字符串校验,这种写法也可能被拦在外面。宁可多写两个零,也别为了省字符给自己埋雷。

2. 取时间、格式化、字符串解析:最常用的函数细节拆解

2.1 当前时间怎么取?NOW()与SYSDATE()的微妙差异

获取当前日期时间,大家第一反应肯定都是NOW(),它返回一个YYYY-MM-DD HH:MM:SS格式的值。但我要先提一个容易被忽略的点:NOW()在一条SQL语句执行期间是固定的。不管你在同一个INSERT里调用几次NOW(),结果都一样。这其实很好用,比如要在订单主表和明细表里写入同一批时间,两条SQL都用NOW(),时间就完全一致,不会出现前后差了0.0几秒的情况。

而SYSDATE()不同,它默认返回的是“函数真正执行那一刻”的时间。在同一语句中,两次调用SYSDATE()值都可能不一样。大多数业务场景里这个差异无关痛痒,但在复制架构下可能出问题:主库语句里用SYSDATE()生成的值,和从库执行时重新计算的值对不上,会导致主从数据不一致。DBA们一般会建议把sysdate_is_now参数设为ON,把SYSDATE()强制变成NOW()的别名。所以我的建议很简单:统一用NOW(),别给自己找事。

顺带提一下CURDATE()和CURTIME(),它们分别只取日期部分和只取时间部分。如果你要“今天零点到现在的数据”,用CURDATE()配合DATE_FORMAT或者直接拼接字符串都能搞定。

2.2 DATE_FORMAT格式化:报表里的大明星与性能陷阱

DATE_FORMAT(date, format)是把日期时间转成指定格式字符串的函数,它几乎是报表工具里出场率最高的函数。很多开发同学拿它显示“YYYY年MM月DD日”,或者“只显示年月”,都很顺手。格式符很零碎,我把高频的几个列一下:

格式符含义示例
%Y四位年份2024
%m两位月份01
%d两位日15
%H24小时制小时23
%i分钟59
%s秒09
%W星期几的英文全称Monday
%a星期几的英文缩写Mon
%M月份的英文全称January
%pAM或PMPM

注意分钟用的是%i,不是%m,这是新手最容易写错的地方。你写%m拿到的实际是月份,放在时间格式里就会输出一个奇怪的数字而不是分钟。另外,格式化后是一个字符串,如果你要拿它去和另一个日期字符串做比较,请确保两边的格式完全一致,否则比较就没有意义。

我要说一个性能问题:WHERE DATE_FORMAT(create_time, '%Y-%m-%d') = '2024-01-15'这种写法,很多人觉得没毛病,但它在create_time有索引的情况下也完全走不了索引。因为函数把列值包裹住之后,MySQL无法直接利用索引进行范围扫描,只能每行都算一遍再过滤。正确做法是写成范围条件:create_time >= '2024-01-15 00:00:00' AND create_time < '2024-01-16 00:00:00'。这个坑我见得太多了,几乎每个季度报表翻车现场都能看到它。

2.3 STR_TO_DATE与CAST:把字符串变成日期的三种路径

如果说DATE_FORMAT是“日期转字符串”,那STR_TO_DATE(str, format)就是它的逆操作:把一个字符串按照你指定的格式解析成日期。这个函数尤其适合处理那些不规范的输入,比如前端传过来的'15/01/2024'或者'2024年1月15日',你可以明确告诉MySQL它该怎么读:

SELECT STR_TO_DATE('15/01/2024', '%d/%m/%Y'); -- 返回 2024-01-15 SELECT STR_TO_DATE('2024年1月15日', '%Y年%m月%d日'); -- 返回 2024-01-15

这里的关键点在于,格式串必须和输入字符串完全匹配,否则返回NULL而不是报错。我曾经排查过一个数据导入问题,就是因为某个月的月份是“1月”不是“01月”,STR_TO_DATE匹配不上,整批记录全部变成NULL,最后清理了一晚上脏数据。所以导入前先在SELECT里跑一遍函数确认结果,比啥都强。

CAST('2024-01-15' AS DATE)则是一种更朴素的转换方式,它适合处理MySQL已经能识别的标准格式。如果你拿一个不标准的字符串去CAST,得到的往往不是NULL而是“0000-00-00”,这个结果在严格SQL模式下会直接报错,在非严格模式下则会把脏数据写进表里。就经验来说,我优先用STR_TO_DATE处理非标准格式,用CAST处理标准格式,绝不让MySQL靠猜来决定怎么解析。

3. 时间加减与区间计算:统计周期和水位判断的核心

3.1 DATE_ADD与DATE_SUB:加减时间的标准姿势

做统计的时候,最常见的需求是“算昨天”“算上周一”“算上个月”。MySQL给出了一组很对称的函数:DATE_ADD(date, INTERVAL expr unit)和DATE_SUB(date, INTERVAL expr unit)。其实DATE_SUB完全可以用DATE_ADD加负值代替,但后者的语义更清晰,团队成员读起来不费劲。

INTERVAL后面可以跟很多单位,我常用的有:YEAR、QUARTER、MONTH、WEEK、DAY、HOUR、MINUTE、SECOND,甚至可以组合成INTERVAL '1-2' YEAR_MONTH这种复合单位。简单做几天加减用INTERVAL 7 DAY就够了,没必要去记复合写法。例:

SELECT DATE_ADD('2024-01-15 10:00:00', INTERVAL 2 DAY); -- 返回 2024-01-17 10:00:00 SELECT DATE_SUB(CURDATE(), INTERVAL 1 MONTH); -- 返回 2023-12-15(假设当天是1月15日)

有个行为必须强调:DATE_ADD('2024-01-31', INTERVAL 1 MONTH)执行结果是2024-02-29。MySQL不会因为你从1月31日加一个月就报错,它会把结果截断到2月的最后一天。这个“末尾溢出”行为对账务系统来说可能是大问题,利息、租期、会员时长这类需要精确对日的业务,必须自己在应用层先做判断,别指望MySQL帮你按“自然月”补全。

3.2 DATEDIFF与TIMESTAMPDIFF:算间隔别用错方向

DATEDIFF(expr1, expr2)返回的是expr1 - expr2日期的天数差,只精确到天,不管时间部分是多少。比如:

SELECT DATEDIFF('2024-01-10 23:59:59', '2024-01-01 00:00:01'); -- 返回 9,因为只比较日期部分

对于只需要“相差几天”的场景,比如用户连续登录天数、任务超期天数,DATEDIFF简单直接。但它有个让人挠头的问题:结果永远是天数,你拿不到“1天半”“3小时”这种带细分单位的差值。要细分就得用TIMESTAMPDIFF(unit, datetime_expr1, datetime_expr2),它的单位可以是SECOND、MINUTE、HOUR、DAY、MONTH、YEAR等,而且返回的是datetime_expr2 - datetime_expr1的结果,注意顺序!

SELECT TIMESTAMPDIFF(HOUR, '2024-01-01 08:00:00', '2024-01-01 20:30:00'); -- 返回 12,也就是 20:30 - 08:00 的小时数

我把参数顺序颠倒的坑提醒一下:如果你写成TIMESTAMPDIFF(HOUR, 后面那个, 前面那个),结果就是负值。这个函数我反正是每次都先念一遍“后面减前面”,因为之前有次统计用户最近一次操作距离现在多久,顺序写反,报表里全是负数,逼得我查了半天才发现是函数参数问题而非数据问题。

3.3 月初月末、周一判断、季度切分:统计周期怎么算

报表系统最常用的就是各种周期切分点。月初很简单,用DATE_FORMAT(CURDATE(), '%Y-%m-01')拼一个出来,或者DATE_SUB(CURDATE(), INTERVAL DAYOFMONTH(CURDATE()) - 1 DAY)。月末直接用LAST_DAY(CURDATE()),这函数返回当月最后一天,配合DATE_FORMAT再去拼时间范围特别方便。

我实际更想说的是星期的算法。很多中国业务把周一当一周的第一天,但MySQL的DAYOFWEEK()返回的却是1=周日,2=周一,这跟多数人的直觉是反的。如果你想把周一作为一周开头,可以配WEEKDAY(),它返回0=周一,1=周二……6=周日,和业务习惯一致。想拿到“本周一”的日期,我常用这套:

SELECT DATE_SUB(CURDATE(), INTERVAL WEEKDAY(CURDATE()) DAY);

如果今天是周三,WEEKDAY返回2,减2天就回到周一。这个方法不依赖任何语言和地区设置,全员可见代码逻辑直白。季度呢?可以用QUARTER(CURDATE())拿到当前季度编号,也可以配合DATE_FORMAT(CURDATE(), '%Y')拼出“2024-Q1”这类展示字符串。

4. 时间戳与跨时区:和Unix时间戳打交道要知道的事

4.1 UNIX_TIMESTAMP与FROM_UNIXTIME:互转时的精度陷阱

很多时候程序端传给MySQL的是Unix时间戳,也就是自1970年1月1日以来的秒数。UNIX_TIMESTAMP()返回当前时间戳,UNIX_TIMESTAMP(date)把指定日期转成时间戳,FROM_UNIXTIME(unix_timestamp)则把时间戳还原成日期时间。

看起来很简单,但精度是最大的坑。Java里经常用System.currentTimeMillis()拿到毫秒级时间戳,直接传进MySQL会发现查询结果不对劲——你会发现少了1970年那一段,或者日期时间被截断成NULL。因为MySQL的UNIX_TIMESTAMP默认是按秒去理解传入值的,你把1700000000123这种13位数当秒塞进去,它压根解析不了。正确做法是把毫秒除以1000以后再传入,或者存的时候用DATETIME而不是数字时间戳。

反过来,如果你用NOW(3)拿到了毫秒精度的日期时间,UNIX_TIMESTAMP(NOW(3))会返回一个带小数的秒数,比如1700000000.123,而FROM_UNIXTIME(1700000000.123)也能把小数部分带出来。这在毫秒级日志系统里很有用。

处理办法我总结一下:应用层负责把毫秒转成“秒(可能带小数)”,MySQL只负责秒级计算。这样最简单,也不容易出现隐式转换问题。

4.2 CONVERT_TZ与全局时区:跨时区业务怎么处理

如果你做的是面向多个时区用户的业务,存时间也要格外小心。最稳的做法是:所有的时间在数据库里统一存UTC时间,应用层展示时再按用户时区转成本地时间。MySQL侧负责时区转换的函数是CONVERT_TZ(dt, from_tz, to_tz):

SELECT CONVERT_TZ('2024-01-15 12:00:00', '+00:00', '+08:00'); -- 返回 2024-01-15 20:00:00

但CONVERT_TZ依赖MySQL的时区表。如果你的环境没加载时区表,使用像'Asia/Shanghai'这样的命名时区会直接返回NULL,只有'+08:00'这类偏移量能用。所以我在部署环境时都会先执行mysql_tzinfo_to_sql导入系统时区数据,再跑一条SELECT CONVERT_TZ('2024-01-01', 'UTC', 'Asia/Shanghai')验证结果,不是NULL才算OK。

这里还有一层:连接会话的time_zone参数也会影响NOW()等函数的输出。如果你的连接串里设置了serverTimezone=Asia/Shanghai,而MySQL全局时区是UTC,就会发生“同一个NOW(),不同客户端看出不同时间”的怪象。排查方法很简单:先SELECT @@global.time_zone, @@session.time_zone,搞清楚两端设置,再判断问题出在哪一层。

5. 实战翻车现场:常见问题与排查技巧速查

5.1 索引失效问题:函数包裹字段后为什么这么慢

说实话,日期时间函数本身计算都很快,真正的性能杀手几乎都是“索引失效”。最常见的一幕是:某天统计报表突然变慢,一查慢SQL,发现是这样写的:

WHERE DATE_FORMAT(create_time, '%Y-%m-%d') = '2024-01-15'

只要函数包裹了create_time,即使该字段上有索引,优化器也无法按索引范围查找,只能拿起全表扫描。类似的还有YEAR(create_time) = 2024、MONTH(create_time) = 1,都会遇到同一个问题。将条件改成范围查询是最直接的处理:

WHERE create_time >= '2024-01-15 00:00:00' AND create_time < '2024-01-16 00:00:00'

如果你用的MySQL版本是8.0以上,也可以考虑函数索引,比如CREATE INDEX idx_create_time_date ON t ((DATE_FORMAT(create_time, '%Y-%m-%d'))),但无论从维护成本还是可读性上讲,我都更建议先改成范围查询。等真的遇到性能瓶颈再考虑函数索引也不迟。

顺带提一个反向场景:不要在字段和常量之间做隐式类型转换,比如把varchar日期字段和DATETIME常量比较,MySQL大多数时候会尝试把字符串转日期,但如果字符串格式不干净,转换规则就可能让你拿不到预期数据。类型和格式都要对齐,这是写SQL的基本素养。

5.2 边界问题:BETWEEN AND为什么不推荐

有一种查询写法特别容易“差一条数据”:用BETWEEN AND筛选日期范围。

WHERE create_time BETWEEN '2024-01-01' AND '2024-01-31'

这条SQL看起来很顺眼,但'2024-01-31'会被解析成2024-01-31 00:00:00,所以凡是1月31日当天任何“晚于零点”的数据,全部查不出来。对于日粒度业务,漏掉这么一天既是逻辑错误,也会让报表和实际数据对不上。

要解决就两条路:要么把右边界写成'2024-01-31 23:59:59',这样能覆盖当天最后一秒,但不建议,因为23:59:59之后还有微秒;要么就用我前面说的>=配合<下一个零点:

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

这种写法把所有情况都包含在内,边界严谨,也没有额外计算开销,是目前我见到的最稳妥的方案。

5.3 字符串年份和两位数年份的坑:数据里混入的“元凶”

再分享一个我排查了很久的坑:数据导入时,有人把日期存成了'24-01-15'这种两位数年份。MySQL的解析规则会把'00'到'69'之间的两位数年份解析为2000-2069年,把'70'到'99'解析为1970-1999年。也就是说,一个'24-01-15'会被当成2024年,而'70-01-15'被当成1970年。这类数据混在表中,平时看不出来,一旦你按年做统计,1970年凭空多出一撮数据,你又得去查导入日志。

这类问题最好的解决时机是在接入数据的环节,用STR_TO_DATE显式指定格式,比如STR_TO_DATE(input, '%y-%m-%d'),明确告诉MySQL你要按两位数年份解析。如果数据已经混进去了,就要先用查询把异常数据捞出来,再统一清洗。

5.4 手工测试时的时间敏感SQL:别被NOW()骗了

最后说一个比较隐蔽的测试陷阱。如果你在测试环境直接跑SELECT NOW(),发现跟系统时间差了几个小时,先别急着怀疑MySQL,先检查你的JDBC连接串、系统时区、全局时区三者的设置是否一致。很多时候应用容器和数据库服务器时区不一致,而连接参数又没有显式指定时区,MySQL就会把当前会话时间按系统默认时区处理,看起来就像时间“跑偏”了。

我习惯在做任何跨时区项目开始时,先把所有环节的时区设置统一,并把时间字段全部按UTC存储。这样做虽然会给应用层增加转换代码,但换来的是数据口径一致,不存在“不同时区的同事看到不同结果”的扯皮问题。毕竟日期时间函数再多,也扛不住底层配置混乱。

如果你是在排查一条SQL为什么时对时错,不妨先检查一下NOW()和SYSDATE()在当前会话里到底返回了什么,然后逐个确认函数参数和日期格式。这个排查路径,比闷头改SQL高效多了。

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询