
从Oracle迁移到MySQL或者一套系统里同时要维护两种数据库时最先遇到的一类差异往往不是复杂的存储过程写法而是“读取当前日期时间”这种看起来再简单不过的操作。两边都有取当前时间的函数名字看着也差不多但返回的到底是什么类型、精度到哪一位、到底受不受时区影响差别能影响一整条业务链路。我刚开始做双库适配时在这里踩过不少坑这篇把Oracle和MySQL读取当前日期时间的差异一次讲清楚。作者资深数据平台运维1. 基础函数对比同样取“现在”结果却各有门道1.1 Oracle的取时函数家族SYSDATE、SYSTIMESTAMP、CURRENT_TIMESTAMPOracle里最常见的取当前日期时间函数是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_TIMESTAMPMySQL里最常用的取当前日期时间的函数是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写法返回类型与精度差异当前日期和时间SYSDATENOW()Oracle返回DATE秒级MySQL返回DATETIME秒级当前日期和时间高精度SYSTIMESTAMPNOW(6)Oracle返回TIMESTAMP WITH TIME ZONE微秒级MySQL返回DATETIME微秒级当前日期时间随会话时区CURRENT_TIMESTAMPNOW()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_CHARSELECT TO_CHAR(SYSDATE, YYYY-MM-DD HH24:MI:SS) FROM DUAL;MySQL里对应的是DATE_FORMATSELECT 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 SYSDATECREATE TABLE T_ORDER ( ID NUMBER PRIMARY KEY, CREATE_TIME DATE DEFAULT SYSDATE );MySQL这边要灵活得多。DATETIME列可以直接设置默认值为CURRENT_TIMESTAMP5.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它只比较日期部分忽略时分秒算精确到秒、分钟、小时的差异用TIMESTAMPDIFFSELECT 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连接串里设置了serverTimezoneAsia/Shanghai或等价的参数连接参数和数据库参数打架是非常典型的原因。至于时间“少了”也就是时分秒全是00:00:00的情况几乎都是字段类型映射错误导致的。Oracle的DATE列被建成了MySQL的DATE类型原本的时分秒被截断了。把MySQL的字段类型改成DATETIME然后回源库重新抽数没有捷径。4.4 跨库读取当前时间一套踩坑实用结论把上面所有细节浓缩成一张速查表实际干活时直接对着它抄就行比每次翻官方文档快得多。对比项OracleMySQL核心取当前时间函数SYSDATENOW()高精度取当前时间函数SYSTIMESTAMPNOW(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 1NOW() INTERVAL 1 DAY加30分钟SYSDATE 30/1440NOW() INTERVAL 30 MINUTE计算天数差date1 - date2DATEDIFF(date1, date2)计算秒数差(date1 - date2) * 86400TIMESTAMPDIFF(SECOND, date2, date1)建表默认值11g用DEFAULT SYSDATEDEFAULT CURRENT_TIMESTAMP自动更新时间戳需要触发器ON UPDATE CURRENT_TIMESTAMP最后分享一个我自己养成的工作习惯现在拿到任何涉及双库的日期时间需求我不会先写代码而是先问清楚四件事——精度要到秒还是毫秒、时区统一以哪个环境为准、字段要存远期日期还是短期日期、默认值逻辑由数据库负责还是应用层负责。把这四个问题定下来SQL照上面的对照表套基本上不会再踩瞎忙半天的坑。跨库适配看着是函数名翻译的活实际上是把两套时间语义梳理清楚的过程这一步省不得。