PostgreSQL的日期和时间能力在关系型数据库里属于第一梯队,但也因为类型多、函数多、时区规则细,给开发者留下了不少疑惑。要真正用好它,第一步不是背函数,而是先分清三类核心数据:绝对时间点、本地日期时间、时间间隔。对应到PostgreSQL就是timestamptz、timestamp、date/time以及interval。下面围绕类型选择、常用函数和时区排错展开。

一、日期时间类型怎么选?先分清绝对时间和本地时间
PostgreSQL原生提供几种日期时间类型,分别是date、time、timestamp、timestamptz和interval。其中date只表示年月日,适合生日、到期日等不需要时分秒的场景;time只表示时分秒,适合营业时间这类只关注时刻的场景;timestamp和timestamptz则同时包含日期和时间,区别在于是否携带时区语义;interval表示一段时间长度,比如两个小时、三天。
最容易混淆的是timestamp和timestamptz。简单说,timestamptz内部统一以UTC存储,客户端读取时根据当前会话的时区参数自动转换成本地时间显示;而timestamp不存储任何时区信息,写入什么值就读出什么值。比如同样在UTC+8的会话中写入2025-03-10 12:00:00,如果列类型是timestamp,查询时仍然是12点;如果列类型是timestamptz,数据库会把它解释为UTC+8的12点,内部存成UTC的04点,但同一会话查询仍显示12点。一旦会话时区改成UTC+0,timestamptz列就会显示04点,而timestamp列还是12点。这个差异正是很多时间不一致问题的根源。
下面这个建表语句演示了几种类型的基本用法:
CREATE TABLE event_log (
id bigserial PRIMARY KEY,
event_name text NOT NULL,
occurred_at timestamptz NOT NULL DEFAULT now(),
business_date date,
open_time time,
duration interval
);
在实际业务中,凡是记录事件发生时间、创建时间、更新时间,都建议使用timestamptz,因为它能表达跨时区的绝对时间点,避免服务器迁移或用户跨时区访问时出现偏移。如果只是保存一个结构化的本地日期,例如合同生效日期、节假日,用date更合适。只有当你非常确定这个时间不会被跨时区解释,并且只关心字符串式的时间表示时,才考虑使用timestamp。
二、常用日期时间函数与运算技巧
PostgreSQL围绕日期时间提供了丰富的函数。获取当前时间方面,now()返回事务开始时的timestamptz,current_date返回当前日期,current_time返回当前时间,localtimestamp返回不带时区的本地时间。需要注意now()在同一事务内多次调用返回值不变,如果希望每条语句都拿到真实当前时间,应使用clock_timestamp()。
数据统计中最常用的当属date_trunc和extract。date_trunc可以按分钟、小时、天、月、年等单位截断时间,例如把销售明细汇总到小时粒度;extract则可以从时间中抽取年份、月份、星期、周数等字段。配合age函数可以计算两个时间的间隔,配合to_char可以灵活输出格式化字符串。下面是一组常用示例:
SELECT now() AS current_tx_time,
current_date AS today,
date_trunc('hour', now()) AS hour_start,
extract(dow FROM now()) AS day_of_week,
age(now(), timestamp '2024-12-01 08:00:00') AS since_date,
to_char(now(), 'YYYY-MM-DD HH24:MI:SS') AS formatted_time;
时间运算方面,timestamp/timestamptz可以直接与interval相加或相减,得到新的时间点;两个时间点相减会得到一个interval。如果要计算两个日期相差的天数,不能直接相减得到数字,需要先相减再用extract取出天数,或者直接将两个date类型相减。对于复杂区间,justify_hours和justify_days可以把超出范围的小时和天数进位调整,例如interval '30 hours'可以整理为1 day 06:00:00。这些细节在处理考勤、租期、订阅周期时很实用。
三、时区转换与高频问题排查
PostgreSQL的时区行为由timezone参数控制,可以在实例级、数据库级、用户级、会话级进行设置。查看当前时区可以使用SHOW timezone;,修改会话时区则用SET timezone = 'Asia/Shanghai';。当timestamptz列被读出时,数据库会按照这个参数把内部UTC值转换成对应时区的本地时间。如果客户端程序连接字符串中没有显式指定时区,它通常会继承服务器的默认时区,这时跨地域部署的应用就可能出现显示差异。
如果想在一次查询中把时间转换到指定时区,可以使用AT TIME ZONE表达式。这个表达式有两个方向:作用于timestamptz时,会转换为指定时区的timestamp;作用于timestamp时,会把它解释为指定时区的时间并返回timestamptz。下面示例展示如何把UTC时间转换为上海时间:
SELECT occurred_at,
occurred_at AT TIME ZONE 'Asia/Shanghai' AS shanghai_local,
(occurred_at AT TIME ZONE 'Asia/Shanghai') AT TIME ZONE 'UTC' AS back_to_utc
FROM event_log
WHERE occurred_at >= now() - interval '7 days';
排查时区问题时,第一步要确认列类型是timestamp还是timestamptz。如果列本身是timestamp,那它永远不会有自动时区转换,查询结果不一致往往是因为插入时客户端或ORM做了本地时间转换。第二步检查会话时区,可以用SET timezone = 'UTC'后重新查询,观察timestamptz字段的变化是否符合预期。第三步确认JDBC连接串的TimeZone或serverTimezone参数,很多Java应用因为驱动默认使用JVM时区而产生偏差。
再补充两个高频需求。计算两个日期相差天数,可以写成extract(day FROM (date '2025-03-10' - date '2025-03-01')),结果直接是数字。生成连续日期序列则用generate_series,可以配合date和interval生成自然日或工作日的清单。下面是一个生成三月份每一天的示例:
SELECT generate_series(
date '2025-03-01',
date '2025-03-31',
interval '1 day'
)::date AS calendar_day;
总体来看,PostgreSQL的日期时间能力并不难掌握,关键是把类型语义和时区规则搞清楚,再用函数去实现业务计算。遇到问题时,先确认列类型、会话时区和写入路径,通常就能快速定位。相比死记硬背函数,理解timestamptz的UTC存储机制更重要,这个机制贯穿查询显示、索引比较和跨时区开发的所有环节。
PostgreSQL日期时间时间函数时区处理修改时间:2026-09-28 10:00:13