likes
comments
collection
share

PostgreSQL技术问答43 - Date&Time 日期和时间-上本文探讨了在Postgres中,对于时间处理的相

作者站长头像
站长
· 阅读数 25

本文是《PostgreSQL技术问答》系列文章中的一篇。关于这个系列的由来,可以参阅开篇文章:

文章的编号只是一个标识,在系列中没有明确的逻辑顺序和意义。读者进行阅读时,不用太关注这个方面。

本文讨论的内容是PostgreSQL中对于日期和时间的相关支持和内容。这部分的内容内容比较多,笔者可能会分为两个部分进行探讨。本文是相关内容的上半部分,是比较基础和常规的内容。在下半部分,会讨论一些相对比较扩展和高级的内容,也建议读者还是去了解一下。

Postgres中,如何实现和支持日期和时间的处理

Postgres技术文档中,有关于时间处理的内容主要在下面的链接页面中:

www.postgresql.org/docs/16/fun…

通过查阅这些相关技术文档,笔者觉得,Postgres对于时间和日期的支持还是比较完善和强大的,不仅仅是简单的获得当前时间或者进行字符串格式的转换,还有很多相关的技术,可以很好的满足各种各样的业务方面的需求。但主要的问题是,它的很多实现和处理,都是针对以英语为母语的应用的模式,对于中文和相关语境的支持并不是很好,这个就比较遗憾了。

首先从数据类型上而言,Postgres将时间相关的信息,分为date、time、timestamp和interval四种类型:

  • date: 就是日期类型或者以字符串形式表达的日期
  • time: 时间,和字符串形式的时间
  • timestamp: 就是时间戳,一个完整的时间信息
  • interval: 时间间隔,这是一个比较独特的数据类型,在postgres中它可以参与时间的计算

基于以上的数据类型,从形式上而言,Postgres对于时间和日期处理方面的支持,大致可以分为操作符和函数两种大的类型。其中,操作符就是直接使用我们都比较熟悉的数学运算符,但逻辑上来支持时间数据和信息的计算;而函数一般用于时间类型相关的处理。通用的相关时间信息和处理,主要包括下面一些操作:

  • 获取当前的时间
  • 获取过去或者未来的时间,就是基于某个特定时间(包括当前时间)和时间间隔,计算相对的未来或者过去时间点
  • 计算两个时间之间的差异,并用时间间隔或者字符串表达
  • 时间数据类型,和表示信息(格式化的时间字符串)之间的转换
  • 从完整的时间信息中,提取特性的属性信息(如年、月、日、周日等等),这通常是业务统计的需求
  • 时区信息和相关的处理
  • 延时或者定时操作和控制

这些内容,其实也是所有编程系统,对于时间进行操作和处理的一般方法论。这些操作,再结合比较丰富的数据类型和表现形式,postgres在时间信息的处理上,表现出来的功能特性,给人的感觉就非常灵活和强大。

下面我们就根据这些操作的分类,分别进行讨论。

如何获取当前时间

获取当前时间,是一个非常常见的应用需求。Postgres提供了很多种方式,可以获取和展示当前时间,它们有一些细微的差别,用于不同的场景当中。下面是一些简单的例子:

-- 时间戳,字符串形式
defaultdb=>  SELECT clock_timestamp(), timeofday();
        clock_timestamp        |              timeofday              
-------------------------------+-------------------------------------
 2024-09-02 09:14:17.376077+00 | Mon Sep 02 09:14:17.376078 2024 GMT
(1 row)

-- 语句和事务时间 
defaultdb=> select statement_timestamp(),transaction_timestamp();
      statement_timestamp      |     transaction_timestamp     
-------------------------------+-------------------------------
 2024-09-02 09:09:45.366577+00 | 2024-09-02 09:09:45.366577+00
(1 row)

-- 系统变量, 默认带时区,可以指定精度
defaultdb=>  SELECT current_date, current_time, current_timestamp, current_timestamp(2) ;
 current_date |    current_time    |       current_timestamp       |     current_timestamp     
--------------+--------------------+-------------------------------+---------------------------
 2024-09-02   | 09:07:20.869468+00 | 2024-09-02 09:07:20.869468+00 | 2024-09-02 09:07:20.87+00
(1 row)


-- localtimestemp, 不带时区信息
defaultdb=> SELECT LOCALTIMESTAMP;
       localtimestamp       
----------------------------
 2024-09-02 09:06:10.869189
(1 row)

-- now函数, now时间戳
defaultdb=> SELECT now(), TIMESTAMP 'now';
              now              |         timestamp          
-------------------------------+----------------------------
 2024-09-02 09:11:23.570569+00 | 2024-09-02 09:11:23.570569
(1 row)
 

