PostgreSQL timestamp 列里到底存了什么?
用通俗语言讲解 PostgreSQL 的 timestamp 与 timestamptz 的存储机制、时区陷阱,以及如何为你的场景选择正确的类型。
Go Tools Team 6 分钟
PostgreSQL timestamp 列里到底存了什么?
PostgreSQL 中的 timestamp 和 timestamptz 都以同一种方式存储:一个 64 位整数,表示距离 1970-01-01 00:00:00 UTC 的微秒数。区别仅在于数据格式化给人看时的行为。
为什么容易踩坑?
- 两个列、一个日期……却查出两个不同的结果
- 你的应用插入了
2025-07-29 10:00,但另一个团队看到的是02:00 - 前端渲染的 ISO 字符串跟后端日志对不上
两罐桃子:一罐无标签,一罐有标签
| 数据类型 | 官方名称 | 存储值 | SELECT 时的行为 |
|---|---|---|---|
timestamp | timestamp without time zone | 原始微秒计数 | 原样返回 — Postgres 不做任何时区猜测 |
timestamptz | timestamp with time zone | 同样的微秒计数 | Postgres 在发送前应用会话的 TimeZone 设置 |
类比
timestamp= 一罐没贴产地标签的桃子。你知道是水果,但不知道在哪儿罐装的。timestamptz= 一罐印着”产地 UTC+8”的桃子。打开的人可以自行决定是否换算。
底层原理:就是一个大数字
2000-01-01 00:00:00 UTC → 0
2000-01-01 00:00:01 UTC → 1 000 000
- 单位:微秒(百万分之一秒)
- 范围:公元前 4713 年 – 公元 294276 年
timestamp和timestamptz的存储完全一样;不同的只是解释方式
15 秒速览 Demo
-- 客户端使用上海时区
SET TimeZone = 'Asia/Shanghai';
CREATE TABLE demo (
created_ts timestamp,
created_tz timestamptz
);
INSERT INTO demo VALUES ('2025-07-29 10:00', '2025-07-29 10:00');
| 查询 | 结果 | 原因 |
|---|---|---|
SELECT created_ts FROM demo; | 2025-07-29 10:00:00 | 原始值,不做时区运算 |
SELECT created_tz FROM demo; | 2025-07-29 10:00:00+08 | 输出时贴上标签 |
SET TimeZone = 'UTC'; 后查询 | 2025-07-29 02:00:00+00 | 同一时刻,换个视角 |
常见陷阱与快速修复
1. 不同用户,不同时钟
- 原因:各客户端使用不同的
TimeZone设置来读取timestamptz - 修复:要么全部用
timestamp并约定统一时区,要么在连接初始化时强制SET TimeZone = 'UTC'
2. 用错类型存”墙上时间”
- 业务日历(营业时间、截止日期)应使用
timestamp - 跨境业务流(订单、日志)应以 UTC 存入
timestamptz
3. API 漂移
timestamptz始终以 ISO-8601 带偏移量的字符串传输(Z或+08:00)- 让前端 UI 根据用户本地时区格式化
速查表:该用哪个?
仅本地日历 → timestamp
涉及全球 → timestamptz(存 UTC)
- 财务报表、排课表 →
timestamp - 审计日志、电商订单 →
timestamptz
用 Go Tools 快速验证
| 需求 | 工具 | 用法 |
|---|---|---|
| 查看 SQL 中的 epoch 值 | 时间戳转换器 | 粘贴 1690622400,点击转换 |
| 一眼比较两个时区 | 时区转换器 | 输入 10:00 Asia/Shanghai |
| 整理含时间字段的 JSON | JSON 格式化工具 | 粘贴数据,一键美化 |
所有工具完全在浏览器中运行 — 数据不会离开你的电脑。
总结
- Postgres 的两种时间类型都是微秒计数器;标签才是唯一的区别
- 选错类型意味着令人困惑的时间戳和错误的计算
- 善用工具测试、转换和验证,可以节省数小时的调试时间