先给结论:2026 年的新表,时间字段默认选 DATETIME——没有 2038 年上限、存进去什么读出来就是什么、不受连接时区干扰。TIMESTAMP 只在一个场景不可替代:同一份数据要按访问者所在时区展示。至于"谁更快、谁省空间"这些老黄历差异,MySQL 8 时代基本可以忽略。

下面把这个结论拆开讲透。

一、本质区别:一个存字面时间,一个存 UTC 秒

DATETIME 存的是字面时间:写入 2026-08-26 10:30:00,磁盘上打包的就是这个时刻的二进制表示,读出来原样返回,跟时区无关。

TIMESTAMP 存的是一个 UTC 时刻:写入时按当前会话时区换算成 UTC 秒数,读取时再按会话时区换算回来。注意"会话时区"四个字——它是连接级变量,同一个库、同样的数据,不同的连接配置能读出不同的显示值。

维度 DATETIME TIMESTAMP
存储语义 字面时间 UTC 秒数,读取时按时区换算
占用空间 5 字节 4 字节(均不含小数秒)
有效范围 1000-01-01 ~ 9999-12-31 1970-01-01 00:00:01 ~ 2038-01-19 03:14:07(UTC)
受 time_zone 影响
索引 / 排序行为 一致 一致

二、时区:TIMESTAMP 唯一的真本事,也是最大的坑

跑一段 demo 就明白(MySQL 8.x):

CREATE TABLE t_time_demo (
    dt DATETIME,
    ts TIMESTAMP
);

SET time_zone = '+08:00';
INSERT INTO t_time_demo VALUES ('2026-08-26 10:30:00', '2026-08-26 10:30:00');

SET time_zone = '+00:00';
SELECT * FROM t_time_demo;
-- dt = 2026-08-26 10:30:00   (原样返回)
-- ts = 2026-08-26 02:30:00   (换算成 UTC 展示)

做全球化产品,这个特性很香:柏林用户和上海用户看同一条"发布时间",各自看到本地时间。

但更多项目里,它是事故来源:

  • 连接串没显式设时区,测试环境和生产的 time_zone 不一致,同一行数据两边"长得不一样",肉眼核对全是坑
  • mysqldump 默认按 UTC 导出 TIMESTAMP 列(--tz-utc 开关),导出文件里的字面值和你在 +08:00 会话里查出来的差 8 小时,不了解机制的话,核对时全是"数据错了"的错觉
  • 按天做统计聚合,时区不同导致"今天"的边界漂移,凌晨的单子被算进前一天

DATETIME 所见即所得,这些坑天然不存在。展示层的时区转换交给应用——PHP 里 Carbon 一行 setTimezone() 的事,职责还更清晰。

三、2038:不是段子,是倒计时

TIMESTAMP 的上限是 2038-01-19 03:14:07 UTC——32 位 Unix 秒数到顶。从 2026 年算,只剩 12 年。

"12 年挺远"是错觉,两类字段今天就会踩雷:

生日、纪念日这类字段。 1980 年出生的人 2040 年满 60 岁;一张 20 年期的合同 2046 年到期。这些值往 TIMESTAMP 列里写,直接报错:

mysql> INSERT INTO t_ts (ts) VALUES ('2040-01-01');
ERROR 1292 (22007): Incorrect datetime value: '2040-01-01' for column 'ts' at row 1

严格模式下写入直接失败,不是截断、不是警告,是失败。只要字段的时间范围可能碰到 2038 年以后,DATETIME 就是唯一安全解。

四、性能:老黄历可以扔了

"TIMESTAMP 底层是整数所以查询快"——这句话在 MySQL 5.6.4 之后就不成立了。从那个版本起,两类时间类型都是紧凑二进制打包存储,走同一套 B+ 树索引,范围扫描、排序、比较行为一致。

真实差异只剩字节宽度:DATETIME 5 字节、TIMESTAMP 4 字节。算笔账:一张 5000 万行的表,时间索引每个条目差 1 字节,整棵索引差约 48 MB——在内存几十 GB 起步的服务器上,这点差距在查询耗时里测不出来。

