☰
MySQL没有to_date?一文讲透日期转换与Oracle迁移避坑指南
2026/9/28 12:52:53 网站建设 项目流程

做数据库开发这些年,我见过太多从 Oracle 转 MySQL 的同事,上来就是to_date('2024-06-18', 'YYYY-MM-DD'),然后对着报错FUNCTION mydb.to_date does not exist一脸茫然。这个报错基本可以评为"Oracle 人初到 MySQL"的第一道门槛。今天这篇文章就把 MySQL 日期转换这件事彻底捋一遍:为什么 MySQL 没有 to_date(),字符串转日期应该用什么,日期转字符串怎么对应,从 Oracle 迁移过来还容易踩哪些坑。无论你是刚学 MySQL 的新手,还是带着 Oracle 习惯在老项目里挣扎的开发,读完这篇至少不会再在这类函数上报错。

1. 为什么你会找 to_date():Oracle 习惯撞上 MySQL 的第一堵墙

1.1 一个我在项目中见过的报错现场

前几年做数据平台迁移,原系统是 Oracle,新库统一换 MySQL 8.0。第一期联调当天,数据组同事跑了一个存储过程,报错信息直接甩到群里:

ERROR 1305 (42000): FUNCTION mydb.to_date does not exist

这个错误的意思不是说函数名拼错了,而是 MySQL 压根就没有注册为to_date的内置函数。同事下意识以为是大小写问题,把to_date改成TO_DATE,再跑,还是同样报错。最后翻官方文档才发现,MySQL 就没有这个函数。

这个场景特别典型,因为 Oracle、PostgreSQL、甚至部分国产数据库都有TO_DATE(str, fmt),很多从这些库迁移过来的人,第一反应就是找to_date。MySQL 的日期转换体系完全走了另一条路,它把"字符串转日期"交给了STR_TO_DATE,把"日期转字符串"交给了DATE_FORMAT。命名上更直白,但从老体系切过来的人确实要花点时间适应。

1.2 两套日期函数的设计逻辑差异

先看 Oracle 这边,日期处理围绕"字符和日期互转"设计了一对函数:

操作Oracle 写法MySQL 写法
字符串转日期TO_DATE('2024-06-18', 'YYYY-MM-DD')STR_TO_DATE('2024-06-18', '%Y-%m-%d')
日期转字符串TO_CHAR(SYSDATE, 'YYYY-MM-DD')DATE_FORMAT(NOW(), '%Y-%m-%d')

除了函数名不一样,格式符也不一样。Oracle 用YYYY、MM、DD、HH24这种写法,MySQL 用%Y、%m、%d、%H这种带百分号的写法。两者只差一个写法,但混用的时候常常不会立即报错,而是静默返回NULL,这是最容易被坑的地方。后面我会专门讲这个现象。

Oracle 之所以把函数命名为TO_DATE,是因为它认为从字符串变成"内部日期类型"是一个显式的类型转换动作,类似TO_NUMBER、TO_CHAR的家族体系,都叫"转换成什么"。而 MySQL 的思路更接近"操作素材本身":STR_TO_DATE就是把一个字符串按给定格式解析成 DATE,名字直接把参数类型和结果类型都写出来了。理念上各有道理,但迁移时就是容易绕晕。

另外补充一点,MySQL 里想要把字符串转成日期,除了STR_TO_DATE,还有CAST('2024-06-18' AS DATE),以及DATE('2024-06-18 14:30:00')这种取值函数。它们之间的区别我会在后面展开。

1.3 一个常见误区:MySQL 早期版本到底有没有 to_date

网上有不少老帖子说"MySQL 5.x 可以用 to_date",其实那是把 MariaDB 和 MySQL 搞混了。MariaDB 在 Oracle 兼容模式下提供了TO_DATE,如果你公司的测试环境其实用的是 MariaDB,或者某云数据库默认开了兼容模式,to_date真的能用。但在官方 MySQL 8.0、8.4 这些版本里,TO_DATE始终不是内置函数。判断标准很简单:看SELECT VERSION()返回的是MySQL还是MariaDB,别拿别人的经验直接套。

我在实际项目里的习惯是:迁移脚本里统一把to_date(...)重写成str_to_date(...),同时把格式符从'YYYY-MM-DD'改成'%Y-%m-%d'。这一步虽然机械,却是所有迁移工作的第一步,也是最容易遗漏的一步。

2. STR_TO_DATE:字符串转日期的正主,用法一次讲清

2.1 语法格式与常用格式符

STR_TO_DATE的基本语法:

STR_TO_DATE(str, format)

