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

djm*_*ris 4 sql postgresql timezone plpgsql dst

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

例如,考虑我们“向前冲”的那个晚上。(在多伦多,我想是 2016 年 3 月 13 日凌晨 2 点)。
如果我拿一个时间戳: 2016-03-13 00:00:00-05 并添加'1 day'到它,在加拿大/东部,我希望得到2016-03-14 00:00:00-04-> 1 天后,但实际上只有 23 小时但是如果我在萨斯喀彻温省(一个没有使用 DST),我希望它增加 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
Run Code Online (Sandbox Code Playgroud)

我基本上想说

t2 = t1 + step; --in a time zone of my choosing
Run Code Online (Sandbox Code Playgroud)

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

t2 = (t1 AT TIME ZONE zoneid + step) AT TIME ZONE zoneid;
Run Code Online (Sandbox Code Playgroud)

似乎不起作用,因为第一次转换将 t1 转换为无时区的时间戳,因此无法计算 DST 转换

t2 = t1 + step;
Run Code Online (Sandbox Code Playgroud)

似乎不起作用,因为它在我的 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"
Run Code Online (Sandbox Code Playgroud)

如何解决这个问题?

a)在适当的时区将时间戳记转换为时间戳记?

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"
Run Code Online (Sandbox Code Playgroud)

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"
Run Code Online (Sandbox Code Playgroud)

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

Lau*_*lbe 7

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

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

  2. 添加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;$$;
Run Code Online (Sandbox Code Playgroud)

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

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)
Run Code Online (Sandbox Code Playgroud)

您可以考虑创建一个组合类型

CREATE TYPE timestampattz AS (
   ts timestamp without time zone,
   zone text
);
Run Code Online (Sandbox Code Playgroud)

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

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