MySQL日期类型选择建议
存时间看起来是小事,选错类型却会在时区切换、跨年范围、跨系统对接时连环爆雷。MySQL 里主流的三种选择是 DATETIME、TIMESTAMP 和 BIGINT 时间戳,它们的差异不只在存储大小,更在于”存的是字面量还是绝对时刻”。
三种类型的原理差异
DATETIME:存的就是你写入的那个字面量,不带任何时区概念。插入 '2026-09-01 10:00:00',无论会话时区怎么改,读出来都是这个值。取值范围 1000-01-01 到 9999-12-31,5.6.4 之后占 5~8 字节(取决于小数秒精度)。
TIMESTAMP:存的是绝对时刻。写入时 MySQL 把当前会话时区的时间换算成 UTC 存储,读出时再换算回当前会话时区。同一行数据,SET time_zone = '+8:00' 前后查到的显示值会不同,但指向同一时刻。它只占 4~7 字节,代价是范围只有 1970-01-01 到 2038-01-19——这就是著名的 2038 问题。另一个特性是支持 DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,行更新时自动刷新,做”最后修改时间”很方便。
BIGINT:存 Unix 时间戳(秒或毫秒),数据库完全不参与解释,时区换算全在应用层。跨语言、跨系统对接时语义最明确——所有系统对同一个整数的理解一致。
排序与索引考量
三种类型按时间排序都没问题,坑在查询写法上:
- 用 VARCHAR 存时间是首先要排除的方案:字典序不等于时间序(
'2024-1-10'会排在'2024-05-01'前面),日期函数用不了,索引范围扫描也低效。 - 对时间列做范围查询时,不要在列上套函数。
WHERE DATE(create_time) = '2026-09-01'会让索引失效;应改成WHERE create_time >= '2026-09-01' AND create_time < '2026-09-02',让索引列保持”干净”才能走范围扫描。 - BIGINT 毫秒值是 19 位整数,比较和索引效率略好于 DATETIME 的字节比较,但这点差距在 B+ 树定位面前通常不是决定性因素,别为这点性能牺牲可读性。
CREATE TABLE event ( |
时区问题的根源
多数时间 bug 不是类型选错,而是各环节时区不一致:JDBC URL 的 serverTimezone、JVM 默认时区、MySQL 全局/会话 time_zone、容器系统时区,任何一处漂移都会让 TIMESTAMP 读出”差 8 小时”的值。DATETIME 看起来免疫,实际上是把换算责任推给了应用——应用层一旦假设错了时区,错得更隐蔽。
全球化部署的通行做法是:存储统一 UTC,展示按用户时区本地化。应用连库时会话时区固定为 UTC,ORM 层统一转换,集成测试覆盖夏令时切换日(某些时刻一天会出现两次或零次)。
分布式环境的建议
- 跨服务交换一律用 Unix 毫秒或 ISO 8601 带时区格式(如
2026-09-01T02:00:00Z),避免”裸”的yyyy-MM-dd HH:mm:ss字符串在上下游被按不同时区解读。 - ID 与时间混排的场景(如按时间分库分表),BIGINT 时间戳做运算最顺手。
- 主从复制:TIMESTAMP 在 binlog 中按 UTC 记录,从库回放时按从库会话时区换算,主从时区不一致会导致显示偏差,这也是统一 UTC 的另一个理由。
选型结论
| 场景 | 建议 |
|---|---|
| 业务时间(下单、出行、生日等) | DATETIME(3),语义直观、范围无忧 |
| 记录创建/更新时间 | TIMESTAMP,自动维护省心(注意 2038 上限) |
| 跨系统对接、日志归档、分布式链路 | BIGINT 毫秒,语义零歧义 |
| 任何场景 | 不要用 VARCHAR 存时间 |
小结
- DATETIME 存字面量、范围大、与时区无关;TIMESTAMP 存 UTC 绝对时刻、随会话时区显示、2038 年到期。
- BIGINT 把时区责任完全交给应用,跨系统最不容易出歧义。
- 范围查询别对时间列套函数,保持索引列干净。
- 全局统一”存 UTC、展示本地化”,并让 JDBC、JVM、MySQL、容器四处时区对齐。