str是待解析的字符串,format是解析格式。返回值是 DATETIME 类型,如果格式里没写时间部分,默认补00:00:00;也可以按解析出的日期部分作为 DATE 类型使用。

常用格式符我整理了一张表,建议直接收藏:

格式符含义例子
%Y四位年份2024
%y两位年份24
%m两位月份06
%c月份,不补零6
%d两位日18
%e日,不补零18
%H24 小时制两位14
%h12 小时制02
%i分钟两位30
%s秒两位45
%f微秒123456
%pAM/PMPM
%T等价于%H:%i:%s14:30:45
%j一年中的第几天170

2.2 三种典型用法拆解

第一种,最普通的日期字符串:

SELECT STR_TO_DATE('2024-06-18', '%Y-%m-%d'); -- 结果:2024-06-18 00:00:00

第二种,带时间的字符串:

SELECT STR_TO_DATE('2024-06-18 14:30:45', '%Y-%m-%d %H:%i:%s'); -- 结果:2024-06-18 14:30:45

第三种,格式不标准的输入,比如日志文件里常见的2024/06/18 14.30.45,或者海外系统里那种06/18/2024月日年和14:30的混合格式:

SELECT STR_TO_DATE('2024/06/18 14.30.45', '%Y/%m/%d %H.%i.%s'); -- 结果:2024-06-18 14:30:45

这种把格式完全交给调用方设计的用法,是STR_TO_DATE最大的优势。只要格式符能精确对应字符串每一位,几乎任何排版都能还原成日期。

2.3 从毫秒时间戳转日期:别在 MySQL 里找 Oracle 的办法

热词里有一条"oracle 毫秒转换日期格式",这里必须多说一句。Oracle 里处理毫秒时间戳通常要经过NUMTODSINTERVAL,或者TO_DATE('1970-01-01','YYYY-MM-DD') + ms/1000/86400这种笨办法。MySQL 里简单得多。

如果表里存的是 bigint 毫秒值,比如1718700000000,转换 SQL 是这样:

SELECT FROM_UNIXTIME(1718700000000 / 1000) AS dt; -- 结果:2024-06-18 14:30:00(按会话时区显示)

如果是秒级,直接去掉/ 1000:

SELECT FROM_UNIXTIME(1718700000) AS dt;

注意一点:FROM_UNIXTIME的返回值受会话时区影响。如果 MySQL 服务端时区设置是+00:00,你看到的可能是06:30:00,而不是本地时间。这时候要么改 JDBC 连接参数里的serverTimezone=Asia/Shanghai,要么用CONVERT_TZ显式转时区。最稳妥的做法是统一在应用层确认连接时区,再执行转换,不要默认服务器时区就一定是本地时区。

3. DATE_FORMAT:反向操作,把日期格式化成想要的字符串

3.1 为什么这个函数和 to_date 同样重要

从 Oracle 迁移过来的人通常只记得TO_DATE,忘了TO_CHAR,实际上在报表和接口对接里,日期转字符串的频率一点也不低。MySQL 里对应的是DATE_FORMAT(date, format),date 可以是日期类型、datetime 类型,也可以是NOW()这种表达式。

SELECT DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s'); -- 结果:2024-06-18 14:30:45

和STR_TO_DATE用的格式符是同一套,字符串互转时只要把方向反过来,函数名跟着换,格式符保持不变。这一点想通了,两套体系其实非常好记。

3.2 一张对照表把两套体系彻底记牢

Oracle 开发天天用的日期转换,在 MySQL 里该怎么写,我列了一张常用对照表。迁移、面试、帮人改脚本的时候,照着抄就行:

需求Oracle 写法MySQL 写法
字符串转日期TO_DATE('2024-06-18', 'YYYY-MM-DD')STR_TO_DATE('2024-06-18', '%Y-%m-%d')
日期转字符串TO_CHAR(SYSDATE, 'YYYY-MM-DD')DATE_FORMAT(NOW(), '%Y-%m-%d')
取当前日期TRUNC(SYSDATE)DATE(NOW())或CURDATE()
取年月日EXTRACT(YEAR FROM SYSDATE)YEAR(NOW())/MONTH(NOW())/DAY(NOW())
日期加减SYSDATE + INTERVAL '1' DAYDATE_ADD(NOW(), INTERVAL 1 DAY)
两个日期差SYSDATE - DATE '2024-01-01'DATEDIFF(NOW(), '2024-01-01')

3.3 最容易写错的几个格式符

%Y和%y最容易混。%Y是四位年份,%y是两位年份,如果字符串里只有24,却写了%Y,结果会是NULL。月份上%m是两位,%c不补零;日期上%d两位,%e不补零。这些细节在DATE_FORMAT输出时表现不明显,但在STR_TO_DATE解析时不匹配就直接NULL,排查起来很费时间。