这里有一个小技巧,就是通过查看使用字符串表示的时间或者时间戳的时候,可以从其尾部是否附带时区(如+00表示UTC),来判断这是否是一个带有时区信息的时间,也可以判断该时间附带的时区信息。

如何创建时间/日期/间隔/时间戳

在Postgres中,有很多种方式,可以从数据和信息中,创建时间/日期/间隔/时间戳,它们通常使用makexxx方法:

-- 创建日期
make_date(2013, 7, 15) → 2013-07-15

-- 创建时间
make_time(8, 15, 23.5) → 08:15:23.5

-- 创建时间戳
make_timestamp(2013, 7, 15, 8, 15, 23.5) → 2013-07-15 08:15:23.5

-- 基于时区创建时间戳
make_timestamptz(2013, 7, 15, 15, 15, 23.5, 'Asia/Shanghai') → 2013-07-15 07:15:23.5+00

-- 创建时间间隔
make_interval(years=>1,days => 10) →  1 year 10 days

-- 格式化字符串转换
defaultdb=> SELECT TO_TIMESTAMP('2023-10-02 14:30:00', 'YYYY-MM-DD HH24:MI:SS') AS timestamp;
       timestamp        
------------------------
 2023-10-02 14:30:00+00
(1 row)

可以看到,大体有两种模式。一个是来自时间构成的数值,一个是来自时间字符串。前者所使用的函数,有其规范和格式,需要注意使用的参数和对应关系;后者本质上是使用一个模板字符串,来进行时间信息和其表示字符串之间进行的相互转换,这里涉及到两个方法,就是to_timestamp和to_char,关于模板字符串的使用,我们在后面的章节有更详细的探讨。

如何从时间对象中提取某些属性或者内容

在日常应用中,我们经常遇到需要从一个完整的时间信息中,提取一部分特定的信息,如日期,某种形式的时间表示等等,这时就需要将一个完整的时间信息进行解构。在Postgres中,通常使用extract函数来操作和实现,下面是一些常见的例子:


defaultdb=> with T(n,t) as (values (now(), '2023-03-11 17:43:17.436')) 
SELECT EXTRACT(CENTURY FROM n ) c1 ,  EXTRACT(CENTURY FROM t::TIMESTAMP) c2,
EXTRACT(HOUR FROM n ) h1 ,  EXTRACT(HOUR FROM t::TIMESTAMP) h2 
from T;
 c1 | c2 | h1 | h2 
----+----+----+----
 21 | 21 |  2 | 17
 

我们可以将一个时间戳(字符串)看成一个时间对象,那么例如年份、小时数等等,就可以看成它的属性,extract就可以用于提取这些属性,它们可以使用一些关键字来指定属性的标识,相当的强大和丰富:

  • Millennium: 千年,如当前(2024年)是3
  • CENTURY: 世纪,如当前(2024)是21
  • DECADE: 世纪中的十年,如2023年的十年份是202
  • YEAR: 年份,如2023
  • ISOYEAR: ISO纪年方式,它和标准年稍有差别(基于周纪年),和公历年不完全一致
  • QUARTER: 一年中的季度序号,1~4
  • MONTH: 月份,一年中的月份序号,1~12
  • WEEK:在年中的周数,1~53
  • DAY: 月份中的天序号,1~31
  • DOY: 年中的天序号,1~366
  • DOW: 星期中的天,0~6,周日到周六
  • ISODOW: ISO周天,1~7,周一到周日
  • HOUR: 小时,一天内的小时数,0~23
  • MINUTE: 分钟,小时内的分钟数,0~59
  • SECOND: 秒,分钟内的秒数, 0~59
  • Milliseconds: 毫秒,秒中的毫秒值
  • Microseconds: 微秒,毫秒中的微秒值
  • EPOCH: 获取指定时间距离标准起始时间(1970-1-1)过去的秒数

前面的例子,都是从一个时间戳,转换成为一个时间对象,然后提取相关的熟悉。extract函数,还支持另外一种形式,就是从一个时间间隔的表达字符串中,来进行提取,如下面的例子:

-- 直接使用字符串
defaultdb=> SELECT EXTRACT(DAY FROM INTERVAL '19 days 3 minute');
 extract 
---------
      19
(1 row)

-- 转换类型
defaultdb=> with I (i) as ( values ('19 days 3 minute'))
SELECT EXTRACT(DAY FROM i::INTERVAL ) days,  
EXTRACT(HOUR FROM i::INTERVAL ) hours,  
EXTRACT(MINUTE FROM i::INTERVAL ) minutes from I;
 days | hours | minutes 
------+-------+---------
   19 |     0 |       3
(1 row)

我们可以看到,extract函数的调用,有其相应的语法规则,并且可以处理TimeStamp和Interval两者类型的信息(字符串形式)。

