DATETIME 存字面值无时区,TIMESTAMP 存UTC并自动按会话时区转换;误用会导致显示错乱、查询偏差;正确做法是明确时间语义:物理时刻用TIMESTAMP,本地事件用DATETIME并显式标注时区。

DATETIME 和 TIMESTAMP 的时区行为完全不同,选错类型或混用会导致时间显示错乱、查询结果偏差、跨时区数据不可比——这不是配置问题,是类型语义本身决定的。
为什么 DATETIME 字段不随 SET time_zone 变化?
DATETIME 存的是字面值,不是时间点。你插入 '2026-06-11 12:59:00',它就永远是这个字符串,不管会话时区设成 '+00:00' 还是 '+08:00',查出来都是 '2026-06-11 12:59:00'。
常见翻车场景:
• 应用层按本地时间生成 DATETIME 字符串写入,但服务器在 UTC 时区,结果所有时间比实际晚 8 小时
• 用 CONVERT_TZ(dt_col, 'SYSTEM', '+08:00') 却没意识到 dt_col 本身没时区信息,'SYSTEM' 是无效源时区参数
• 把 DATETIME 当作“带时区的时间”参与计算,比如 DATEDIFF(NOW(), dt_col) 在不同会话下结果不一致(因为 NOW() 随 time_zone 变,dt_col 不变)
TIMESTAMP 的自动转换到底在什么时候发生?
TIMESTAMP 的转换发生在**每次读写瞬间**,且严格依赖当前会话的 time_zone 值:
• 写入时:把输入值当作「当前会话时区的时间」→ 转成 UTC 存储
• 读取时:把存储的 UTC → 按当前会话时区转回显示
这意味着:
• 同一条记录,在上海查是 '2026-06-11 20:59:00',在纽约查是 '2026-06-11 08:59:00'(假设各自会话时区正确)
• 如果没显式设置会话时区,它会继承全局或系统时区,而 @@global.time_zone 可能是 SYSTEM,实际值取决于操作系统,极易不一致
• 服务器时区改了(比如从 SYSTEM 改成 '+08:00'),所有已存 TIMESTAMP 的显示值会集体跳变,但底层 UTC 值没动——这是隐式行为,不是 bug
如何安全地把 DATETIME 转成目标时区时间?
不能靠 SET time_zone,必须显式声明源时区和目标时区:
• 错误写法:SELECT CONVERT_TZ(created_at, '+00:00', @@session.time_zone) —— 假设 created_at 是 UTC,但实际它只是个字符串,没时区含义
• 正确前提:你得知道这个 DATETIME 值原本代表哪个时区的时间。比如日志记录明确是「北京时间」,那就该:SELECT CONVERT_TZ(created_at, '+08:00', '+00:00') AS utc_time
• 更稳妥的做法:应用层统一用 UTC 时间生成字符串,再以 TIMESTAMP 类型存;如果必须用 DATETIME,就在字段名或注释里注明其隐含时区(如 created_at_beijing),避免后续误用
• 注意:CONVERT_TZ() 依赖 MySQL 时区表(mysql.time_zone),需提前加载,否则返回 NULL
什么时候该用 UNIX_TIMESTAMP + FROM_UNIXTIME?
当你需要脱离 MySQL 时区机制、做确定性转换时:
• UNIX_TIMESTAMP(dt_col) 总是把 DATETIME 当作当前会话时区的时间,转成 UTC 秒数(即等价于 CONVERT_TZ(dt_col, @@session.time_zone, '+00:00'))
• FROM_UNIXTIME(ts_val) 总是按当前会话时区把秒数转回 DATETIME 字符串
• 所以,如果你确认所有 DATETIME 值都来自同一固定时区(比如全是东八区),可以用:FROM_UNIXTIME(UNIX_TIMESTAMP(dt_col) + 3600*8) 强制转成 UTC 字符串(但不如直接存 TIMESTAMP 干净)
• 关键限制:UNIX_TIMESTAMP() 输入超 2038 年会返回 0,FROM_UNIXTIME(0) 返回 '1970-01-01 08:00:00'(受会话时区影响),对历史/远期时间慎用
真正麻烦的不是函数怎么写,而是得先搞清你存的那个时间,到底想表达什么——是某个物理时刻(用 TIMESTAMP + 统一时区),还是某个本地日历事件(用 DATETIME + 显式标注时区)。混淆这两者,后面所有转换都是在补漏洞。











