☰
MySQL DATETIME 全面解析:存储原理、插入格式与时区坑
2026/10/9 3:37:43 网站建设 项目流程

做后端时间一长,你会发现很多关于时间的坑都是从 MySQL 的 DATETIME 类型开始的。原因很简单,绝大多数项目都拿它当默认的“时间字段”,可真正能说清楚它怎么存储、怎么插入、显示格式怎么控制的人,其实不多。DATETIME 看起来就是YYYY-MM-DD HH:MM:SS一串字符,但往深了挖,字节占用、严格模式、时区、格式符、边界条件,每样都能出幺蛾子。

这篇文章就把 DATETIME 从存储到插入再到格式化完整过一遍。我不会只贴语法,而是把每一步背后的原理、选型逻辑、实际踩过的坑一起讲清楚。不管你是在维护老系统,还是新项目刚建表,按着这篇文章的思路走,至少不会再被“时间字段”这种小事绊倒。新手可以照着示例直接写,老手也能在某些边界细节上有点收获。

1. DATETIME 到底怎么存的

先说存储原理。很多人有个错觉,觉得 DATETIME 和字符串差不多,存的就是一串 “2024-08-15 14:30:00”。实际上 MySQL 内部用的是二进制结构,不是普通文本,只是在返回给客户端时格式化成人类能看的样子。这就解释了为什么 DATETIME 排序、比较都特别可靠——因为底层就是在做数值比较。

1.1 字节数、取值范围与显示格式

DATETIME 在 MySQL 5.6.4 之后的标准存储是8 字节,不需要小数秒时就是固定 8 字节。这 8 个字节里其实混合编码了日期部分和时间部分,内部做了位运算压缩,但你不必关心它到底怎么压,只需要记住:它比 DATE(3 字节)大,比 TIMESTAMP(4 字节)大,但换来的是超大的表示范围。

取值范围是1000-01-01 00:00:00到9999-12-31 23:59:59。什么意思?就是更早的日期存不进去,而 9999 年这种远超业务需求的远日子倒是完全没压力。这个范围比 TIMESTAMP 宽得多,TIMESTAMP 的上限是 2038 年 1 月 19 日,也就是传说中的“2038 年问题”来源。如果你要存孩子的生日、长期合同的到期日、保险理赔日期这种动辄几十年的数据,DATETIME 明显更稳妥。

默认的显示格式就是标准的YYYY-MM-DD HH:MM:SS,没有 AM/PM,24 小时制。秒是可选显示的,但底层存储始终精确到秒,你没有显式给秒数时,默认补00。很多人在这一步会下意识以为2024-08-15存进去就是2024-08-15 00:00:00吗?是的,MySQL 会自动补时间部分,这条规则后面插入章节还会再讲。

1.2 DATETIME、TIMESTAMP、DATE,到底选谁

我见过不少团队建表时随手选一个,等出问题才回头纠结。这里直接给一张对比表,建表时对着选就行。

类型存储字节表示范围时区处理典型场景
DATE3 字节1000-01-01 ~ 9999-12-31无时区生日、开户日期、自然日统计
DATETIME8 字节1000-01-01 00:00:00 ~ 9999-12-31 23:59:59无时区,存的是字面时间业务时间、下单时间、日志时间
TIMESTAMP4 字节1970-01-01 00:00:01 UTC ~ 2038-01-19 03:14:07 UTC自动按会话时区转换跨时区协作、需要精确“时刻”的数据

可以用一个生活化的类比来理解 DATETIME 和 TIMESTAMP 的区别:DATETIME 是贴在墙上的钟表读数,你抬头看到几点就是几点,它不关心你现在在哪个时区;TIMESTAMP 是一个绝对时刻,像是“世界标准时 2024-08-15 06:30:00”,你在北京看是 14:30,在纽约看是凌晨 2:30,但底层那秒是同一个瞬间。

所以选型逻辑很简单:

  • 业务里只关心“某天是哪天”,用 DATE,省 5 个字节,索引也更小。
  • 业务跨时区,或者用户分布在多个时区,需要展示各自本地时间,优先考虑 TIMESTAMP 或 DATETIME + 时区偏移字段。
  • 正常管理后台、日志、订单这类“记录事件发生时间”的,无脑 DATETIME 基本不会错。