时间部分还有一个坑:%h是 12 小时制,%H是 24 小时制。如果你拿%h去解析'14:30:45',MySQL 会把"14"理解成 12 小时制的下午 2 点,然后因为格式不匹配返回NULL或者出现语义错乱。我在项目里一直建议统一用 24 小时制格式,除非确认数据一定是AM/PM格式。

4. 从 Oracle 迁移到 MySQL:五个最容易踩的日期场景还原

4.1 存储过程里的日期拼接

迁移脚本里最常见的写法是这样:

-- Oracle 写法 v_date := TO_DATE('2024-06-18', 'YYYY-MM-DD'); -- MySQL 改写 SET v_date = STR_TO_DATE('2024-06-18', '%Y-%m-%d');

如果是在查询条件里,还要注意 MySQL 存储过程的语法差异,比如参数、变量声明方式、DECLARE的位置等。日期转换本身不复杂,难的是迁移时容易漏掉格式符的改写。我习惯先写一个脚本批量扫描to_date(这个关键字,把出现的每一处都人工核对格式符后再统一替换,效率最高。

4.2 按日期区间查询:索引为什么突然失效

Oracle 里很多人会写TO_DATE(create_time, 'YYYY-MM-DD'),但注意 create_time 本来就是日期列的时候,TO_DATE传入的就是日期,Oracle 会做隐式转换。迁移到 MySQL 后如果机械改成STR_TO_DATE(create_time, '%Y-%m-%d'),MySQL 会先把 create_time 转成字符串再去解析,导致传入索引列的函数破坏了索引使用。

正确做法是让被比较的字段保持原样,只在边界值上做转换:

-- 错误示例:在索引列上套函数 SELECT * FROM orders WHERE STR_TO_DATE(create_time, '%Y-%m-%d') >= STR_TO_DATE('2024-06-18', '%Y-%m-%d'); -- 正确示例:字段不动,边界值转换 SELECT * FROM orders WHERE create_time >= STR_TO_DATE('2024-06-18 00:00:00', '%Y-%m-%d %H:%i:%s') AND create_time < STR_TO_DATE('2024-06-19 00:00:00', '%Y-%m-%d %H:%i:%s');

后一种写法能给优化器留出使用create_time索引的空间。这个坑在数据量小的库上完全无感,数据量一上来,差的可能就是全表扫描和索引扫描的差别。

4.3 日期字段默认值不能随心所欲

Oracle 建表时可以给 DATE 类型的默认值写SYSDATE:

CREATE TABLE t (create_time DATE DEFAULT SYSDATE);

MySQL 的日期默认值限制很死。5.7 之后虽然支持了DEFAULT CURRENT_TIMESTAMP,但CURRENT_TIMESTAMP是带时间部分的,如果你想默认到当天日期(不带时间),官方并不允许直接写DEFAULT CURDATE()。解决办法通常是:

CREATE TABLE t ( create_date DATE NOT NULL DEFAULT (CURRENT_DATE) );

注意括号,MySQL 8.0 支持用表达式作为默认值,但必须加括号。很多人迁移时忽略这一点,建表语句直接报语法错误。如果数据库版本还是 5.7,连带括号的表达式默认值都不支持,只能在应用层插入时显式赋值,或者在BEFORE INSERT触发器里补日期。

4.4 报表统计里的日期维度

报表开发里最常见的是按天、按月分组:

SELECT DATE_FORMAT(create_time, '%Y-%m-%d') AS day, COUNT(*) FROM orders GROUP BY day ORDER BY day;

注意 MySQL 允许在GROUP BY里引用SELECT中的别名,这是它比 Oracle 方便的地方。但如果表很大,按DATE_FORMAT分组就无法走create_time索引,处理办法一般是先把查询范围缩小到确定日期区间,再格式化输出,或者在表设计阶段就增加冗余的日期字符串字段用于报表查询。

4.5 日志表场景:字符串字段存日期,什么时候必须转换

有的旧系统图省事,把日期直接存成了VARCHAR,比如'2024-06-18 14:30:45'。这种表在查询时如果传参是字符串,MySQL 里其实可以直接比较,因为字符串日期和标准日期格式对得上时隐式转换是可靠的。但一旦遇到这种写法:

SELECT * FROM log_table WHERE STR_TO_DATE(log_time, '%Y-%m-%d %H:%i:%s') >= '2024-06-18 00:00:00';

性能就很危险。与其在查询中到处转换,我更建议一次性把存量数据清洗成DATETIME列,再建索引。转换的脚本用STR_TO_DATE写一遍:

UPDATE log_table SET log_time = STR_TO_DATE(log_time, '%Y-%m-%d %H:%i:%s') WHERE log_time IS NOT NULL;

然后ALTER TABLE修改列类型。数据量大时先备份,分批提交,转换失败的行单独捞出来排查格式,避免一条脏数据卡掉全部任务。

5. 实测踩坑记录:那些让你怀疑人生的日期转换细节

5.1 格式不匹配:MySQL 返回 NULL 而不是报错

这是我最想强调的一个行为差异。STR_TO_DATE解析失败时,多数情况下不抛异常,而是返回NULL。比如:

SELECT STR_TO_DATE('2024-13-45', '%Y-%m-%d'); -- 结果:NULL(13 月、45 日,MySQL 不报错,直接给 NULL)

如果这个值插入到表的非空日期字段中,就会被当作NULL处理,严重时直接违反非空约束,或者悄悄写入了'0000-00-00'。所以用STR_TO_DATE做清洗时,一定要先跑一遍检查:

SELECT COUNT(*) FROM raw_data WHERE STR_TO_DATE(raw_date, '%Y-%m-%d') IS NULL AND raw_date IS NOT NULL;

把所有解析不出来、解析出来但日期非法的数据先揪出来,等源头修正或人工核对,再批量入库。不要相信"格式肯定没问题"这种话,真实生产数据里什么都有。

5.2 隐式转换和 STR_TO_DATE 的微妙区别

MySQL 在字符串与日期比较时存在隐式转换,比如WHERE create_time = '2024-06-18',MySQL 会尝试把字符串按日期解析,如果 create_time 是 DATETIME,'2024-06-18'会被补成2024-06-18 00:00:00,然后比较。于是你在页面上输入一个日期,想查当天记录却查不到当天 18:30 的单,因为那条记录的 create_time 是2024-06-18 18:30:00,等值匹配不成立。

处理技巧是明确写出范围:

WHERE create_time >= '2024-06-18 00:00:00' AND create_time < '2024-06-19 00:00:00';

这个习惯比纠结隐式转换规则更实用,而且对索引也友好。

5.3 时区函数:跨时区系统不要直接格式化

如果你维护的系统里有跨时区用户,日期转换的坑会出现在浏览端。比如服务器时区是UTC,业务时区是Asia/Shanghai,你直接DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s'),展示出来的是 UTC 时间,和用户本地时间差 8 小时。

MySQL 提供CONVERT_TZ:

SELECT CONVERT_TZ('2024-06-18 06:30:00', '+00:00', '+08:00'); -- 结果:2024-06-18 14:30:00

但CONVERT_TZ依赖时区表数据,如果系统表有对应时区数据,直接能算;如果返回 NULL,需要先加载时区表。在 Java 项目里更常见的方案是 JDBC 连接串直接指定serverTimezone=Asia/Shanghai,让驱动在应用层统一时区,数据库层保持标准时间。两种方案各有取舍,但一定要保证应用、数据库、展示端三方时区口径一致,否则日期转换所有函数都会出现看似"算错"的问题。

5.4 分组统计时别在日期列上反复套函数

我见过一个统计脚本,慢到连运维都来问是不是锁表。打开一看,SELECT ... WHERE DATE_FORMAT(create_time, '%Y-%m-%d') = '2024-06-18',这个写法在数据量大时完全走不了索引。优化思路是改成:

WHERE create_time >= '2024-06-18 00:00:00' AND create_time < '2024-06-19 00:00:00'

另一种常见场景是GROUP BY DATE_FORMAT(create_time, '%Y%m')按月统计。除非这张表的日期字段本身很少被范围查询,否则建议考虑增加month_col、day_col这类冗余字段,查询时直接分组冗余字段,既简单又快。这也是我处理千万级别流水表时最常用的优化手段。

5.5 实战中总结的几条写作原则

我实际做项目时的习惯是,每张业务表都至少保留一个DATETIME类型的create_time,所有查询条件都按"字段不做函数处理、边界值显式转换"的原则来写。日期转换函数本身并不难,难的是在真实数据、真实索引和真实时区条件下,让每次格式化的结果都符合预期。你把STR_TO_DATE和DATE_FORMAT这两个函数用熟,把%Y、%m、%d、%H、%i、%s这套格式符刻在脑子里,绝大多数 MySQL 日期转换的问题都不用再查资料。

再分享一个我常用的调试技巧:不确定格式符怎么写时,先用SELECT STR_TO_DATE('一段样例数据', '猜测的格式')单独跑一遍,返回结果正常再放进查询里。一次只验证一个函数,比在几百行 SQL 里翻错误更高效。

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

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

立即咨询