选型时,性能这一栏可以直接划掉。

五、TIMESTAMP 的两大历史特权,MySQL 8 都没收了

早年间大家用 TIMESTAMP,一半原因是能白嫖两个魔法:不写默认值就自动 DEFAULT CURRENT_TIMESTAMP,不写更新行为就自动 ON UPDATE CURRENT_TIMESTAMP

现在这两个特权都没了:

  1. 自动更新:MySQL 5.6.5 起 DATETIME 同样支持这两个子句,写法完全对称
  2. 零值魔法默认:MySQL 8.0 起 explicit_defaults_for_timestamp 默认开启,TIMESTAMP 不再自动获得任何默认行为,必须显式声明
-- 8.0 之后,两种类型的建表写法完全对称
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP

顺手说 Laravel:migration 里 $table->timestamps() 建出来的是两列 TIMESTAMP NULL。多数业务不用恐慌——created_at 只存"当下",真出问题也是 2038 年实时到点才出。但如果是打算长期维护的产品,把关键业务表的时间列换成 datetime() 是低成本的未来保险。

六、还有第三个选项:int 时间戳

拿这个博客自己举例:articlesfiles 表的 created_at 用的就是 int unsigned 存 Unix 秒。

  • 优点:跨语言最通用、宽度固定、unsigned 能存到 2106 年
  • 缺点:裸查不可读(一串 1756181040)、日期函数要先 FROM_UNIXTIME() 换算

适合"机器消费为主"的字段:埋点、审计、缓存版本号。要给人看的业务时间,还是老老实实用 DATETIME。

七、决策表

场景 推荐 一句话理由
常规业务时间(下单、发布、审批) DATETIME 所见即所得,无 2038
生日 / 纪念日 / 证照到期 DATETIME(必须) 范围必然穿越 2038
全球化产品,按访问者时区展示 TIMESTAMP 唯一不可替代的场景
created_at / updated_at 审计列 DATETIME 或 int 别依赖已消失的魔法默认
海量大表的机器消费字段 int unsigned 便宜、通用、撑到 2106

总结成一句:默认 DATETIME;明确要"按访问者时区展示"才 TIMESTAMP;机器消费的极端场景用 int。

FAQ

Q:老表已经用了 TIMESTAMP,要马上迁移吗?

不用急,但别拖过 2030。改列类型是表重建操作(COPY 算法),大表会锁很久——用 pt-online-schema-change 或 gh-ost 在低峰期做,预留回滚窗口。

Q:要存毫秒、微秒怎么办?

两种类型都支持:DATETIME(3) 存毫秒、TIMESTAMP(6) 存微秒,代价是每档小数多占 1~3 字节。注意部分老版本 JDBC 驱动对小数秒的支持有坑,先升级驱动再上。

Q:DATETIME 不带时区,信息不就丢了?

会丢——但 TIMESTAMP 也没好到哪去:它存的是"UTC 时刻",同样不含"这是哪个时区的本地时间"这层语义。正解是团队约定库里统一存一个时区(全 UTC 或全 +08:00),展示层再转。

艾林博客 - 技术分享、开发经验与AI探索的个人技术博客
艾林博客 - 技术分享、开发经验与AI探索的个人技术博客

延伸阅读:

PHP常用数组函数解析 开发编程
PHP常用数组函数解析

PHP常用数组函数解析,完整解析核心函数,包含参数类型、模式常量、多维数组处理等高级技巧,提供简单到企业级的实战案例演示。

资源 后端 PHP

Valencio

/

2025-02-27

PHP 执行<span class="text-primary">时间与内存</span>管理解析 开发编程
PHP 执行时间与内存管理解析

本文详解PHP脚本的max_execution_time、memory_limit核心参数,对比Nginx与PHP-FPM的超时配置优先级,提供无限制执行脚本的安全方案和常见问题解决方案,帮助开发者彻底解决超时与内存溢出问题。

扩展 命令 后端 PHP

Valencio

/

2025-02-25