适用版本:MySQL 8.0+ / Laravel 10+。MySQL 5.7 已 EOL,文中不再覆盖。
一句话结论
- 「物理时刻」(事件发生于何时)→
TIMESTAMP,自动跟随会话时区转换 - 「字面值」(合同上写的、用户输入的日期)→
DATETIME,原样存取不转换 - 仅日期 →
DATE;仅时长 →TIME;JS/跨语言毫秒戳 →BIGINT - 永远不要用
VARCHAR存日期——无强制约束、无法范围查询、排序按字符串
选型对照表
| 类型 | 范围 | 字节 | 时区行为 | 小数秒 | 典型场景 |
|---|---|---|---|---|---|
DATETIME | 1000-01-01 00:00:00 ~ 9999-12-31 23:59:59 | 5 + fsp | 字符串原样存取,不转换 | DATETIME(0~6) | 合同生效时点、用户填写的本地时间、历史事件 |
TIMESTAMP | 1970-01-01 00:00:01 UTC ~ 2038-01-19 03:14:07 UTC | 4 + fsp | 写入:会话时区 → UTC;读取:UTC → 会话时区 | TIMESTAMP(0~6) | created_at / updated_at、日志、跨时区事件戳 |
DATE | 1000-01-01 ~ 9999-12-31 | 3 | — | — | 生日、纪念日、报表日期 |
TIME | -838:59:59 ~ 838:59:59 | 3 + fsp | — | TIME(0~6) | 营业时长、计时器 |
YEAR | 1901 ~ 2155 | 1 | — | — | 罕用 |
BIGINT | INT64 | 8 | 应用层控制 | — | 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)
DATETIME 与 TIMESTAMP 都支持自动初始化与自动更新——这点 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 时区 | UTC | config/app.php → timezone |
timestamps() 列 | TIMESTAMP | Schema::create → $table->timestamps() |
softDeletes() | TIMESTAMP | $table->softDeletes() |
| Eloquent date cast | Carbon 实例 | created_at / updated_at 自动;其它字段在 casts() 显式 |
| 序列化精度(Laravel 11+) | 微秒 | DATETIME(6) / TIMESTAMP(6) |
| 序列化格式 | UTC ISO-8601(YYYY-MM-DDTHH:MM:SS.uuuuuuZ) | 默认 |
推荐做法(全球化项目)
存储侧统一 UTC:
mysqld default-time-zone='+00:00'、config('app.timezone') = 'UTC'。Laravel 官方原话「strongly encouraged」。created_at/updated_at走 Laravel 默认TIMESTAMP——自动跟随会话时区。业务时间字段(合同生效、活动开始、订单期望送达)用
DATETIME(6)——这些是「字面值」,不该被时区影响。展示层按用户时区转换:
$model->created_at ->setTimezone(auth()->user()->timezone ?? 'UTC') ->format('Y-m-d H:i:s');
容易踩的坑
created_at序列化总是 UTC,不受app.timezone影响。 这是 Laravel 12 文档明确写的——timestampcast 字段「always formatted in UTC, regardless of the application’s timezone setting」。想本地化展示请走 cast 自定义或显式setTimezone。Carbon::now()vsDB::raw('NOW()')不等价。 前者用 PHPapp.timezone,后者用 MySQL 会话时区。两边不一致时会写入错位的「物理时刻」。统一 UTC 是最稳的根治方案。PDO 连接默认不发
SET time_zone,沿用@@global.time_zone。 想在连接级隔离,在config/database.php的 mysql 驱动加:'options' => [ PDO::MYSQL_ATTR_INIT_COMMAND => "SET time_zone = '+00:00'", ],$table->timestamp('paid_at')不会自动加ON UPDATE CURRENT_TIMESTAMP——需要时显式->useCurrentOnUpdate(),默认值用->useCurrent()。JS 互操作存毫秒:用
BIGINT比让 JS 解析 MySQL DATETIME 字符串少很多麻烦。
References
- MySQL 8.0 Reference — Date and Time Data Types
- MySQL 8.0 Reference — DATE, DATETIME, and TIMESTAMP
- MySQL 8.0 Reference — Automatic Initialization and Updating for TIMESTAMP and DATETIME
- MySQL 8.0 Reference — MySQL Server Time Zone Support
- Laravel — Date Casting, Serialization, and Timezones
– EOF –