日期/时间类型
PostgreSQL 的日期/时间类型分别表达“日历日期”“一天中的时间”“不带时区的日期时间”“带时区语义的时间点”和“时间间隔”。最重要的设计原则是:先判断字段表示时间点还是日历概念,再选择类型。
常用类型
| 类型 | 表达的含义 | 典型场景 |
|---|---|---|
date | 年、月、日 | 生日、账期、节假日 |
time | 一天中的时刻 | 营业时间、闹钟时间 |
timestamp | 不带时区的日期和时间 | 墙上时钟读数、当地营业时间 |
timestamptz | 绝对时间点,按会话时区显示 | 创建时间、事件发生时间 |
interval | 时间长度或日历间隔 | “3 天后”“1 个月” |
PostgreSQL 中 timestamp with time zone 常写作 timestamptz。它不会保存原始时区名称,只保存可换算为 UTC 的时间点;查询时按 TimeZone 会话参数显示。
时间点与时区
SET TIME ZONE 'Asia/Shanghai';
SELECT '2026-09-21 10:00+08'::timestamptz;
SELECT now(), current_timestamp;
同一个 timestamptz 值切换会话时区后显示不同,但时间点不变:
SET TIME ZONE 'UTC';
SELECT event_at FROM events;
SET TIME ZONE 'America/Los_Angeles';
SELECT event_at FROM events;
服务端和应用应约定时区策略。常见做法是数据库保存 timestamptz,接口统一输出带偏移的 ISO 8601 字符串,展示层再转换为用户时区。
timestamp 不是“没有时区的 UTC”
timestamp without time zone 只保存字段中的年月日时分秒,不会记录它属于哪个时区:
SELECT '2026-09-21 10:00+08'::timestamp;
输入中的 +08 会被忽略。若值实际表示一个跨地区发生的事件,应使用 timestamptz;只有“每天 10 点营业”这类不绑定绝对时间点的概念才适合 timestamp 或 time。
精度、日期和时间运算
类型可以带精度 p,表示小数秒位数,例如 timestamp(3) 保存到毫秒。精度应根据来源和业务需要设置,不要以为更高精度一定更准确。
SELECT date '2026-09-21' + interval '2 months';
SELECT timestamptz '2026-09-21 09:00+08' + interval '90 minutes';
SELECT finished_at - started_at AS elapsed FROM jobs;
interval '1 month' 是日历月份,不等于固定天数;月末加减可能出现不同日期。需要固定时长时使用小时、分钟或秒,并用目标时区做边界测试。
夏令时与截断
date_trunc 可以按指定粒度截断时间:
SELECT date_trunc('day', event_at, 'Asia/Shanghai')
FROM events;
跨夏令时地区时,“一天”不一定是 24 小时。统计按自然日分组时要明确使用哪个时区;统计过去 24 小时时应使用时间点差值,而不是本地日期减法。
默认值、索引和范围查询
CREATE TABLE events (
event_id bigint PRIMARY KEY,
event_at timestamptz NOT NULL DEFAULT now()
);
CREATE INDEX events_event_at_idx ON events (event_at);
范围查询使用半开区间通常更稳健:
WHERE event_at >= $1
AND event_at < $2
不要把列包在 to_char、date() 等函数中再过滤,否则普通索引可能无法使用;需要按日期搜索时,传入边界时间或建立匹配的表达式索引。
常见误区
- 把时间点保存为文本,导致排序、校验和时区换算不可靠。
- 把
timestamp当成 UTC 保存,却没有记录约定,之后无法恢复真实时间点。 - 用
now()的字符串结果拼 SQL,而不是使用参数化时间值。 - 以为
interval '1 month'永远等于 30 天。 - 忽略夏令时、闰秒处理和数据库会话
TimeZone设置。