在PG技术文档中,还提到有一个类似的函数date_part,也可以实现类型的功能。笔者简单研究了一下,初步的感觉就是和extract相比,两者都可以实现相同的功能。主要差异就是调用的方式,一个是子句形式,一个是标准的参数方式,不知道为什么这样设计。下面是一个简单的测试用例,读者可以体会一下:

defaultdb=> SELECT 
EXTRACT(HOURS FROM INTERVAL '4 hours 3 minutes'), 
date_part('hour', INTERVAL '4 hours 3 minutes');
 extract | date_part 
---------+-----------
       4 |         4
(1 row)

如何将一个时间,转换成为数值形式的时间戳

在很多应用和计算过程当中,都需要使用数值形式,来对时间进行计算。方法是,取一个标准的起始时间,然后计算时间戳距离此时间点过去的时间数值,得到的数字,就可以来表示时间。在很多系统,如Unix中,都是使用这种处理方式,Postgres也沿用这个模式。它们设置的时间基点,就是1970-1-1,所有时间戳,都可以表示成为距离此基点过去的数值,单位为秒,数据类型可以是双精度或者浮点型(依据实现精度和范围)。由于数值计算的优势,我们也有理由认为,在系统内部,这些时间,也都是以数值形式进行存储和计算的,表示出来的时间戳字符串,只不过是方便人类理解而已。这里顺便提一下,在nodejs中,这个情况有点特殊,它的Date.now()方法结果是一个长整型,单位是ms。

所以,一个字符串形式的时间戳,如何转换成为对应的数值呢?

Postgres中,通常使用extract结合epoch来实现这一需求。epoch是“纪元”的意思,它用于获取某一个时间,距离标准起始时间(1970-1-1)过去的秒数(双精度类型)。也可以结合Interval类型计算以时间间隔字符串标识的时间区间的秒数。如:


-- 特定时间
defaultdb=> SELECT EXTRACT(EPOCH FROM TIMESTAMP '2023-03-11 17:43:17.436');
      extract      
-------------------
 1678556597.436000
(1 row)

-- 当前时间
defaultdb=> SELECT EXTRACT(EPOCH FROM NOW());
      extract      
-------------------
 1724988499.777206
(1 row)

-- 时间间隔
defaultdb=> SELECT EXTRACT(EPOCH FROM INTERVAL '2 day 10 hours');
    extract    
---------------
 208800.000000
(1 row)

所有的时间,都可以简单用一个数值来表示,非常容易管理和计算。这个解决方案看起来很完美?在某些方面确实是这样的,但实际上还有很多工程和现实方面的问题。例如,在真实世界的时间中,还有闰秒的问题,所以,通过这种方法计算出来的数值,和真实的数值是有差异的,可能有些时间点被丢弃了(想象我们怎么表示闰秒),有些时间点其实是重复的。当然,一般情况下,这种处理并不会太大的影响普通业务的应用,在绝大多数场景中,是可以被接受的,但作为开发者,我们应该知晓并理解这个情况。

如何计算有间隔的时间

在应用中,经常会需要计算和某个时间,间隔一段时间的另一个时间,如当前时间5天后的时间。下面是一个简单明了的示例:

-- 指定时间基点
defaultdb=> SELECT TIMESTAMP '2023-10-01 12:00:00' + INTERVAL '2 days 3 hours';
      ?column?       
---------------------
 2023-10-03 15:00:00
(1 row)

-- 将来时间
defaultdb=> SELECT NOW() + INTERVAL '2 days 3 hours';
           ?column?            
-------------------------------
 2024-09-01 06:37:31.581509+00
(1 row)

-- 过去的时间
defaultdb=> SELECT NOW() - INTERVAL '10 days';
           ?column?            
-------------------------------
 2024-08-20 03:46:22.957945+00
(1 row)

这里面有几个值得注意的地方:

  • 时间计算的基点,可以是当前时间,使用now()获取,类型是一个时间戳
  • 也可以是一个字符串形式表达的时间戳
  • 时间间隔,是一个字符串形式的Interval
  • 两者可以直接使用操作符进行计算,加或者减,也显示出Postgres操作符的灵活和强大
  • 理论上,我们都可以将所有时间都转换成为时间整数进行计算,但这种方式更加直观并容易理解

除了简单直观的操作符之外,Postgres还提供了date_add和date_subtract来实现类似的功能,这些函数可能的附加的特性,就是对于时区计算的支持,有兴趣的读者可以查阅文档自行了解。

如何基于时间计算间隔

PG提供了一个age函数,用于计算两个时间之间的间隔,并以年龄的形式呈现出来。本质上,是将两个时间点的时间差,使用interval字符串的形式表达出来。

-- age函数
defaultdb=> SELECT age(timestamp '2015-01-15', timestamp '1972-12-28');
       age        
------------------
 42 years 18 days
