PostgreSQL:在不同时区的时间戳中添加间隔

时间:2016-08-09 22:24:14

标签: sql postgresql timezone plpgsql dst

如果我不想在服务器的时区中进行计算,那么将指定的时间间隔添加到带时区的时间戳的最佳方法是什么?这对于夏令时过渡非常重要。

e.g。 考虑一下我们前进的那个晚上"。 (在多伦多,我认为这是2016-03-13凌晨2点) 如果我带时间戳: 2016-03-13 00:00:00-05 并在加拿大/东部地区添加'1 day',我希望得到2016-03-14 00:00:00-04 - > 1天后,但实际上只有23个小时 但如果我在萨斯喀彻温省(一个没有使用DST的地方)加入1天,我希望它能增加24小时,这样我才能最终得到它 2016-03-13 01:00:00-04

如果我有列/变量

t1 timestamp with time zone;
t2 timestamp with time zone;
step interval; 
zoneid text; --represents the time zone

我基本上想说

t2 = t1 + step; --in a time zone of my choosing

Postgres文档似乎表明带时区的时间戳在内部以UTC时间存储,这似乎表明timestamptz列没有计算其中的时区。 SQL标准表明了这一点 datetime + interval操作应保持第一个操作数的时区。

t2 = (t1 AT TIME ZONE zoneid + step) AT TIME ZONE zoneid;

似乎无法正常工作,因为第一次演员将t1转换为无时区的时间戳,因此无法计算DST过渡

t2 = t1 + step;

似乎无法正常工作,因为它在我的SQL服务器的时区中进行操作

在操作之前设置postgres时区并在之后将其更改回来?

更好的说明:

CREATE TABLE timestamps (t1 timestamp with time zone, timelocation text);
SET Timezone 'America/Toronto';
INSERT INTO timestamps(t1, timelocation) VALUES('2016-03-13 00:00:00 America/Toronto', 'America/Toronto');
INSERT INTO timestamps(t1, timelocation) VALUES('2016-03-13 00:00:00 America/Regina', 'America/Regina');

SELECT t1, timelocation FROM timestamps; -- shows times formatted in Toronto time. OK
"2016-03-13 00:00:00-05";"America/Toronto"
"2016-03-13 01:00:00-05";"America/Regina"

SELECT t1 + '1 day', timelocation FROM timestamps; -- Toronto timestamp has advanced by 23 hours. OK. Regina time stamp has also advanced by 23 hours. NOT OK.
"2016-03-14 00:00:00-04";"America/Toronto"
"2016-03-14 01:00:00-04";"America/Regina"

如何解决这个问题?

a)将timestamptz投放到适当时区的时间戳tz?

SELECT t1 AT TIME ZONE timelocation + '1 day', timelocation FROM timestamps; --OK. Though my results are timestamps without time zone now.
"2016-03-14 00:00:00";"America/Toronto"
"2016-03-14 00:00:00";"America/Regina"

SELECT t1 AT TIME ZONE timelocation + '4 hours', timelocation FROM timestamps; -- NOT OK. I want the Toronto time to be 5am
"2016-03-13 04:00:00";"America/Toronto"
"2016-03-13 04:00:00";"America/Regina"

b)更改postgres的时区并继续。

SET TIMEZONE = 'America/Regina';
SELECT t1 + '1 day', timelocation FROM timestamps; -- Now the Regina time stamp is correct, but toronto time stamp is incorrect (should be 22:00-06)
"2016-03-13 23:00:00-06";"America/Toronto"
"2016-03-14 00:00:00-06";"America/Regina"

SET TIMEZONE = 'America/Toronto';
SELECT t1 + '1 day', timelocation FROM timestamps; -- toronto is correct, regina is not, as before
"2016-03-14 00:00:00-04";"America/Toronto"
"2016-03-14 01:00:00-04";"America/Regina"

只有在每次操作时间间隔操作之前不断切换postgres时区时,此解决方案才有效。

2 个答案:

答案 0 :(得分:3)

