数据库日期时间设计 - 选择正确的列类型
在 DATE、TIME、TIMESTAMP 和 TIMESTAMPTZ 之间的选择,决定了应用程序能否正确处理时区。本文详解预约系统、未来事件调度、审计日志,以及从忽略时区的遗留架构中迁移的策略。
MySQL的时区行为由三个层次控制:在my.cnf中设置的服务器级默认值、通过SET time_zone调整的会话级值,以及列类型本身(TIMESTAMP与DATETIME)。大多数时区bug都源于对这三个层次的混淆。知道针对特定现象应该修改哪一层,是可靠运维的基础。
服务器默认值通过my.cnf文件中的default-time-zone配置。会话值可在连接后通过SET time_zone = '+09:00'修改,大多数ORM驱动会在建立连接时自动执行此操作。而列的行为则完全取决于建表时选择的类型,运行时无法更改。
TIMESTAMP在内部以UTC存储值,读取时转换为会话的time_zone。这意味着处于不同时区的客户端对同一底层时间点会看到一致的本地时间。有效范围从1970-01-01 00:00:01 UTC到2038-01-19 03:14:07 UTC,因为该值是32位的Unix纪元秒,这一上限在整个8.0系列都没有改变。MySQL 8.0.28在64位平台上扩大的是UNIX_TIMESTAMP()与FROM_UNIXTIME()可接受的参数范围(可达3001年),而TIMESTAMP列类型的上限仍然是2038年。
DATETIME不具备时区概念,会按写入时的原样保留该值,不做任何时区转换。无论会话设置如何,同一值读取结果相同,有效范围从1000-01-01到9999-12-31。DATETIME看起来更简单,但在多时区系统中正确使用反而更困难,因为存储值的含义依赖于数据库之外的上下文。
| 对比项 | TIMESTAMP | DATETIME |
|---|---|---|
| 实际存储的内容 | 在内部转换为UTC之后的时间点 | 写入时的原样值,例如2026-05-20 10:00:00 |
| 读取时是否依赖time_zone | 依赖。读取时转换为会话的time_zone | 不依赖。无论会话设置如何,同一值读取结果相同 |
| 有效范围 | 1970-01-01 00:00:01 UTC到2038-01-19 03:14:07 UTC(源于32位Unix纪元秒表示) | 1000-01-01到9999-12-31 |
| DEFAULT CURRENT_TIMESTAMP | 可用。当前时间按会话的time_zone转换为UTC后存入 | 从MySQL 5.6起可用,但更改服务器time_zone会悄然改变此前写入值的含义 |
| 由什么确定值的含义 | 由类型本身确定,因为它始终表示绝对时间点 | 由数据库之外的上下文确定,需另行记录该值属于哪个时区 |
新项目的推荐做法是:将服务器time_zone固定为UTC,使用TIMESTAMP类型,并把显示时的时区转换集中在应用层。
连接后执行SET time_zone = 'Asia/Tokyo',后续的TIMESTAMP读取、NOW()和CURRENT_TIMESTAMP()都将使用指定的时区。要使用IANA时区名称,服务器必须通过mysql_tzinfo_to_sql加载时区数据。许多MySQL官方Docker镜像不包含此数据,因此SET time_zone = 'Asia/Tokyo'会报ERROR 1298,除非你先加载相关数据。
如果加载IANA数据不切实际,可以使用偏移量表示法如SET time_zone = '+09:00'。这适用于全年偏移量固定的时区,但无法表示夏令时。对于需要处理夏令时的区域(如美国或澳大利亚),IANA名称必不可少,前期加载tzdata的工作是值得的。
MySQL Connector/J在JDBC URL中接受serverTimezone参数,8.0+版本中为connectionTimeZone。现代驱动版本推荐值为connectionTimeZone=SERVER(遵循服务器端设置)或显式指定IANA时区名,其默认值为LOCAL,即JVM的默认时区。若不指定该参数,正是这个默认值使Java的Instant或OffsetDateTime值在写入时被静默偏移。另外,仅设置connectionTimeZone并不会改变服务器会话变量time_zone,因此当会话也需要随之改变时,要同时设置forceConnectionTimeZoneToSession=true。
常见的生产事故是:代码在本地正常运行(因为开发者的JDK和MySQL都是JST),但在生产环境的UTC服务器上产生9小时的偏移。修复方法是在JDBC URL中显式指定connectionTimeZone,并将所有java.util.Date的用法迁移到java.time类型(如Instant或OffsetDateTime)。Date携带隐式的本地时区语义,JDBC层会以出人意料的方式进行转换。
TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP这一模式广泛用于created_at和updated_at列。当前时间取自会话的time_zone,然后以UTC形式内部存储。如果服务器的time_zone为UTC,存储值与Unix纪元秒精确对应。如果为JST,经转换后同样成立,但后续更改服务器的time_zone不会追溯地重新解释已有行。
从MySQL 5.6开始,DATETIME列也可以使用CURRENT_TIMESTAMP作为默认值,但DATETIME不存储时区信息,因此更改服务器time_zone会改变此前写入的所有值的含义。这使得服务器time_zone在数据积累后成为几乎不可逆的决定,任何变更都应在充分了解历史数据解释方式的前提下规划。
对于新项目,建议将服务器time_zone设为UTC,使用TIMESTAMP列,并在应用代码中以Instant或OffsetDateTime表示时间值。只有显示层才将时间转换为用户的本地时区。这消除了一整类bug,并使多区域部署变得简单,因为每个副本对存储值的解释完全一致。
对于运行在JST服务器上并使用DATETIME列的现有系统,无法一夜之间切换。务实的方案是:新增列使用TIMESTAMP,在JDBC URL中显式设置connectionTimeZone,日志时间戳带上明确的UTC偏移量,并记录每个现有列的预期含义。分几个季度逐列收紧schema,远比一刀切的全面迁移安全得多。
这篇文章对您有帮助吗?
在 DATE、TIME、TIMESTAMP 和 TIMESTAMPTZ 之间的选择,决定了应用程序能否正确处理时区。本文详解预约系统、未来事件调度、审计日志,以及从忽略时区的遗留架构中迁移的策略。
国际商务出差中,时差反应可能让第一天完全浪费,或者让重要会议安排在认知低谷时段。本文涵盖出发前准备、飞行策略、会议时段安排、与总部的异步协作,以及保护出差后工作的恢复计划。
以本地时间配置的 cron 任务在夏令时切换期间会静默地重复执行或跳过。本文详解其故障模式,介绍以 UTC 运行调度的方案、Kubernetes CronJob 的 timeZone 字段,以及云调度器如何处理同样的问题。