timestamp 与 timestamptz:三个函数、两个时区、一个常见错误
时间类型出错通常不是语法问题,是语义问题:存进去的到底是"某个时刻"还是"墙上时间"。这两者在跨时区、跨夏令时的时候会分叉。
1. 两种类型
| 类型 | 存储 | 含义 | 字节 |
|---|---|---|---|
timestamp | 8 字节 | 不带时区的"墙上时间",存什么读什么 | 8 |
timestamptz | 8 字节 | 存 UTC 时刻,按会话 TimeZone 渲染 | 8 |
两者都是 8 字节(本机 pg_type.typlen 实测),所以选型不是空间问题,是语义问题。
规则很简单:记录"什么时候发生"用 timestamptz;记录"当地几点"(比如营业时间、闹钟)用 timestamp。混着用就会出错。
2. 三个"现在"
SELECT now(), statement_timestamp(), clock_timestamp();
本机实测输出:
2026-11-10 05:52:57.8938+00 | 2026-11-10 05:52:57.8938+00 | 2026-11-10 05:52:57.894057+00
now() 与 statement_timestamp() 在这一行里相同,因为都在语句开始时取值;clock_timestamp() 是真正调用时刻。差别在长事务里才显现:
| 函数 | 取值时机 | 用途 |
|---|---|---|
now() / transaction_timestamp() | 事务开始 | 同一事务内保持一致,适合记业务时间 |
statement_timestamp() | 语句开始 | 语句级审计 |
clock_timestamp() | 函数调用瞬间 | 计时、测量耗时 |
用 now() 给循环里的多行打时间戳,会得到完全相同的时间——这通常是想要的;想测每步耗时,就得用 clock_timestamp()。
3. 三个时区坑
列类型写成 timestamp 却存 now()。now() 返回 timestamptz,插进 timestamp 列会按会话时区转成本地墙上时间,丢失时区信息。之后换一个时区的会话去读,得到的是"错的时刻"。这类表的典型症状是:报表在东八区正确,UTC 环境下差 8 小时。
跨夏令时。timestamptz 存的是绝对时刻,没有问题;但用 timestamp 做"每天 9 点"这类计算时,夏令时切换当天可能多一小时或少一小时。
AT TIME ZONE 的方向容易记反。timestamptz AT TIME ZONE 'Asia/Shanghai' 得到的是该时区的墙上时间(timestamp);反过来 timestamp AT TIME ZONE 'Asia/Shanghai' 得到的是对应的绝对时刻(timestamptz)。记法:AT TIME ZONE 总是"理解为"后面那个时区。
4. 建表与查询建议
-- 推荐:存绝对时刻
CREATE TABLE events (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
created_at timestamptz NOT NULL DEFAULT now()
);
-- 按时区做日聚合:先转墙上时间再截断
SELECT date_trunc('day', created_at AT TIME ZONE 'Asia/Shanghai') AS d, count(*)
FROM events GROUP BY d ORDER BY d;
小结
timestamptz 是默认选择:它存绝对时刻,展示交给会话时区。timestamp 只在"这个时间与地点绑定"的场景下用,比如门店营业时间。
三个函数的区别记住一句话就够:now() 跟事务、statement_timestamp() 跟语句、clock_timestamp() 跟调用。混用它们不会报错,只会让时间戳看起来对、算起来错。
数据来源与检索时间
检索时间 2026-11-10。字节数与函数输出为本机 18.6 实测。
| 来源 | URL | 对应数据 |
|---|---|---|
| PostgreSQL 18 文档《8.5 日期/时间类型》 | https://www.postgresql.org/docs/18/datatype-datetime.html | timestamp 与 timestamptz 的存储语义、8 字节宽度、时区处理规则 |
| PostgreSQL 18 文档《9.9 日期/时间函数》 | https://www.postgresql.org/docs/18/functions-datetime.html | now()/transaction_timestamp()/statement_timestamp()/clock_timestamp() 的取值时机、AT TIME ZONE 的方向语义 |
核验说明
- 8 字节宽度来自
pg_type.typlen实查(timestamp与timestamptz均为 8),三个函数的时间戳来自同一次SELECT的真实输出。 - 未做:没有构造夏令时时区的实际数据来验证第三条坑,该条依据文档语义;没有测
timestamptz与timestamp在索引上的差异(两者都是 8 字节定长,实际差异很小)。