这里有一个误区要纠正:很多人觉得 DATETIME 因为不带时区,就一定比 TIMESTAMP 差。实际正好相反。对于大多数国内单时区业务,DATETIME 反而更省心,因为它不会因为连接时区设置不同而一会儿多 8 小时、一会儿少 8 小时。TIMESTAMP 的自动时区转换是好功能,但也是最大的乱源,后面第 3 章会展开。

1.3 小数秒:DATETIME(6) 到底多占多少存储

MySQL 从 5.6.4 开始支持小数秒,也就是DATETIME(6)这种写法,括号里的数字表示最多保留几位小数秒,范围 0 到 6,对应微秒精度。

小数秒不是线性占用存储的,按官方文档的说明:

小数秒位数额外存储字节
00 字节
1 ~ 21 字节
3 ~ 42 字节
5 ~ 63 字节

也就是说,DATETIME(6)一共占 8 + 3 = 11 字节,而DATETIME就是 8 字节。如果你平时根本不需要毫秒、微秒,不要手贱把字段定义成DATETIME(6),除了每行多占 3 字节,还会带来一个负面影响:查询结果会显示成2024-08-15 14:30:00.000000,看着很闹心,好多 BI 工具和接口文档还得专门去截断。

那什么时候需要小数秒?缓存击穿、接口耗时的监控埋点、交易流水、秒杀系统的精确排序,这些场景里同一秒内可能有大量数据,微秒精度能帮你在没有额外序号的情况下做稳定排序。但普通业务表加个login_time DATETIME就足够了。

2. 插入数据,别让格式坑了你

DATETIME 的插入是问题重灾区。MySQL 在设计上对日期时间字符串的宽容程度远超你想象,但这个宽容就像安全气囊,平时没感觉,关键时刻能救命也可能误伤。

2.1 MySQL 认哪几种时间书写格式

先来一张速查表,都是实测过能正常插入 DATETIME 列的写法:

写入形式插入结果说明
'2024-08-15 14:30:00'2024-08-15 14:30:00标准格式,最推荐
'2024-08-15 14:30'2024-08-15 14:30:00秒省略时自动补 00
'2024-08-15'2024-08-15 00:00:00缺时间部分时补 00:00:00
'20240815143000'2024-08-15 14:30:00纯数字形式,也能解析
'2024/08/15 14:30'2024-08-15 14:30:00分隔符可以是斜杠
'2024-08-15T14:30:00'2024-08-15 14:30:00ISO 风格带 T,也可以
202408152024-08-15 00:00:00数字日期会自动转换

这些都是“宽松模式”下 MySQL 能解析的。另外,MySQL 对分隔符很随意,连@、!这种符号都能当分隔符用,比如'2024@08@15 14@30@00'也能插进去。但这种写法除了让你的同事骂人没有任何价值,生产代码里一定要用标准'YYYY-MM-DD HH:MM:SS'。

我在实际项目里还见过把2024-8-5这种不补零的日期直接拼进 SQL 的,非严格模式下居然也成功了,Storage Engine 帮你自动把缺的零补上。但这种行为依赖 sql_mode,一旦切到严格模式直接报错。换句话说,MySQL 的宽容是好事,但别把宽容当默认。

2.2 用函数生成时间:NOW 和它的兄弟们

除了直接写字符串,MySQL 还提供了不少生成时间值的函数,插入时用它们可以省掉应用层不少事。

  • NOW()/CURRENT_TIMESTAMP():返回语句开始执行时的会话时区时间,实际项目里最常用。
  • SYSDATE():返回函数真正执行到那一刻的时间。注意它和 NOW() 不一样,一条 UPDATE 里涉及多行时,NOW() 全程不变,SYSDATE() 可能因为执行时长产生几毫秒甚至几秒差异。除非你明确需要这个,否则统一用 NOW()。
  • CURDATE()/CURRENT_DATE():只返回日期,例如2024-08-15,插入 DATETIME 列时自动变成当天 00:00:00。
  • UTC_TIMESTAMP():返回当前 UTC 时间。如果你决定全库统一存 UTC 字面量,插入时直接用它最省事。

举个例子:

INSERT INTO user_login_log (user_id, login_time, ip) VALUES (1001, NOW(), '192.168.1.10');

