从Oracle迁移到MySQL,或者一套系统里同时要维护两种数据库时,最先遇到的一类差异往往不是复杂的存储过程写法,而是“读取当前日期时间”这种看起来再简单不过的操作。两边都有取当前时间的函数,名字看着也差不多,但返回的到底是什么类型、精度到哪一位、到底受不受时区影响,差别能影响一整条业务链路。我刚开始做双库适配时,在这里踩过不少坑,这篇把Oracle和MySQL读取当前日期时间的差异一次讲清楚。
作者:资深数据平台运维
1. 基础函数对比:同样取“现在”,结果却各有门道
1.1 Oracle的取时函数家族:SYSDATE、SYSTIMESTAMP、CURRENT_TIMESTAMP
Oracle里最常见的取当前日期时间函数是SYSDATE。它返回的是数据库服务器所在操作系统的当前日期和时间,数据类型是DATE,精度只到秒。也就是说,如果你在SQL里执行SELECT SYSDATE FROM DUAL;,拿到的结果类似2025-03-23 14:35:22,后面没有毫秒,也没有时区信息。
SYSDATE有个容易被忽略的特点:它依赖的是数据库服务器主机的时钟,而不是客户端或会话的时区。如果服务器是北京时间,客户端连过来时用的会话时区设置成伦敦时间,SYSDATE返回的依然是服务器上的北京时间。
如果业务对精度有更高要求,就要用SYSTIMESTAMP。它返回的是TIMESTAMP WITH TIME ZONE类型,带时区信息,默认能取到小数点后6位的微秒精度,很多操作系统下甚至可以到纳秒级。执行SELECT SYSTIMESTAMP FROM DUAL;,结果类似2025-03-23 14:35:22.123456 +08:00。
另外还有一个CURRENT_TIMESTAMP,它和SYSTIMESTAMP类似,但会跟随当前会话的时区设置。Oracle里可以通过ALTER SESSION SET TIME_ZONE = 'Europe/London';把会话时区切到伦敦,再执行SELECT CURRENT_TIMESTAMP FROM DUAL;,返回的就是伦敦当地的时间。简单说,Oracle里SYSDATE和SYSTIMESTAMP以服务器为准,CURRENT_DATE和CURRENT_TIMESTAMP以会话时区为准。
1.2 MySQL的取时函数家族:NOW()、SYSDATE()与CURRENT_TIMESTAMP
MySQL里最常用的取当前日期时间的函数是NOW(),返回的是DATETIME类型,精度默认到秒,比如2025-03-23 14:35:22。
MySQL的CURRENT_TIMESTAMP和CURRENT_TIMESTAMP()其实都是NOW()的同义写法,返回结果完全一样,不用刻意区分。CURDATE()只返回日期部分,CURTIME()只返回时间部分,这两个函数在只需要日期或时间时很有用,Oracle里反而没有完全对等的单函数实现。
MySQL还有一个SYSDATE(),这个函数名字很容易让跨库的人误会,以为它对应Oracle的SYSDATE。实际上MySQL的SYSDATE()与NOW()在语义上有细微差别:NOW()取的是这条SQL语句开始执行那一刻的时间,而SYSDATE()取的是它自己被执行到那一行时的时间。绝大多数场景看不出差别,但如果SQL执行时间很长,或者中途有等待,两者的返回值就可能不一致。后面第4章会专门说这个坑。
MySQL的NOW()还允许指定小数秒精度,NOW(3)可以取到毫秒,NOW(6)可以取到微秒。从MySQL 5.6.4版本开始才支持这个特性,老版本不支持。
1.3 对照表:两张图看懂返回类型与精度差异
为了不让你在跨库写SQL的时候临时翻文档,我先把两个数据库里最常用的取当前日期时间的函数以及它们的返回类型列成对照表。
| 功能需求 | Oracle写法 | MySQL写法 | 返回类型与精度差异 |
|---|---|---|---|
| 当前日期和时间 | SYSDATE | NOW() | Oracle返回DATE秒级;MySQL返回DATETIME秒级 |
| 当前日期和时间(高精度) | SYSTIMESTAMP | NOW(6) | Oracle返回TIMESTAMP WITH TIME ZONE微秒级;MySQL返回DATETIME微秒级 |
| 当前日期时间(随会话时区) | CURRENT_TIMESTAMP | NOW() | Oracle返回带时区类型;MySQL返回会话时区下的DATETIME |
| 只取当前日期 | TRUNC(SYSDATE) | CURDATE() | Oracle返回DATE类型当日零点;MySQL返回DATE纯日期 |
| 只取当前时间 | TO_CHAR(SYSDATE, 'HH24:MI:SS') | CURTIME() | Oracle本质是格式化字符串;MySQL返回TIME类型 |
这个表里最核心的差异有三点:第一,Oracle的SYSDATE是DATE类型,MySQL的NOW()是DATETIME类型,虽然查询结果看起来差不多,但底层存储和精度不同;第二,Oracle的SYSTIMESTAMP带时区信息,MySQL的NOW(6)不带时区信息;第三,Oracle里“只取当前日期”要用TRUNC包一层,MySQL则直接给你一个CURDATE(),这反映的是两边数据类型设计的差异。刚开始做迁移时,先把这张表刻在脑子里,能省掉后面一半的返工。
2. 藏在细节里的坑:类型、精度与时区的不对等
2.1 Oracle的DATE不是“日期”,MySQL的DATE才是“纯日期”
这一点是跨库同学最容易踩的坑:Oracle的DATE类型虽然名字叫DATE,但它实际上包含了完整的时分秒,精度到秒。比如你在Oracle里定义一个列CREATE_DATE DATE,存进去的值天然就有2025-03-23 14:35:22这种年月日时分秒的完整信息。使用Navicat等工具看表数据时,DateTime部分就显示在那里。
MySQL则完全不同,它把日期和时间拆得很开。MySQL的DATE类型只存储日期部分2025-03-23,时间部分一律为零,DATETIME类型才同时存日期和时间,TIME类型只存时间,TIMESTAMP类型也是日期加时间但范围受限。
这个类型语义差异在迁移时是致命的。很多工具或者手工建表脚本会把Oracle的DATE列直接映射成MySQL的DATE列,看起来是“对应”了,但数据一导入,Oracle里原本的2025-03-23 14:35:22直接变成2025-03-23,订单创建、日志记录这种业务的时分秒全部丢失,而且数据已经迁过去之后很难追回。正确做法是:Oracle的DATE列映射到MySQL时要仔细判断业务含义,只要业务数据里有非零的时间部分,就必须用DATETIME而不是DATE。
反过来,从MySQL往Oracle迁,如果原来用的是DATETIME,到Oracle需要映射成DATE或TIMESTAMP,同样要避免把精度和范围搞错。
2.2 时区策略完全不同:会话级与数据库级
时区的差异是我在实际生产环境里遇到过的最隐蔽问题。表面上两边都能正确“取当前时间”,但取的到底是哪个时区的当前时间,Oracle和MySQL的答案不一样。
Oracle的SYSDATE和SYSTIMESTAMP读的是数据库服务器的系统时钟,与客户端会话时区无关。CURRENT_DATE和CURRENT_TIMESTAMP会跟随当前会话的时区设置,但默认情况下会话时区又取自操作系统时区。所以绝大多数部署场景下,Oracle的四个取时函数结果都指向同一台服务器的时间。
MySQL这边,NOW()返回的是会话时区下的当前时间。连接建立时,MySQL会读取服务器的time_zone变量作为会话时区,通常默认是SYSTEM,也就是跟随操作系统时区。但有一个关键差异:MySQL的TIMESTAMP类型列在存储时会先转换成UTC时间,读取时再按会话时区转换成当地时间;DATETIME类型列则不做任何转换,存进去是什么,取出来就是什么。
举个真实场景:服务器是UTC时区,Oracle里用SYSDATE存了一条记录是UTC时间的2025-03-23 06:35:22,应用在连接MySQL时把会话时区设置成了+08:00,然后写入一个DATETIME列,应用里取到的是2025-03-23 14:35:22。表面看两边程序代码都是“取当前时间”,但数据库里存的值相差8小时。这类问题不看参数配置根本发现不了,真要排查起来比SQL写错还费劲。
2.3 精度问题:秒、毫秒、微秒怎么对表
精度差异在日志类、订单类、对账类系统里影响很大,尤其做交易流水核对的时候。
Oracle的SYSDATE精度到秒,注意是秒,不是毫秒。你要是靠WHERE UPDATE_TIME > SYSDATE去判断一个刚刚发生的操作,正好卡在秒级边界上就可能漏数据。需要毫秒以上精度时,Oracle必须用SYSTIMESTAMP,它返回带时区的TIMESTAMP类型,默认6位小数秒,也就是微秒级精度。
MySQL的NOW()默认也是秒级精度,但NOW(3)是毫秒,NOW(6)是微秒。注意MySQL的DATETIME类型最高支持小数点后6位,所以不论怎么设置,MySQL里都到不了纳秒级。Oracle的TIMESTAMP类型二进制存储里可以到小数点后9位,也就是纳秒级。如果业务上有高精度对账需求,从Oracle迁到MySQL就要提前评估,纳秒级精度在MySQL里放不下,必须做舍入或者改用字符串存储。
另外,格式化时也要留神精度。Oracle里TO_CHAR(SYSTIMESTAMP, 'YYYY-MM-DD HH24:MI:SS.FF')可以输出微秒,MySQL里DATE_FORMAT(NOW(6), '%Y-%m-%d %H:%i:%s.%f')可以输出6位微秒。两者的格式化符号完全不同,后面第3章会给对照表。
2.4 MySQL里DATETIME与TIMESTAMP该选谁
把Oracle迁到MySQL时,很多人会被TIMESTAMP这个名字吸引,因为在Oracle里TIMESTAMP就是高精度时间戳,感觉“高级”。但MySQL的TIMESTAMP和Oracle的TIMESTAMP完全是两个物种。
MySQL的TIMESTAMP存储范围最大只能到2038年,就是经典的2038年问题。存储时会转成UTC,读取时按会话时区转换,这带来两层风险:一是远期业务数据存不进去,比如会员有效期到2050年,用TIMESTAMP会直接报错;二是时区配置一变,历史数据的展示结果跟着变。
MySQL的DATETIME存储范围从1000年到9999年,不做任何时区转换,存什么取什么,适合绝大多数业务场景。我的建议比较直接:新业务或者迁移项目里,默认用DATETIME,除非你明确需要“存进去自动转UTC”这种特性。相比之下,Oracle的DATE和TIMESTAMP在范围上都能到公元9999年,DATE支持时分秒,TIMESTAMP支持小数秒,选型逻辑简单很多。
3. 实战操作:跨数据库取当前时间的等价写法
3.1 查询与条件过滤:正确取“今天”的数据
先从一个最常见的业务需求说起:查询今天创建的所有订单。
Oracle里常见的写法有两种。一种是在条件里直接对列做TRUNC,如WHERE TRUNC(CREATE_TIME) = TRUNC(SYSDATE),这种写法能跑,但列上套了TRUNC函数之后,CREATE_TIME列的普通索引用不上了,表一大就是全表扫描。另一种是范围查询:WHERE CREATE_TIME >= TRUNC(SYSDATE) AND CREATE_TIME < TRUNC(SYSDATE) + 1,这种写法可以利用索引,Oracle里的日期加减直接以天为单位,TRUNC(SYSDATE) + 1表示明天零点。
MySQL对应的范围查询写法是:
SELECT * FROM orders WHERE created_at >= CURDATE() AND created_at < CURDATE() + INTERVAL 1 DAY;CURDATE()返回当天零点,加INTERVAL 1 DAY就是明天零点,用半开区间把今天的数据全包进去。不要写成WHERE DATE(created_at) = CURDATE(),因为在列上套DATE函数同样会让索引失效。
这组写法的精髓不在“取当前时间”本身,而在于怎么把“当前时间”转化成适合索引扫描的查询条件。我见过太多跨库同学只改了函数名,把TRUNC(CREATE_TIME) = TRUNC(SYSDATE)机械翻译成DATE(created_at) = CURDATE(),功能没错,但查询性能掉一个量级。核心思路是:取当前时间只是第一步,把它用在WHERE条件里时,永远优先考虑范围扫描而不是在列上套函数。
3.2 格式化输出与毫秒转换:TO_CHAR和DATE_FORMAT怎样对齐
业务系统里经常要把数据库当前时间格式化成指定字符串,比如生成文件名的日期后缀、报表里的日期列。Oracle和MySQL的格式化函数和格式符完全不同,不能直接照搬。
Oracle里常用TO_CHAR:
SELECT TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS') FROM DUAL;MySQL里对应的是DATE_FORMAT:
SELECT DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s');这里的坑在于,Oracle和MySQL甚至对同一个含义的格式符用了不同的字符。年份都是YYYY,但小时在Oracle里是HH24,在MySQL里是%H;分钟在Oracle里是MI,在MySQL里是%i;秒在Oracle里是SS,在MySQL里是%s。大小写也有讲究,Oracle的MM表示月份,MySQL的%m是数字月份,%M反而是英文月份名。
再补充一个热词关联度很高的场景:毫秒时间戳和日期互转。Oracle里要把毫秒级时间戳1735689600000转成日期,常用这样一段:
SELECT TO_TIMESTAMP('1970-01-01 00:00:00', 'YYYY-MM-DD HH24:MI:SS') + 1735689600000 / 86400000 FROM DUAL;MySQL里就简单很多:
SELECT FROM_UNIXTIME(1735689600000 / 1000);反向操作,Oracle里日期转毫秒时间戳:
SELECT (SYSTIMESTAMP - TO_TIMESTAMP('1970-01-01 00:00:00', 'YYYY-MM-DD HH24:MI:SS')) * 86400000 FROM DUAL;但直接用SYSTIMESTAMP做减法,结果里带小数秒,要做ROUND或CAST处理。MySQL里直接SELECT UNIX_TIMESTAMP(NOW(3)) * 1000;就能拿到毫秒。这类转换逻辑放到应用层做往往更省心,数据库里临时排查时用得到,但不要写成核心业务逻辑依赖的定时任务。
3.3 建表默认值:从Oracle到MySQL迁移时的重灾区
建表时给日期时间列设置默认值“当前时间”是几乎每个表都逃不掉的需求。这里的差异非常大,处理不好,SQL脚本直接不兼容。
Oracle在11g之前,列默认值不允许使用函数表达式,建表时只能写常量,日期类默认值要在应用层插入时显式传入SYSDATE,或者用触发器补默认值,非常繁琐。Oracle 11g之后终于支持DEFAULT SYSDATE:
CREATE TABLE T_ORDER ( ID NUMBER PRIMARY KEY, CREATE_TIME DATE DEFAULT SYSDATE );MySQL这边要灵活得多。DATETIME列可以直接设置默认值为CURRENT_TIMESTAMP,5.6.5之后还支持ON UPDATE CURRENT_TIMESTAMP,更新记录时自动刷新时间戳:
CREATE TABLE t_order ( id INT PRIMARY KEY AUTO_INCREMENT, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );这里有个容易踩到的老坑:MySQL 5.6.5之前,一个表里只有一个TIMESTAMP列能设置默认值,且TIMESTAMP默认值还有非空限制,很多人因此在早期版本里写了各种奇怪的配置。从5.6.5开始,多个TIMESTAMP和DATETIME列都可以独立设置默认CURRENT_TIMESTAMP,问题基本消失。
MySQL 8.0.13之后还支持表达式默认值,比如:
CREATE TABLE t_order ( id INT PRIMARY KEY AUTO_INCREMENT, expire_at DATETIME DEFAULT (CURRENT_TIMESTAMP + INTERVAL 30 DAY) );Oracle这边则还做不到这种灵活的默认表达式,通常靠应用层或触发器完成。从我的迁移经验看,建表默认值这块是整个DDL迁移里最容易出脚本错误的地方,建议迁移时把这类字段单独拎出来,一行一行对照着改,不要依赖自动转换工具。
3.4 日期运算与时间差计算:加减法的底层逻辑
日期时间运算也是双库代码里躲不开的高频操作。Oracle和MySQL的底层逻辑完全不同,一个以“天”为自然单位做加减法,一个用显式INTERVAL做加减法。
Oracle里日期加减直接写数字,单位是天。SYSDATE + 1代表明天的此刻,SYSDATE + 1/24代表一小时后,SYSDATE + 30/86400代表30秒后。这种写法很灵活,但可读性一般,需要自己换算。
-- Oracle:找到30分钟前未支付的订单 SELECT * FROM t_order WHERE status = 'UNPAID' AND create_time <= SYSDATE - 30/1440;MySQL里则是用INTERVAL关键字:
SELECT * FROM t_order WHERE status = 'UNPAID' AND created_at <= NOW() - INTERVAL 30 MINUTE;也可以写DATE_SUB(NOW(), INTERVAL 30 MINUTE),两种等价。注意MySQL里NOW() - 1这种写法虽然不报错,但含义完全不同,它是把DATETIME转成数值再减,结果不是你想要的“昨天此刻”,这也是跨库改造里最容易出现的无语bug。
时间差计算方面,Oracle计算两个日期相差多少天,直接相减就行:DATE1 - DATE2,结果是一个数字,表示多少天。计算月份差用MONTHS_BETWEEN,甲骨文还专门提供了ADD_MONTHS做月份加减。MySQL里算日期差要分单位,算天数用DATEDIFF,它只比较日期部分忽略时分秒;算精确到秒、分钟、小时的差异用TIMESTAMPDIFF:
SELECT TIMESTAMPDIFF(SECOND, created_at, NOW()) FROM t_order;Oracle里如果想精确到秒,要这样写:
SELECT (SYSDATE - create_time) * 86400 FROM t_order;同样是“两者相减”,Oracle拿到的是天数,需要乘86400换秒;MySQL的TIMESTAMPDIFF直接在参数里指定单位,语义更清晰。
4. 常见问题与排查经验速查
4.1 MySQL的SYSDATE()和NOW()结果为什么偶尔不一样
这个问题在论坛上隔三差五就有人问。前面提过,MySQL的NOW()返回的是语句开始执行时的时间,语句里所有NOW()的值在整个SQL执行期间保持一致;而SYSDATE()返回的是它真正被执行到那一刻的时间,是动态的,语句执行多久,它就可能往后飘多久。
当一条SQL里同时有耗时操作和SYSDATE()调用时,就有可能出现“同一张表里两条记录的时间不同”的诡异现象。比如一条存储过程里先执行一个大表的UPDATE,再执行INSERT INTO ... SELECT SYSDATE(),那么INSERT拿到的时间实际上是UPDATE执行完之后的“当前时间”,比SQL开始执行的时间晚了一截。
跨库习惯真的要改一下:从Oracle过来的人习惯了SYSDATE这个名字,很容易在MySQL里下意识写SYSDATE(),但业务上想要的一般都是“语句开始时间”这种稳定的语义,它对应MySQL的NOW()而不是SYSDATE()。所以在MySQL里,取当前日期时间请默认写NOW(),SYSDATE()这种动态取值很少是业务真正需要的。
4.2 2038年问题与TIMESTAMP的时间边界
MySQL的TIMESTAMP类型存储上限是2038-01-19 03:14:07 UTC。这个边界是Unix时间戳的32位溢出时刻,在Oracle里完全不存在,因为Oracle的DATE和TIMESTAMP都支持到公元9999年。
实际影响场景:会员有效期到2099年、保险到期日在下个世纪、设备的质保期特别长,这些数据在MySQL里如果用TIMESTAMP字段,建表时不一定报错,但写入那天直接报Out of range value for column 'expire_time' at row 1。排查这类报错时,第一反应往往去怀疑数据格式,很少想到是字段类型范围问题。
我当时排查一个设备质保系统迁移报错时,花了半小时,后来才意识到是TIMESTAMP的2038年边界。所以说,凡是业务上可能出现远期日期的场景,MySQL里请直接使用DATETIME。DATETIME范围到9999年,不受时区转换影响,从Oracle的DATE类型转换过来时语义也最接近。
4.3 迁移后时间“少了”或“多了8小时”怎么排查
如果迁移后发现两个库同一张业务表的时间差8小时,按下面的顺序排查最快。
第一,查两边数据库服务器的系统时区是否一致。Linux上执行date -R,看返回的时区偏移,比如+0800还是+0000。第二,查Oracle的会话时区和系统时区:SELECT SESSIONTIMEZONE, SYSTIMESTAMP FROM DUAL;。第三,查MySQL的时区参数:SHOW VARIABLES LIKE '%time_zone%';,重点看全局time_zone是SYSTEM还是具体的时区。第四,看应用连接MySQL时是否在JDBC连接串里设置了serverTimezone=Asia/Shanghai或等价的参数,连接参数和数据库参数打架是非常典型的原因。
至于时间“少了”,也就是时分秒全是00:00:00的情况,几乎都是字段类型映射错误导致的。Oracle的DATE列被建成了MySQL的DATE类型,原本的时分秒被截断了。把MySQL的字段类型改成DATETIME,然后回源库重新抽数,没有捷径。
4.4 跨库读取当前时间:一套踩坑实用结论
把上面所有细节浓缩成一张速查表,实际干活时直接对着它抄就行,比每次翻官方文档快得多。
| 对比项 | Oracle | MySQL |
|---|---|---|
| 核心取当前时间函数 | SYSDATE | NOW() |
| 高精度取当前时间函数 | SYSTIMESTAMP | NOW(6) |
| 取当前日期(当天零点) | TRUNC(SYSDATE) | CURDATE() |
| 返回类型 | SYSDATE是DATE,秒精度;SYSTIMESTAMP带时区,微秒精度 | NOW()是DATETIME,秒精度;NOW(6)微秒精度 |
| 是否受会话时区影响 | SYSDATE不受,CURRENT_TIMESTAMP受 | NOW()受会话时区影响,DATETIME列不受影响 |
| 格式化函数 | TO_CHAR(SYSDATE, 'YYYY-MM-DD HH24:MI:SS') | DATE_FORMAT(NOW(), '%Y-%m-%d %H:%i:%s') |
| 加一天 | SYSDATE + 1 | NOW() + INTERVAL 1 DAY |
| 加30分钟 | SYSDATE + 30/1440 | NOW() + INTERVAL 30 MINUTE |
| 计算天数差 | date1 - date2 | DATEDIFF(date1, date2) |
| 计算秒数差 | (date1 - date2) * 86400 | TIMESTAMPDIFF(SECOND, date2, date1) |
| 建表默认值 | 11g+用DEFAULT SYSDATE | DEFAULT CURRENT_TIMESTAMP |
| 自动更新时间戳 | 需要触发器 | ON UPDATE CURRENT_TIMESTAMP |
最后分享一个我自己养成的工作习惯:现在拿到任何涉及双库的日期时间需求,我不会先写代码,而是先问清楚四件事——精度要到秒还是毫秒、时区统一以哪个环境为准、字段要存远期日期还是短期日期、默认值逻辑由数据库负责还是应用层负责。把这四个问题定下来,SQL照上面的对照表套,基本上不会再踩瞎忙半天的坑。跨库适配看着是函数名翻译的活,实际上是把两套时间语义梳理清楚的过程,这一步省不得。