MySQL里的日期格式化,是我见过使用频率最高、也最容易踩坑的MySQL功能之一。无论是按月统计订单量、按小时分析日志,还是把数据库里的datetime直接导出成前端要的日期字符串,几乎每天都会碰到。今天这篇内容我从实际项目出发,把DATE_FORMAT、STR_TO_DATE、时间戳转换、格式符细节、常见报错和性能问题一次讲透,希望能帮你少走弯路。内容适合刚入门的新人,也适合用久了想系统梳理一遍的老手。
1. 为什么需要日期格式化:从业务需求到核心函数
1.1 业务场景里的日期格式化需求
一个很常见的场景是后端接口返回值。数据库里存的是datetime类型,比如“2024-06-12 14:30:00”,但前端往往只需要日期“2024-06-12”,或者只要“2024年6月12日”。如果没有在SQL里做格式化,就得在业务代码里再处理一遍,反而多一层转换,而且每张表都要写一遍类似逻辑,维护成本很高。
另一个高频场景是报表统计。管理后台要按天、按周、按月展示用户注册趋势,这时候直接拿原始的datetime去GROUP BY是没法按天分组的,必须先截取到日期部分,再聚合。这里的核心做法就是把datetime转成指定格式的字符串,或者截断到指定的时间粒度。我遇到过很多次,开发初期为了省事直接用LEFT(order_time, 10)来处理日期,结果遇到时区、格式不统一的问题时,后面越改越累。
在数据同步、数据迁移、日志清洗中,也经常要把字符串时间转成date或者datetime,这时候就得用STR_TO_DATE做反向解析。例如从第三方接口拿到的数据是“20240612”,需要插入到timestamp字段,不转换直接插入会报错,或者存成一个奇怪的0000-00-00。所以,日期格式化不仅仅是“让显示好看”,它贯穿在数据清洗、存储、计算、导出的每一个环节。你看到的可能只是一条DATE_FORMAT语句,但它背后往往连接着一条完整的数据链路。
1.2 MySQL日期格式化核心函数矩阵
MySQL中跟日期格式化关系最密切的函数有三个:DATE_FORMAT、STR_TO_DATE、UNIX_TIMESTAMP/FROM_UNIXTIME。DATE_FORMAT负责把日期和时间类型按照给定格式输出成字符串,STR_TO_DATE负责把字符串按照特定格式解析成日期类型,FROM_UNIXTIME则处理时间戳和日期字符串之间的转换。很多人以为只有DATE_FORMAT是格式化,实际上STR_TO_DATE是反向的格式化,两者配合才能解决绝大多数场景。
如果只是截取日期部分,还有DATE()、YEAR()、MONTH()、DAY()等简化函数。不过这些函数内部其实也是在做格式化运算。从可维护性讲,我建议统一用DATE_FORMAT来做展示层的格式化,这样格式串一眼就能看出意图。用YEAR/MONTH这种函数做时间粒度的提取也可以,但一旦需求变成“按周几统计”或者“按小时+分钟统计”,就得回到DATE_FORMAT。这个函数矩阵不需要死记,但要清楚每个函数的定位,避免在错误的地方用错误的函数。
| 函数 | 作用 | 返回类型 | 示例 |
|---|---|---|---|
| DATE_FORMAT(date, format) | 按格式输出日期字符串 | 字符串 | DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s') → '2024-06-12 14:30:00' |
| STR_TO_DATE(str, format) | 按格式把字符串解析为日期 | DATETIME | STR_TO_DATE('2024/06/12', '%Y/%m/%d') → 2024-06-12 |
| FROM_UNIXTIME(ts, format) | 时间戳转为格式化字符串 | 字符串 | FROM_UNIXTIME(1718175000, '%Y-%m-%d') → '2024-06-12' |
| UNIX_TIMESTAMP(date) | 日期时间转时间戳 | 整数 | UNIX_TIMESTAMP('2024-06-12 00:00:00') |
| DATE(date) | 提取日期部分 | DATE | DATE('2024-06-12 14:30:00') → '2024-06-12' |
| YEAR/MONTH/DAY | 提取年/月/日 | 整数 | YEAR('2024-06-12') → 2024 |
当然还有其他函数,但上面这张表已经覆盖了日常90%的用法。下面我们把重点放到DATE_FORMAT和STR_TO_DATE的细节上,因为细节决定成败。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 日期格式化的核心细节与格式符解析
2.1 DATE_FORMAT常用格式符对照表
DATE_FORMAT里的格式符是区分大小写的,而且大小写代表完全不同含义。这是新手最容易踩的坑,比如%M表示月份英文名,%m表示两位数字月份,%Y是四位年份,%y是两位年份,%H是24小时制,%h是12小时制,%i是分钟,%s是秒。注意:分钟用的是%i而不是%m,%m是月份,这个必须记牢。还有%p是AM/PM,配合%h使用。
我整理了一份日常够用的格式符对照表,建议直接收藏:
| 格式符 | 含义 | 示例输出 |
|---|---|---|
| %Y | 四位年份 | 2024 |
| %y | 两位年份 | 24 |
| %m | 两位月份 | 06 |
| %c | 数字月份(1-12) | 6 |
| %M | 英文月份全称 | June |
| %b | 英文月份缩写 | Jun |
| %d | 两位日 | 05 |
| %e | 数字日(1-31) | 5 |
| %H | 24小时(00-23) | 14 |
| %h | 12小时(01-12) | 02 |
| %i | 分钟(00-59) | 30 |
| %s | 秒(00-59) | 00 |
| %W | 星期英文全称 | Wednesday |
| %w | 星期数字(0=Sunday) | 3 |
| %a | 星期英文缩写 | Wed |
| %p | AM/PM | PM |
| %j | 一年中的第几天 | 164 |
| %u | 一年中的第几周(周一为一周开始) | 24 |
| %v | 一年中的第几周(周一为一周开始, 与%x配合) | 24 |
| %x | 周所属的年份 | 2024 |
这张表里,数字部分默认会补零,比如%m是06,%d是05。如果你不希望补零,就用%c和%e。实际报表里经常遇到“6月5日”这种格式,用%c和%e就对了。不要觉得类似“05”和“5”的差别无所谓,在数据导出、对接外部系统时,很多坑都是在这种细节上埋下的。
2.2 日期字符串与时间戳的互转
格式化不只是DATE_FORMAT往字符串方向转,还经常要从字符串解析成日期。STR_TO_DATE就是专门干这个的。它的语法和DATE_FORMAT完全对应,同样的一套格式符,只是方向反了。举个例子:STR_TO_DATE('2024-06-12 14:30:00', '%Y-%m-%d %H:%i:%s')返回的就是datetime类型。
这里有个实用技巧:STR_TO_DATE的解析过程并不会强校验日期的合理性。比如STR_TO_DATE('2024-02-30', '%Y-%m-%d')不会报错,而是返回NULL,业务上需要自己处理这种脏数据。另外一个坑是:如果你只解析了年月日,得到的是一个date类型;如果只解析了时分秒,则日期部分变成当前的日期(或者0000-00-00,取决于版本和SQL_MODE)。所以解析时最好显式把完整格式写全,避免隐式行为影响结果。
时间戳方向同样简单。FROM_UNIXTIME可以把整数秒数转成日期字符串,UNIX_TIMESTAMP则把一个日期表达式转成秒数。注意这里的“时间戳”默认是秒,不是毫秒。很多系统接口里存的是毫秒时间戳,需要除以1000再转。我见过不少人在这个细节上翻车,查半天数据对不上,最后发现是毫秒忘除了。处理时可以先写SELECT FROM_UNIXTIME(1718175000123 / 1000, '%Y-%m-%d')验证一下,再决定是否除法。
2.3 格式符大小写、零填充的坑
除了上面提到的%M和%m容易看混,还有几组特别容易出错。%Y和%y就不用说了,很多人把%y当成四位年份,结果输出“24”而不是“2024”。%H和%h也要注意:%h是12小时制,如果不带%p,凌晨和下午会显示成一样的数字。%i和%m是最隐蔽的组合,分钟是%i,月份是%m,两个在键盘上离得不算远,在格式串里写错后报表数据会变得非常难查。
补零的问题也要重视。默认格式符会补零,但业务上经常需要去掉前导零。比如日期显示要求“2024-6-5”,那就要用%Y-%c-%e,而不是%Y-%m-%d。还有周数相关的%u和%v,区别在于%u返回1-53,%v返回周所属的ISO年份,配合%x使用。如果公司用的是“周一作为一周开始”,并且想按ISO周计算,就别用%w。这些细节单独看都不难,组合起来容易乱,建议在使用前先在数据库里跑一条测试SQL,把输出结果打出来看一眼。
3. 实操过程:从查询到报表的日期格式化实战
3.1 按天、周、月、季度分组统计
我直接分享一组我在报表系统里常用的写法。假设你有一张订单表orders,字段有order_id、amount、order_time(datetime),现在需要统计每天、每周、每月的订单总额。天维度最直接:
sql复制SELECT DATE_FORMAT(order_time, '%Y-%m-%d') AS day, SUM(amount) AS total
FROM orders
WHERE order_time >= '2024-01-01' AND order_time < '2025-01-01'
GROUP BY day
ORDER BY day;
注意GROUP BY day可以直接引用SELECT里的别名,MySQL允许这样用。但WHERE里不能使用别名,所以我还是把过滤条件写成了原始范围。周维度稍微特殊一点,因为周几作为一周开始在不同业务里定义不同。如果按周一为一周开始,可以用:
sql复制SELECT DATE_FORMAT(DATE_SUB(order_time, INTERVAL WEEKDAY(order_time) DAY), '%Y-%m-%d') AS week_start,
SUM(amount) AS total
FROM orders
GROUP BY week_start;
这里WEEKDAY(order_time)返回0到6,0表示周一。DATE_SUB把这个日期挪到本周一,然后再格式化。你也可以用YEARWEEK(order_time, 1)来计算,但是结果形如202424,可读性不如上面的方案。月维度直接DATE_FORMAT(order_time, '%Y-%m')就完了。季度维度没有内置格式符,需要配合QUARTER()函数或者自己写CASE,比如:
sql复制SELECT CONCAT(YEAR(order_time), '-Q', QUARTER(order_time)) AS quarter,
SUM(amount) AS total
FROM orders
GROUP BY quarter;
这种写法最大的好处是语义清晰,而且能直接在SQL里完成报表数据,不需要拉到应用层再处理。遇到跨年数据时,可以自己在WHERE里加年份过滤,或统一在年份维度上再做汇总。
3.2 日期格式化配合条件筛选
很多人以为格式化只能用SELECT列表里,其实它也能用在WHERE条件里做筛选。例如查出所有凌晨0点到6点之间的订单:
sql复制SELECT * FROM orders
WHERE DATE_FORMAT(order_time, '%H') BETWEEN '00' AND '05';
但这种写法有个大问题:DATE_FORMAT(order_time, '%H')会让order_time上的索引失效。表数据量小无所谓,一旦订单表千万级,这条SQL会全表扫描,响应时间直接爆炸。更推荐用原生的时间范围判断:
sql复制SELECT * FROM orders
WHERE order_time >= CONCAT(CURDATE(), ' 00:00:00')
AND order_time < CONCAT(CURDATE(), ' 06:00:00');
这样order_time的索引能正常用到。如果确实需要在某个粒度上统计,也尽量先把范围缩小,再在结果集上做格式化。格式化函数的开销不只是CPU,还有索引利用率。这是很多人写SQL时忽略的性能点。
另外一个容易忽略的场景是动态日期范围。比如统计“最近7天”的订单,有人会写DATE_FORMAT(NOW(), '%Y-%m-%d')再减7,这种其实没问题,但更直接的是用DATE_SUB(NOW(), INTERVAL 7 DAY)作为过滤条件。我倾向于把“格式化”和“范围计算”分开,格式化负责显示,范围计算负责过滤,组合使用时语义更清楚。
3.3 性能优化:格式化是否影响索引
上面的例子已经说明了,在WHERE条件中用DATE_FORMAT会导致索引失效,因为MySQL无法对函数表达式做范围匹配。但有一种例外:MySQL 8.0支持函数索引(Generated Column + Index),也就是把DATE_FORMAT(order_time, '%Y-%m')存成虚拟列,然后在这个虚拟列上建索引。如果你真的高频按天/月查询,可以考虑这个方案。但老实说,绝大多数场景用原生时间范围条件就能解决,没必要为了格式化专门建函数索引,维护成本不低。
在SELECT列表里做格式化对索引影响不大,但如果只是需要日期部分,用DATE(order_time)可能比DATE_FORMAT(order_time, '%Y-%m-%d')效率略高一点点,因为DATE函数有更简单的内部实现。不过这个差距在单条SQL里几乎感知不到。关键是养成一个习惯:需要按时间范围过滤时,把格式化的操作放到数据量已经缩小之后。比如先拉出最近一个月的数据,再在内存层面处理格式,能够避免很多慢查询。
另外,GROUP BY中直接使用格式化表达式也会让MySQL做临时表,因为排序和分组需要计算后的结果。如果分组字段很多,可以考虑把格式化结果先写入临时表或中间表,再在中间表上继续聚合,能明显减少重复计算。这个技巧在跑月度报表时非常实用,尤其是数据量达到百万级以上,效果一眼可见。
4. 常见问题与排查技巧实录
4.1 常见格式化报错及原因
我踩过的坑里,最经典的是格式串里用了双引号而不是单引号,或者少了%号。特别是从其他语言复制过来的字符串,很容易把%i写成i。下面是我整理的几个高频报错:
| 报错信息 | 常见原因 |
|---|---|
| Incorrect datetime value: '...' | 字符串格式和STR_TO_DATE的格式串不匹配 |
| Data truncation: Incorrect date value: '...' | 日期字段被赋了非法字符串,通常是日期格式不对 |
| FUNCTION database.DATE_FORMAT does not exist | 函数名拼错,或者当前版本的MySQL不支持 |
| Unknown system variable 'sql_mode' | 迁移环境配置问题,和日期格式化本身无关但会干扰 |
第一个报错最典型。例如日期字符串里是“2024/06/12”,你用的格式串是“%Y-%m-%d”,那解析就会失败。排查思路很简单:先用SELECT STR_TO_DATE('你的字符串', '对应的格式串')单独测,看是不是返回NULL,再结合业务代码看格式串到底传的什么。
第二个报错多发生在直接插入字符串日期时。比如字段是datetime,插入“2024-6-5 8:30:00”虽然MySQL经常能自动转,但一旦SQL_MODE开了NO_ZERO_DATE或STRICT_TRANS_TABLES,就会非常严格。所以我一直建议,所有业务代码里插入日期时间都统一用标准格式:YYYY-MM-DD HH:MM:SS,避免依赖MySQL的隐式转换。日期格式化不是SQL标准的一部分,不同数据库行为有差异,尽早规范能减少跨库迁移的阻力。
4.2 时区与默认值导致的显示偏差
MySQL的DATE_FORMAT只是原样格式化存储的时间,不会做时区转换。如果服务器的时区是UTC,而业务在北京时间UTC+8,那直接DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s')得到的就是UTC时间,和用户本地时间差了8小时。这个问题不在格式化本身,而是数据库连接时区设置。
解决办法是在连接串里指定time_zone参数,或者在MySQL配置里设置default-time-zone。例如JDBC连接时加上serverTimezone=Asia/Shanghai。同时,代码里获取当前时间不要用NOW(),最好由数据库统一维护插入时间的字段(DEFAULT CURRENT_TIMESTAMP),展示时再按前端需要的时区格式化。日期格式化函数本身是无状态的,它不负责时区换算,这个定位要搞清楚。
我在生产环境遇到过一个问题:同一条SQL,在测试环境时间完全正常,到生产环境就少了8小时。最后发现是MySQL实例的time_zone不同。因此在排查“日期差8小时”时,先跑SELECT NOW()看看数据库当前时间是不是和预期一致。如果NOW()正常,再检查客户端连接是否覆盖了时区参数。顺序排查下来能少走很多弯路。
4.3 DATE_FORMAT与STR_TO_DATE的常见误用
DATE_FORMAT和STR_TO_DATE方向不同,但格式符通用。常见误用是把DATE_FORMAT用在字符串解析上,比如DATE_FORMAT('2024-06-12', '%Y-%m'),这其实也能返回“2024-06”,因为MySQL会隐式把字符串转成日期,但这样做并不安全。如果字符串是“2024/06/12”,DATE_FORMAT也能自动解析,因为斜杠是MySQL的合法日期分隔符。但遇到“20240612”这种紧凑格式,DATE_FORMAT会返回NULL,必须用STR_TO_DATE。
反过来,STR_TO_DATE容易把“2024年6月12日”这种中文字符串解析成NULL,因为MySQL的格式串不支持中文修饰符。实际做法是先用REPLACE把“年”“月”“日”替换成斜杠或短横线,再STR_TO_DATE。我处理第三方数据时经常写这种转换SQL:
sql复制SELECT STR_TO_DATE(REPLACE(REPLACE(REPLACE('2024年6月12日', '年', '-'), '月', '-'), '日', ''), '%Y-%m-%e');
这个例子虽然看起来啰嗦,但确是我在对接外部数据时最常用的处理方式。类似的还有把“20240612143000”转成datetime,用STR_TO_DATE(str, '%Y%m%d%H%i%s')就能一步到位,比程序里用substring截取再拼接可靠得多。
4.4 问题排查速查表
给一张速查表,方便直接对照解决。
| 问题 | 排查思路 |
|---|---|
| 格式串输出不对 | 确认大小写,分钟是%i不是%m,12小时制用%h |
| 解析结果为NULL | 用SELECT STR_TO_DATE单独测,比对实际字符串字符 |
| 日期差8小时 | 检查连接时区、服务器时区、是否用了UTC |
| 格式化后索引失效 | 不要在WHERE左侧套函数,改用范围条件 |
| 补零不符合预期 | 用%c、%e代替%m、%d去掉前导零 |
| 周数统计错位 | 确认需求里一周从周几开始,用对应格式符 |
这张表覆盖了大多数情况,排查思路其实就一句话:先把格式串单独拿出来测,再对照字符串的每一个字符。很多问题不是函数不会,而是数据本身不符合预期。我每次遇到日期相关的bug,第一件事就是写一条最小化SQL,把数据样本打出来看,基本能定位一大半。
5. 日期格式化的扩展组合与实战经验
5.1 日期格式化与日期运算函数的搭配
项目里很少有只格式化一个日期就完事的场景,更多是把日期运算和格式化揉在一起。比如统计上月订单量,用DATE_ADD(DATE_FORMAT(CURDATE(), '%Y-%m-01'), INTERVAL -1 MONTH)得到上月的第一天;用LAST_DAY(DATE_SUB(CURDATE(), INTERVAL 1 MONTH))得到上月最后一天。这样SQL就能做到“动态取上个月完整区间”,不用在代码里算日期。
sql复制-- 上月第一天
SELECT DATE_FORMAT(DATE_ADD(CURDATE(), INTERVAL -1 MONTH), '%Y-%m-01');
-- 上月最后一天
SELECT LAST_DAY(DATE_SUB(CURDATE(), INTERVAL 1 MONTH));
这种写法的好处是天然适配月末不等长的问题。2月最后一天是28或29,程序语言里要写不少逻辑,SQL里一个LAST_DAY就搞定了。再配合DATE_FORMAT,就能把月报每天自动跑出来。我经常在存储过程或定时任务里用到这一套组合,运维成本很低。
另一个高频组合是HOUR和DATE_FORMAT一起用。比如统计当天各个小时的订单量,可以写成DATE_FORMAT(order_time, '%H:00')作为小时分组。如果想看周末和工作日的差异,可以用DATE_FORMAT(order_time, '%W')直接拿到星期英文名。注意中文环境建议在应用层做映射,或者用CASE WHEN把星期英文名转成“周一”“周二”等中文,SQL里写中文映射可读性很差。
5.2 多数据库迁移时格式化函数差异
用过SQL Server或PostgreSQL的同学应该体会很深,格式化函数在不同数据库里差异巨大。SQL Server用CONVERT(varchar, getdate(), 112),PostgreSQL用TO_CHAR(now(), 'YYYY-MM-DD'),而MySQL用DATE_FORMAT。如果要把项目从SQL Server迁到MySQL,这种函数差异是会让你改SQL改到头大的地方。我的建议是尽量在SQL里少用格式化函数,把展示层的格式化工作交给业务代码,比如Java的LocalDateTime格式化、Python的datetime.strftime,这样迁移成本会低很多。
但报表SQL很难完全避免格式化,这时候可以在应用层统一做一层“SQL模板翻译”。我自己遇到过的做法是定义了统一的时间维表,在维表里预计算好日期、周、月、季度、年等字段,业务查询直接JOIN维表,彻底摆脱手工DATE_FORMAT。这样虽然建表麻烦一点,但查询性能和可维护性都非常好,值得在数仓项目里尝试。
如果你正在做数据库选型,还要考虑一个隐性问题:不同数据库对非法日期的容忍度差别很大。MySQL默认允许某些宽松的格式,但SQL Server更严格。迁移时如果日期列存在脏数据,往往不是格式化函数能解决的,而是要先把数据清洗干净。也就是说,格式化函数只是最后一公里,前面数据管好了,后面转换才省心。
5.3 我的几个实用小技巧
最后分享几个我平时用得比较多的小技巧。
第一,格式化之前先确认字段类型。CHAR和DATETIME类型都能被DATE_FORMAT处理,但遇到VARCHAR中混入非法字符时,可以先CAST或STR_TO_DATE清洗。养成类型意识能省很多排查时间。比如我经常在写查询前先跑DESC table,确认字段是datetime还是varchar,避免隐式转换带来意外结果。
第二,报表里想要“2024年第2季度”这种文案,可以写成CONCAT(YEAR(order_time), '年第', QUARTER(order_time), '季度'),比用复杂格式符直观得多。SQL本来就不擅长做文案拼接,能用简单函数就不要硬记冷门格式符。
第三,如果要在Linux命令行或MySQL客户端里查当前时间,直接用SELECT DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s'),不要用NOW()默认格式,因为默认格式会带小数秒,看着很乱。很多自动化脚本解析日期时也会因为小数秒丢失精度,标准格式化之后反而不容易出问题。
第四,批量导入日期时,用STR_TO_DATE做一次完整校验。比如在INSERT之前先跑一遍清洗SQL,把NULL或异常的记录找出来,避免最后导入阶段才发现脏数据。我通常在临时表里先做数据清洗,等数据都符合预期再插入正式表,这个流程比在程序里写复杂校验要直观得多。
第五,不要把日期格式化写进索引条件。非写不可时,优先考虑生成列索引,但更推荐的是调整查询条件用范围扫描。这个原则我前面提过,但它值得单独列出来。很多慢查询问题最后都出在习惯性在WHERE里套函数上,改成一个范围条件,性能立刻提升几个量级。
我在实际项目里用过很多数据库,MySQL的日期格式化属于那种“简单但容易出细节”的功能。每次有人问我要日期格式化工具,我都建议先别急着背格式符,而是先搞清楚业务需要的输出格式、时区、补零规则,再动手写SQL。格式符记不牢没关系,多写几次、多踩几次坑就记住了。最后再分享一个小技巧:如果你实在拿不准某个格式串解析结果,直接跑一句SELECT STR_TO_DATE/DATE_FORMAT试一下,MySQL会告诉你真实结果,比翻文档快得多。