这样插入的时间就是数据库服务器上的会话本地时间。我再强调一次,NOW() 和 app 服务器的时间未必一致,如果 app 服务器和数据库服务器不在同一个时区,或者系统时间没同步,插入的 DATETIME 就可能与你预期差好几个小时。这种问题排查起来最烧脑,后面第 4 章专门讲。

2.3 参数化插入才是正经做法

可能有人会觉得,直接用字符串拼 SQL 不是挺好吗?Java 里不就是"INSERT INTO t (login_time) VALUES ('" + timeStr + "')"?这种方式在单机自测没问题,可一旦上生产,字符串格式、转义、时区、注入风险全来了。

强烈建议所有语言环境都走参数化查询。比如 Java 的 JDBC:

PreparedStatement ps = conn.prepareStatement( "INSERT INTO user_login_log (user_id, login_time) VALUES (?, ?)"); ps.setLong(1, userId); ps.setTimestamp(2, new Timestamp(System.currentTimeMillis())); ps.executeUpdate();

再比如 Python 的 pymysql:

cursor.execute( "INSERT INTO user_login_log (user_id, login_time) VALUES (%s, %s)", (user_id, login_time) )

参数化有几个实际好处:第一,格式由驱动层处理,你传java.time.LocalDateTime或datetime.datetime对象都行,不用自己拼YYYY-MM-DD HH:MM:SS;第二,避免 SQL 注入;第三,类型明确,不容易出现 MySQL 把字符串按错误语义解析的情况。

如果你的框架需要手动指定 JDBC 连接串,记得加上serverTimezone=Asia/Shanghai,否则新版驱动可能因为本地时区和服务器时区不一致直接抛异常。

2.4 严格模式与非严格模式下差别有多大

MySQL 的行为很大程度上取决于会话的sql_mode。你可以用这条命令查看当前模式:

SELECT @@sql_mode;

默认情况下,从 MySQL 5.7 开始,sql_mode里包含STRICT_TRANS_TABLES、NO_ZERO_DATE、NO_ZERO_IN_DATE这些严格选项。这意味着:

  • 插入'2024-02-30 10:00:00'这种非法日期,会直接报错Incorrect datetime value。
  • 插入'0000-00-00'这种零日期,也会被拒绝。
  • 插入2024-13-01这种不存在的月份,同样报错。

如果把严格模式关掉,MySQL 会退回到“尽量修复”模式:非法日期被转成0000-00-00 00:00:00,同时丢一条 Warning 给你。这不是“宽容”,这是埋雷——你以后查询这批数据时,会看到一堆0000-00-00,统计口径全乱。

我见过的生产事故里,有一类特别典型:某个历史系统用非严格模式跑了七八年,表里攒了一堆零日期,后来迁移到 MySQL 8.0,默认严格模式直接不认这些数据,迁移脚本报错一大片。所以我的建议是:新建库表一律保持默认严格模式,非法日期在应用层就拦截掉,把“2024-02-30”这种输入交给后端校验。

3. 格式化输出:让时间按你的想法说话

存储归存储,展示归展示。DATETIME 类型的默认输出虽然标准,但业务需求千奇百怪:有的要在列表页显示“08月15日 14:30”,有的要在报表里按“2024年08月”分组,有的要按小时统计在线分布。这些都要靠格式化函数处理。

3.1 DATE_FORMAT:最常用的格式化函数

DATE_FORMAT(date, format)接受两个参数,第一个是日期值,第二个是格式模板。关键点是模板里的格式符,这里挑最常用的整理成表:

格式符含义示例
%Y四位年份2024
%y两位年份24
%m两位月份08
%c月份,无前导零8
%M英文月份全名August
%d两位日15
%e日,无前导零15
%H24 小时制,两位14
%k24 小时制,无前导零14
%h12 小时制,两位02
%i分钟30
%s秒00
%pAM 或 PMPM
%T等价%H:%i:%s14:30:00
%W星期一~星期日英文全名Thursday

一个很典型的报表查询:

SELECT DATE_FORMAT(login_time, '%Y-%m-%d %H:%i') AS login_time_str FROM user_login_log ORDER BY login_time DESC;

