适用版本:MySQL 8.0+ / Laravel 10+。MySQL 5.7 已 EOL,文中不再覆盖。

一句话结论

  • 「物理时刻」(事件发生于何时)→ TIMESTAMP,自动跟随会话时区转换
  • 「字面值」(合同上写的、用户输入的日期)→ DATETIME,原样存取不转换
  • 仅日期 → DATE;仅时长 → TIME;JS/跨语言毫秒戳 → BIGINT
  • 永远不要用 VARCHAR 存日期——无强制约束、无法范围查询、排序按字符串

选型对照表

类型范围字节时区行为小数秒典型场景
DATETIME1000-01-01 00:00:00 ~ 9999-12-31 23:59:595 + fsp字符串原样存取,不转换DATETIME(0~6)合同生效时点、用户填写的本地时间、历史事件
TIMESTAMP1970-01-01 00:00:01 UTC ~ 2038-01-19 03:14:07 UTC4 + fsp写入:会话时区 → UTC;读取:UTC → 会话时区TIMESTAMP(0~6)created_at / updated_at、日志、跨时区事件戳
DATE1000-01-01 ~ 9999-12-313生日、纪念日、报表日期
TIME-838:59:59 ~ 838:59:593 + fspTIME(0~6)营业时长、计时器
YEAR1901 ~ 21551罕用
BIGINTINT648应用层控制JS 毫秒戳、Unix 微秒戳、跨语言互操作

小数秒(fsp,fractional seconds precision) 0~6 位,每 2 位多占 1 字节。Laravel 11+ 默认就用 DATETIME(6) / TIMESTAMP(6)

2038 注意TIMESTAMP 已剩 12 年寿命。新表对长期存储字段倾向 DATETIME(6)created_at 当前用 TIMESTAMP 仍可(届时官方会延或可迁移),但业务时间字段直接上 DATETIME 更稳。

自动初始化(DEFAULT / ON UPDATE)

DATETIMETIMESTAMP 都支持自动初始化与自动更新——这点 5.6 之前只有 TIMESTAMP 能用。

CREATE TABLE t (
  created_at TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP(6),
  updated_at TIMESTAMP(6) DEFAULT CURRENT_TIMESTAMP(6) ON UPDATE CURRENT_TIMESTAMP(6),
  paid_at    DATETIME(6) DEFAULT CURRENT_TIMESTAMP(6)
);

精度必须在 DEFAULT / ON UPDATE 间一致。

时区机制(核心)

三个变量

SHOW VARIABLES LIKE '%time_zone%';
-- system_time_zone: OS 时区(mysqld 启动时确定,运行期改 OS 不生效)
-- time_zone:        MySQL 实际使用的时区,'SYSTEM' 表示跟随 system_time_zone
-- 每个连接还有一份 @@session.time_zone

核心规则

类型写入读取
DATETIME字符串原样落盘字符串原样返回
TIMESTAMP会话时区解释字符串 → 转 UTC 落盘UTC → 会话时区字符串

推论:

  • 同一行 TIMESTAMP,UTC 会话读到 2026-05-17 12:00:00+08:00 会话读到 2026-05-17 20:00:00——同一物理时刻的两种表达
  • 同一行 DATETIME,两个会话读到的字符串一模一样——MySQL 不知道这字符串是哪个时区的

修改时区

SET GLOBAL time_zone = '+00:00';      -- 全实例(不需要 FLUSH PRIVILEGES,与权限无关)
SET time_zone = '+08:00';              -- 当前会话
SET time_zone = 'Asia/Shanghai';       -- 仅在已导入 IANA 表后可用

固化到配置:

# /etc/mysql/mysql.conf.d/mysqld.cnf
[mysqld]
default-time-zone = '+00:00'

导入 IANA 时区表

默认安装下 MySQL 只认 +08:00 这种数字偏移;要用 Asia/Shanghai 这类 IANA 名(处理夏令时更安全),先从系统 zoneinfo 导一次:

mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root -p mysql

⚠️ 不要用 CST 命名时区。 MySQL 报告的 CST 是歧义缩写——可能是 China / Central / Cuba Standard Time 三者之一,跨平台行为不可控。一律用 +08:00 或 IANA 名。

Laravel 实战

默认行为速查

默认位置
App 时区UTCconfig/app.phptimezone
timestamps()TIMESTAMPSchema::create$table->timestamps()
softDeletes()TIMESTAMP$table->softDeletes()
Eloquent date castCarbon 实例created_at / updated_at 自动;其它字段在 casts() 显式
序列化精度(Laravel 11+)微秒DATETIME(6) / TIMESTAMP(6)
序列化格式UTC ISO-8601(YYYY-MM-DDTHH:MM:SS.uuuuuuZ默认

推荐做法(全球化项目)

  1. 存储侧统一 UTCmysqld default-time-zone='+00:00'config('app.timezone') = 'UTC'。Laravel 官方原话「strongly encouraged」。

  2. created_at / updated_at 走 Laravel 默认 TIMESTAMP——自动跟随会话时区。

  3. 业务时间字段(合同生效、活动开始、订单期望送达)用 DATETIME(6)——这些是「字面值」,不该被时区影响。

  4. 展示层按用户时区转换

    $model->created_at
      ->setTimezone(auth()->user()->timezone ?? 'UTC')
      ->format('Y-m-d H:i:s');
    

容易踩的坑

  1. created_at 序列化总是 UTC,不受 app.timezone 影响。 这是 Laravel 12 文档明确写的——timestamp cast 字段「always formatted in UTC, regardless of the application’s timezone setting」。想本地化展示请走 cast 自定义或显式 setTimezone

  2. Carbon::now() vs DB::raw('NOW()') 不等价。 前者用 PHP app.timezone,后者用 MySQL 会话时区。两边不一致时会写入错位的「物理时刻」。统一 UTC 是最稳的根治方案。

  3. PDO 连接默认不发 SET time_zone,沿用 @@global.time_zone 想在连接级隔离,在 config/database.php 的 mysql 驱动加:

    'options' => [
      PDO::MYSQL_ATTR_INIT_COMMAND => "SET time_zone = '+00:00'",
    ],
    
  4. $table->timestamp('paid_at') 不会自动加 ON UPDATE CURRENT_TIMESTAMP——需要时显式 ->useCurrentOnUpdate(),默认值用 ->useCurrent()

  5. JS 互操作存毫秒:用 BIGINT 比让 JS 解析 MySQL DATETIME 字符串少很多麻烦。

References

– EOF –