(1 row)

defaultdb=> SELECT age(timestamp '1973-01-25');
          age           
------------------------
 51 years 7 mons 3 days
(1 row)

-- 操作符
defaultdb=> SELECT (timestamp '2015-01-15')- (timestamp '1972-12-28');
  ?column?  
------------
 15358 days
(1 row)

defaultdb=> SELECT (timestamp '1972-12-28') - (timestamp '2015-01-15');
  ?column?   
-------------
 -15358 days
(1 row)

-- 调整显示
defaultdb=>  SELECT justify_interval ((timestamp '1972-12-28') - (timestamp '2015-01-15'));
      justify_interval      
----------------------------
 -42 years -7 mons -28 days
(1 row)

如果有两个参数,age函数会计算两个参数日期之间的间隔;如果有一个参数,将会计算当前时间和参数日期之间的间隔。输出的结果是一个语义化的字符串,能够显示年、月、日的数量。

虽然名字叫做age,但其实这个函数有非常广泛的应用场景,所有需要对快速表示两个时间之间间隔的操作,都可以用这个方式来进行处理。比如可以显示人员或者用户的年龄,两个日期之间的间隔时间,事件过去的时间,历史报告和分析中时间的表述等等。

除了age函数之外,还有就是可以非常简单的使用操作符“-”来计算两个时间之间的间隔。

这里顺便提一下,当计算间隔的时候,通常有一个问题,就是它的结果,可能就是一个简单的时间间隔(如15338 days这种形式),这时可以使用justify系列函数,可以对这个结果进行“调整”,转换成为更易读的形式,这些函数包括:

  • justify_days ( interval ) → interval 调整日期显示
  • justify_hours ( interval ) → interval 时间显示
  • justify_interval ( interval ) → interval 综合调整

关于时区是怎样处理的

在时间的相关处理中,特别是对于在全球使用的应用而言,有一个比较麻烦的问题,就是需要考虑时区对于时间处理的影响。

笔者理解,默认情况下,所有的时间戳和生成时间,都是世界标准时(UTC)的,所以我们看到的时间戳,都带有时区的信息,但为+00时区,就是标准时。上面的例子中创建时间戳时,可以指定时区,但系统存储和记录时,会将其转换成为标准时的时间。

如果要在使用中,指定带有时区信息的时间戳,一般需要结合 "timestamp with time zone/ at time zone"等标识 和时区字符串标识进行操作。下面是一些示例:


-- 带有时区的时间,显示为标准时间
defaultdb=> select timestamp '2001-02-16 20:38:40' at time zone 'America/Denver';
        timezone        
------------------------
 2001-02-17 03:38:40+00
(1 row)

-- 将一个时区时间,转换为另一个时区的时间
defaultdb=> select timestamp with time zone '2001-02-16 20:38:40-05' 
at time zone 'America/Denver';
      timezone       
---------------------
 2001-02-16 18:38:40
(1 row)

-- 只转换时间
defaultdb=> select time with time zone '05:34:17-05' at time zone 'UTC';
  timezone   
-------------
 10:34:17+00
(1 row)

有一些函数,本身是支持时区参数的,在处理过程中也基于基于时区信息进行计算。如下面的例子:

date_trunc('day', timestamptz '2001-02-16 20:38:40+00', 'Australia/Sydney') 
2001-02-16 13:00:00+00

这个例子中,需要对一个时间戳截断到天。使用date_trunc函数,这个函数有一个带时区处理的版本(三个参数)。这个处理的过程是:

  • 原始时间戳, 02-16 20:38,标准时
  • 指定时区转换时,使用时区为悉尼,+11
  • 转换到悉尼时间,为 02-17 07:38
  • 截断日期时间,为悉尼时间 02-17 00:00
  • 再转换为UTC,为标准时 02-16 13:00
  • 最终处理的结果为: 2001-02-16 13:00:00+00

Postgres中,可以使用标准字符串形式来表示时区,常见的时区和字符串包括:

  • Asia/Shanghai 中国标准时CST,+08
  • America/New_York 美国东部时间EDT -04
  • US/Pacific PST 美国太平洋时间PST,-08
  • Europe/London 英国标准时, +01
  • Europe/Moscow 莫斯科标准时MST, +03
  • Asia/Tokyo 日本标准时JST,+09
  • Australia/Sydney 澳大利亚东部标准时AEST, +11

这些信息,可以在系统视图 pg_timezone_names中, 查询得到。

小结

本文探讨了在Postgres中,对于时间处理的相关操作,包括常见的操作场景,如获取当前时间,计算两个时间的间隔,基于基础时间和间隔,计算过去或者将来的时间点,并讨论和展示了常见的函数和使用方式。

转载自:https://juejin.cn/post/7410980982377529398
评论
请登录