这里有个细节特别容易踩坑:格式符的大小写含义不同。%Y是四位年,%y是两位年;%H是 24 小时制,%h是 12 小时制;%M是英文月份名,%m才是数字月份。我见过有人写date_format(create_time, '%Y-%m-%d %h:%i:%s'),结果下午三点显示成“03”,列表里出现 03 和 02 完全分不清。这个坑没有任何报错,纯粹肉眼难查。

3.2 STR_TO_DATE:反向解析字符串

真正折腾人的不是从 DATETIME 输出字符串,而是用户或上游系统给你一个乱七八糟的日期字符串,你要把它变成 DATETIME 存进表。比如前端上传一个15/08/2024 14:30,后端在应用层可以自己解析,也可以在 SQL 里用STR_TO_DATE()处理:

SELECT STR_TO_DATE('15/08/2024 14:30', '%d/%m/%Y %H:%i'); -- 结果:2024-08-15 14:30:00

再比如处理中文环境常见的日期格式:

SELECT STR_TO_DATE('2024年8月15日 14时30分', '%Y年%m月%d日 %H时%i分'); -- 结果:2024-08-15 14:30:00

STR_TO_DATE()的解析规则和DATE_FORMAT()的格式符基本一致,所以会写格式化就能写反解析。有一点要提醒:STR_TO_DATE()对非法日期的容忍度同样受sql_mode影响,严格模式下解析失败返回 NULL 或者报错,建议在应用层先校验数据格式,再决定是否走数据库解析。

3.3 时区问题:DATETIME 真的安全吗

这里必须把话说透:DATETIME 类型本身不存储时区。你插进去'2024-08-15 14:30:00',查出来还是2024-08-15 14:30:00,不管你怎么改数据库的time_zone,它都不会自己变。这个特性既是优点也是陷阱。

陷阱在于,很多人以为 DATETIME 不受时区影响,于是高枕无忧。但实际受影响的是“写入时的连接时区”和“查询时的展示逻辑”。比如,你在 Java 里setTimestamp()传入一个Timestamp,驱动会按 JVM 本地时区转换成会话时区对应的字符串再发给 MySQL。如果你的 JVM 是 UTC,数据库会话是+08:00,插进去的时间就比预期慢了 8 小时。这类问题不在 DATETIME 本身,而在驱动和连接的转换层。

反过来,TIMESTAMP 类型的时区转换是数据库内部干的,存的时候从会话时区转成 UTC,查的时候从 UTC 转回会话时区。举个例子,同一个值,你把会话时区改成+00:00再查 TIMESTAMP,结果就会平白少了 8 小时;而 DATETIME 完全不受影响。

所以多时区项目里,我的习惯是:所有业务表统一用 DATETIME 存 UTC 字面量,插入时在服务端生成UTC_TIMESTAMP(),展示时由前端或后端统一转成用户本地时区。这样既避开了 TIMESTAMP 的 2038 限制,又不会因为数据库会话时区设置不同而数据错乱。

3.4 日期计算与分组统计的常见写法

格式化不止是为了显示,还能配合日期运算、分组统计做报表。常用的查询套路这几个足够覆盖 90% 场景:

按天统计登录人数,正确写法用半开区间:

SELECT DATE(login_time) AS login_date, COUNT(DISTINCT user_id) AS uv FROM user_login_log WHERE login_time >= '2024-08-01 00:00:00' AND login_time < '2024-09-01 00:00:00' GROUP BY login_date ORDER BY login_date;

这里特别想说一个索引问题。很多人习惯写WHERE DATE(login_time) = '2024-08-15',你去看执行计划,通常是全表扫描。因为DATE(login_time)对列做了函数运算,索引直接失效。改成范围写法后,如果login_time上有索引,就能走到 range 访问,几千万行的表查询速度完全两个量级。

按小时统计分布:

SELECT HOUR(login_time) AS hour_of_day, COUNT(*) AS cnt FROM user_login_log WHERE login_time >= DATE_SUB(CURRENT_DATE, INTERVAL 7 DAY) GROUP BY hour_of_day ORDER BY hour_of_day;

计算两个时间之间的间隔:

SELECT TIMESTAMPDIFF(SECOND, created_at, finished_at) AS duration_sec FROM task_log WHERE id = 123;

