时间日期处理是数据库操作中最容易出错的部分之一。PostgreSQL在日期时间类型和函数上的设计相当完善,提供了date、time、timestamp、timestamptz、interval等多种类型,配合丰富的操作符和内置函数,几乎可以覆盖所有时间计算场景。但正因为类型多、规则细,不少人在实际使用中会遇到interval精度丢失、时区隐式转换、格式化输出乱套等问题。本文把常用的时间日期函数和操作符梳理一遍,并把容易踩的坑一并说明。

一、时间日期类型与基本操作符
在讲函数之前,先弄清楚PostgreSQL里几个核心的时间类型。date只存储年月日,time只存储时分秒,timestamp存储日期加时间但不带时区信息,timestamptz即timestamp with time zone,存储的是绝对时间点,写入时会根据会话时区转换成UTC存储,读取时再按会话时区转换显示。需要特别强调的一点是,timestamptz内部并不真正保存时区,它保存的是一个绝对时刻,时区只影响显示和输入时的解释。
操作符方面,日期时间类型支持加减法。timestamp减去timestamp得到interval,date减去date得到整数天数,interval可以加减到timestamp或date上,两个interval也可以相加减,interval还能与数值相乘除。下面的例子演示了这些基础运算:
-- 日期相减得到天数 SELECT date '2024-05-20' - date '2024-05-01'; -- 结果 19 -- 时间戳加间隔 SELECT timestamp '2024-05-01 10:00:00' + interval '2 hours'; -- 2024-05-01 12:00:00 -- interval运算 SELECT interval '1 day' * 3; -- 3 days SELECT interval '90 minutes'; -- 00:90:00 显示为 01:30:00 -- 比较操作符同样适用于时间类型 SELECT now() > timestamp '2024-01-01'; -- true
这些操作符看似简单,但要注意date加上interval的结果类型是timestamp而不是date,例如date加上1.5天会得到带时间的timestamp值。如果希望结果保持date类型,需要显式转换。另外AT TIME ZONE操作符是时区转换的关键,它的行为根据操作数类型不同而不同:对timestamp使用是给它附加时区含义转成timestamptz,对timestamptz使用则是转换到目标时区的本地时间显示,这一双向特性在后面的坑点部分会详细展开。
二、核心函数详解
PostgreSQL内置的时间函数非常多,这里挑出使用频率最高的几个讲透用法。首先是EXTRACT和date_part,两者功能等价,用于从时间值中提取某个字段,比如年、月、日、小时、星期、一年中的第几天等。日常统计报表中按月汇总、按周汇总基本都靠它:
SELECT EXTRACT(YEAR FROM now()); -- 提取年份 SELECT EXTRACT(MONTH FROM timestamp '2024-05-20 14:30:00'); -- 5 SELECT EXTRACT(EPOCH FROM now()); -- 距离1970-01-01的秒数 SELECT EXTRACT(DOW FROM now()); -- 星期几,0表示周日 -- 按月统计订单量的典型写法 SELECT EXTRACT(YEAR_MONTH FROM created_at) AS ym, count(*) FROM orders GROUP BY 1 ORDER BY 1;
其次是date_trunc,它把时间戳截断到指定精度,是做时间维度统计的利器。date_trunc('month', now())会返回当月第一天的零点,date_trunc('week', ...)返回本周一的零点。相比EXTRACT再拼接字符串的做法,date_trunc直接返回时间戳类型,可以继续参与计算和建索引,性能和可读性都更好。还有age函数,专门用于计算年龄或时间差,age(timestamp, timestamp)返回两个时间点之间相差的年月日,按人类习惯的日历方式计算,比直接相减得到interval更直观:
-- 计算精确到年月日的年龄 SELECT age(timestamp '2024-05-20', timestamp '1990-08-15'); -- 结果 33 years 9 mons 5 days -- 单参数版本:与昨日零点比较 SELECT age(timestamp '1990-08-15');
格式化输出靠to_char,模式字符串中YYYY表示四位年、MM两位月、DD两位日、HH24二十四小时制、MI分钟、SS秒。反过来解析字符串用to_timestamp和to_date。取当前时间的函数有now()、current_timestamp、current_date、clock_timestamp(),其中now和current_timestamp返回事务开始时间,同一事务内多次调用结果一致,而clock_timestamp每次调用都返回真实当前时间,做延迟计算或高精度计时时要选对函数。
三、常见坑点与注意事项
第一个坑是interval的精度问题。interval '1 month 30 days'这种复合间隔在计算时月份和天数是分开进位的,一个月的天数取决于具体跨越的月份,所以加上同样的interval在不同日期上产生的天数偏移可能不同。更隐蔽的是用interval转成秒数再除以86400来算天数这种写法,遇到夏令时切换的日期会算出23或25小时,结果就差一天。需要固定天数时,建议明确使用天数单位而不是月份。
第二个坑是timestamp与timestamptz的混用。当两者比较或插入时,PostgreSQL会按会话的TimeZone参数做隐式转换,如果应用服务器和数据库会话时区不一致,同一份数据在不同环境下查询结果会不一样。排查这类问题时先执行SHOW timezone;确认会话时区,必要时在连接串里显式指定时区参数。第三个坑是AT TIME ZONE的方向性容易搞反:timestamp '2024-05-01 12:00:00' AT TIME ZONE 'Asia/Shanghai'的意思是把墙上的12点看作上海时间,转成UTC时间点;而now() AT TIME ZONE 'Asia/Shanghai'是把当前UTC时刻转成上海本地墙钟时间。一个简单的记忆方法是,操作timestamp时是给它赋予时区,操作timestamptz时是切换显示时区。
最后几点实用建议:对频繁按时间范围查询的字段建索引时,如果查询条件用了函数包裹列,比如WHERE date_trunc('day', created_at) = ...,普通索引会失效,应改用范围条件created_at >= x AND created_at < y,或者建表达式索引。存储时刻类数据(如事件发生时间)优先用timestamptz,存储纯日期(如生日)用date,这样能避开大部分时区烦恼。掌握这些类型规则和函数特性后,PostgreSQL的时间处理会变得非常顺手可靠。
PostgreSQL日期函数时间操作符时间戳计算修改时间:2026-09-06 11:46:35