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

Posted

技术标签:

【中文标题】PostgreSQL:在不同时区的时间戳中添加间隔【英文标题】:PostgreSQL: Adding an interval to a timestamp in a different time zone 【发布时间】:2016-12-16 03:05:02 【问题描述】:

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

例如 想想我们“向前冲”的那个晚上。 (在多伦多,我想是 2016 年 3 月 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 时区时,此解决方案才有效。

【问题讨论】:

编辑:t1 和 t2 变量应该写成“带时区的时间戳”。抱歉,我是新来的,找不到编辑按钮。 如果答案解决了您的问题,您能否将其标记为已接受的答案? TIA。 【参考方案1】:

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

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

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

由于您真正需要存储的是时间戳及其有效时区,因此您应该存储timestamp without time zone 和代表时区的text 的组合。

正如您正确注意到的那样,您必须切换当前时区才能正确执行夏令时转换的时间间隔加法(否则 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 可以做到这一点;也许这正是你所需要的(我没有看加法的语义是什么)。

【讨论】:

谢谢劳伦兹。不幸的是,我打错了我的问题。正如您所建议的,这两个变量确实是带有时区的时间戳。但问题似乎仍然存在。 请证明。您的用例与我给出的示例有何不同? 代替 TIMESTAMPTZ '2016-03-13 00:00:00 CST',将该值与 TIMESTAMPTZ '2016-03-13 00:00:00 America/Toronto' 一起插入到表中带有时间戳列。然后我需要一种方法在“CST”时间范围或“多伦多”时间范围内将“1 天”添加到这些值中的任何一个。 现在我明白了,我已经修改了答案。【参考方案2】:

根据@LaurenzAlbe 提供的信息,我使用不同的方法来处理向timestamptz 添加间隔并使用夏令时。我在 2020 年 10 月 25 日遇到了问题,因为在比利时(时区“欧洲/布鲁塞尔”,即 UTC+1)我们观察了 DST,并在 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

【讨论】:

以上是关于PostgreSQL:在不同时区的时间戳中添加间隔的主要内容,如果未能解决你的问题,请参考以下文章

PostgreSQL:无法使用 DST 使时区正常工作

PostgreSQL:将字符串转换为没有时区的时间戳以更改时间日期时,我得到了意想不到的结果

数据类型“带时区的时间戳”中的时区存储

PostgreSQL 中的本地时区偏移量

如何从java中的字符串时间戳中提取日期和时间

在PostgreSQL中,我们可以直接比较两个不同时区的时间戳吗?