DATEDIFF只计算日期差,忽略时间;TIMESTAMPDIFF可以精确到秒、分钟、小时,实际业务里我更常用后者。

4. 实战踩坑实录:那些年 DATETIME 给我挖的坑

格式讲完,这里才是真正值钱的部分。下面几个坑我都在生产环境见过,有的排查了一下午,有的差点数据迁移失败。整理成速查表放在最前面,谁遇到直接对号入座。

现象可能原因解决办法
插入2024-02-30直接报错严格模式开启前端加校验,拒绝非法日期
查询结果和实际时间差 8 小时连接层时区不一致统一 JDBCserverTimezone,确认数据库时区
DATETIME 值为0000-00-00 00:00:00非严格模式下插入了非法日期数据清洗,应用层校验
查询结果带着.000000表结构误定义成DATETIME(6)改用DATETIME或用DATE_FORMAT截断
用DATE(login_time)查询特别慢索引列被函数包裹改成范围查询

4.1 非法日期和零日期:一次前端传参引发的血案

有次我们接一个活动报名系统,前端直接传2024-02-30 10:00:00,数据库是 MySQL 8.0,严格模式开着,插入时直接报错。运维一阵排查后才发现是某个动态表单没有做日期合法性校验。

当时我们的处理方案是在后端统一加一层日期校验,白名单格式且用LocalDate.parse()验证合法性,非法输入直接返回参数错误。还有人提议把数据库的严格模式关掉,让 MySQL 自己转成0000-00-00。这个方案被否了,理由很简单:零日期对统计是灾难,COUNT、BETWEEN、排序都会出现诡异结果,为了省一个校验函数不值得。

顺带提一句,如果你接手的老库里已经有零日期,清洗时可以用STR_TO_DATE结合条件更新,把无效日期修正或置成 NULL。别留着,否则报表一出来全是坑。

4.2 客户端看着时间不对,锅在连接时区

另一个高频事故是“明明是 DATETIME,怎么查出来多了 8 小时”?DATETIME 本身不乱变,变的是连接层。

一次线上问题:运维反馈某张表的时间比真实时间晚了 8 小时。查了半天,发现是 Java 服务部署在 Docker 容器里,容器的系统时区是 UTC,而 MySQL 的time_zone是+08:00。JDBC 驱动在把java.sql.Timestamp转成 SQL 语句时,默认按 JVM 时区转,结果从 UTC 转出来就少了 8 小时。

解决办法是把 JDBC 连接串里的serverTimezone显式指定成Asia/Shanghai,同时容器里不要用 UTC 系统时间,要么RUN ln -sf /usr/share/zoneinfo/Asia/Shanghai /etc/localtime,要么启动参数里加-e TZ=Asia/Shanghai。

我的经验是:新版 JDBC 驱动对时区检查越来越严格,宁可早点显式声明,别依赖系统默认值。数据库服务器、应用服务器、JVM 时区三者保持一致,是最省心也是最容易被忽略的原则。

4.3 用字符串存时间的后遗症

有个项目早期表结构设计随意,create_time字段用的varchar(50),数据格式五花八门:有的是2024-08-15,有的是2024/8/15 14:30,还有15-Aug-2024。后来要做按月统计,查询直接崩溃,GROUP BY MONTH(create_time)一半行返回 NULL,排序更是乱成一锅粥。

后来我们做了一次彻底清洗:加一个DATETIME列,用STR_TO_DATE分批把能解析的都转过去,解析失败的单独标记处理。整个过程最痛苦的不是写 SQL,而是数据格式太多导致解析规则左堆右补。

这个案例想说明一个理念:时间字段就是时间字段,别为了一时省事用字符串存时间。DATETIME 的 8 字节换来的是排序、比较、索引、统计全流程可靠,这种底子上的优势是字符串完全给不了的。哪怕多花一点时间设计表结构,也别把时间存成 varchar。

4.4 自动初始化与默认值陷阱

MySQL 5.6.5 之前,只有 TIMESTAMP 能用DEFAULT CURRENT_TIMESTAMP自动填充,DATETIME 并不支持。所以很多老项目里会看到这种写法:一个TIMESTAMP自动填充创建时间,一个DATETIME由应用层赋值。5.6.5 之后 DATETIME 也可以了,直接用:

CREATE TABLE demo ( id INT PRIMARY KEY, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );

ON UPDATE CURRENT_TIMESTAMP的作用是这行数据只要发生 UPDATE,时间字段就自动刷新。这个特性好用,但有个细节要当心:如果你只是误操作更新了无关字段,它也会把updated_at改掉。想在更新时保留原时间,显示指定updated_at = updated_at即可。

另一个坑是ALTER TABLE给已有表加列时写上DEFAULT CURRENT_TIMESTAMP,所有存量行会被填充为执行 ALTER 的时间。有些场景这不是你要的,尤其是你想给历史数据补一个“默认创建时间”时,务必先想清楚填充策略,别让历史数据的时间全都变成迁移当天。

5. 一个完整案例:用户登录日志表

最后用一个我曾经实际做过的登录日志模块,把前面所有知识点串起来。这个案例的完整 SQL 直接可以抄。

5.1 表结构设计

CREATE TABLE user_login_log ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, user_id BIGINT UNSIGNED NOT NULL, login_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, login_date DATE GENERATED ALWAYS AS (DATE(login_time)) STORED, ip VARCHAR(45) DEFAULT NULL, device VARCHAR(100) DEFAULT NULL, PRIMARY KEY (id), KEY idx_user_time (user_id, login_time), KEY idx_login_date (login_date) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这里有几个设计点解释一下:

  • login_time用 DATETIME,不用 TIMESTAMP。因为登录日志主要做统计和展示,不涉及多时区换算,DATETIME 更稳。
  • login_date是 MySQL 5.7+ 才支持的存储生成列。它把login_time的日期部分单独提取出来存成一列,并建了索引。这样按天查询时可以直接走login_date索引,而不会因为DATE(login_time)函数包裹导致索引失效。
  • 索引idx_user_time设计成(user_id, login_time)联合索引,既能覆盖“某个用户最近登录记录”的查询,也能支撑“某个时间段内活跃用户”统计。

5.2 插入与查询完整示例

插入一条登录日志,最稳妥的方式是用参数化,SQL 里直接写NOW()也可以,但如果你希望统一用 UTC 字面量,就写UTC_TIMESTAMP():

INSERT INTO user_login_log (user_id, login_time, ip, device) VALUES (1001, NOW(), '192.168.1.10', 'Chrome/Windows');

批量插入多条一模一样的时间,用NOW()会让所有行保持一致,用SYSDATE()则可能出现毫秒级差异,这点上面已经说过。

查询最近 20 条登录记录并格式化显示:

SELECT user_id, DATE_FORMAT(login_time, '%Y-%m-%d %H:%i:%s') AS login_time_str, ip, device FROM user_login_log ORDER BY login_time DESC LIMIT 20;

统计 8 月份每日活跃用户数:

SELECT login_date, COUNT(DISTINCT user_id) AS uv FROM user_login_log WHERE login_date >= '2024-08-01' AND login_date < '2024-09-01' GROUP BY login_date ORDER BY login_date;

统计最近 7 天每个小时的登录量分布:

SELECT HOUR(login_time) AS hour_of_day, COUNT(*) AS cnt FROM user_login_log WHERE login_time >= CURRENT_DATE - INTERVAL 7 DAY GROUP BY hour_of_day ORDER BY hour_of_day;

5.3 从运维视角看 DATETIME

表上线后,DBA 那边的检查清单我也列一下。第一,确认所有写入时间的服务连接串都显式声明了时区,避免驱动层暗改。第二,大表的时间字段必须建索引,并且查询尽量用区间条件,别用函数包裹索引列。第三,备份恢复或者数据导出时注意--tz-utc这类参数,MySQL 客户端默认会把 TIMESTAMP 转成 UTC 输出,DATETIME 不受影响,但容易让不熟悉的人误判。

从我个人的实践经验来说,DATETIME 在绝大多数业务场景里是比 TIMESTAMP 更省心的选择。只要在建表时想清楚要不要小数秒,写入时统一走参数化与标准格式,查询时别在索引列上做函数运算,格式化显示交给DATE_FORMAT,这个类型几乎不会给你找麻烦。真遇到时间错乱,优先检查连接时区,而不是怀疑 DATETIME 本身。把基础的事情做对,后面就能少熬好几个夜。

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

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

立即咨询