这是导致您出现问题的两个属性的组合:

  1. timestamp with time zone以UTC格式存储,不包含任何时区信息。更好的名称是“UTC时间戳”。

  2. timestamp with time zoneinterval的添加始终在当前时区执行,即使用配置参数TimeZone进行设置。

  3. 由于您真正需要存储的是时间戳及其有效的时区,因此您应该存储表示时区的timestamp without time zonetext的组合。

    正如您正确注意到的那样,您必须切换当前时区以在正确的夏令时间转换时执行间隔添加(否则PostgreSQL不知道1 day有多长)。 但是您不必手动执行此操作,您可以使用PL / pgSQL函数为您执行此操作:

    CREATE OR REPLACE FUNCTION add_in_timezone(
          ts timestamp without time zone,
          tz text,
          delta interval
       ) RETURNS timestamp without time zone
       LANGUAGE plpgsql IMMUTABLE AS
    $$DECLARE
       result timestamp without time zone;
       oldtz text := current_setting('TimeZone');
    BEGIN
       PERFORM set_config('TimeZone', tz, true);
       result := (ts AT TIME ZONE tz) + delta;
       PERFORM set_config('TimeZone', oldtz, true);
       RETURN result;
    END;$$;
    

    这将为您提供以下内容,其中结果将在与参数相同的时区中被理解:

    test=> SELECT add_in_timezone('2016-03-13 00:00:00', 'America/Toronto', '1 day');
       add_in_timezone
    ---------------------
     2016-03-14 00:00:00
    (1 row)
    
    test=> SELECT add_in_timezone('2016-03-13 00:00:00', 'America/Regina', '1 day');
       add_in_timezone
    ---------------------
     2016-03-14 00:00:00
    (1 row)
    
    test=> SELECT add_in_timezone('2016-03-13 00:00:00', 'America/Toronto', '4 hours');
       add_in_timezone
    ---------------------
     2016-03-13 05:00:00
    (1 row)
    
    test=> SELECT add_in_timezone('2016-03-13 00:00:00', 'America/Regina', '4 hours');
       add_in_timezone
    ---------------------
     2016-03-13 04:00:00
    (1 row)
    

    您可以考虑创建组合类型

    CREATE TYPE timestampattz AS (
       ts timestamp without time zone,
       zone text
    );
    

    并在其上定义运算符和强制转换,但这可能是一个超出您想要的主要项目。

    甚至有一个PostgreSQL扩展timestampandtz就是这样做的;也许这正是你所需要的(我没看到添加的语义是什么)。

答案 1 :(得分:0)

根据@LaurenzAlbe提供的信息,我使用了一些不同的方法来处理向timestamptz添加间隔并使用DST。我在2020-10-25时遇到了问题,因为在比利时(时区“欧洲/布鲁塞尔”为UTC + 1),我们观察到夏令时,而在2020-10-25,03:00,我们向后退了1个小时,以02结束: 00,即我们从夏令时UTC + 2转到冬令时UTC + 1。

下面的必须在给定日期的16:00查找timestamptz的代码在当天失败,而是在15:00结束,因为我们向后退了1小时。有问题的代码行是:

select date_trunc('day', myfct.datetime) + interval 'PT16H'

例如,第一个查询可以,第二个查询不能。

select date_trunc('day', timestamptz '2020-10-27 17:00:00+01') + interval 'PT16H';
        ?column?        
------------------------
 2020-10-27 16:00:00+01

select date_trunc('day', timestamptz '2020-10-25 17:00:00+01') + interval 'PT16H';
        ?column?        
------------------------
 2020-10-25 15:00:00+01

这个想法是用timestamp(无时区)而不是timestamptz进行加法运算,最后将其转换为具有数据库配置时区的timestamptz

hydro_dev=> select date_trunc('day', timestamptz '2020-10-25 17:00:00+01');
       date_trunc       
------------------------
 2020-10-25 00:00:00+02
(1 row)

hydro_dev=> select date_trunc('day', timestamptz '2020-10-25 17:00:00+01')::timestamp;
     date_trunc      
---------------------
 2020-10-25 00:00:00
(1 row)

hydro_dev=> select date_trunc('day', timestamptz '2020-10-25 17:00:00+01')::timestamp + interval 'PT16H';
      ?column?       
---------------------
 2020-10-25 16:00:00
(1 row)

select (date_trunc('day', timestamptz '2020-10-25 17:00:00+01')::timestamp + interval 'PT16H')::timestamptz;
      timestamptz       
------------------------
 2020-10-25 16:00:00+01