在 PostgreSQL 中正确处理 TIME WITH TIME ZONE

Properly handle TIME WITH TIME ZONE in PostgreSQL

我们有一个 table,其中填充了来自另一个系统的遗留报告的数据。该 table 的列反映了报告的相同结构。

这里是 table 的缩写结构:

CREATE TABLE IF NOT EXISTS LEGACY_TABLE (
  REPORT_DATE DATE NOT NULL,
  EVENT_ID BIGINT PRIMARY KEY NOT NULL,
  START_HOUR TIMESTAMP WITHOUT TIME ZONE,
  END_HOUR TIME WITHOUT TIME ZONE,
  EXPECTED_HOUR TIME WITHOUT TIME ZONE
);

我们正在重构此 table 以处理不同客户的不同时区。新结构类似于:

CREATE TABLE IF NOT EXISTS LEGACY_TABLE (
  REPORT_DATE DATE NOT NULL,
  EVENT_ID BIGINT PRIMARY KEY NOT NULL,
  START_HOUR TIMESTAMP WITH TIME ZONE,
  END_HOUR TIME WITH TIME ZONE,
  EXPECTED_HOUR TIME WITH TIME ZONE
);

这些小时字段表示 REPORT_DATE 列表示的一天中的特定时间点。我的意思是,每个 TIME 列代表 REPORT_DATE.

中指定的一天中的一个时刻

需要考虑的其他几点:

但现在问题来了。这些列的值用于在我们的系统中多次计算另一个值,如下所示:

START_HOUR - END_HOUR (the result of this operation is currently being casted to TIME WITHOUT TIME ZONE)
START_HOUR < END_HOUR
START_HOUR + EXPECTED_HOUR
EXPECTED_HOUR - END_HOUR
EXPECTED_HOUR < '05:00' 

经过一些研究,我发现不建议使用 TIME WITH TIME ZONE (Postgres time with time zone equality) 类型,现在我对重构这个 [=51 的最佳方法是什么感到有点困惑=] 来处理不同的时区和处理我们需要的不同的列操作。

除此之外,我已经知道减去 TIMESTAMP WITH TIME ZONE 类型的两列是安全的。这个减法运算考虑了夏令时的变化(),但其他的呢?还有从 TIMESTAMP 中减去 TIME 的那个?

关于 table 重构,我们是否应该使用 TIME WITH TIME ZONE?我们应该继续使用 TIME WITHOUT TIME ZONE 吗?或者最好完全忘记类型 TIME 并将 DATE 与 TIME 结合起来并将列更改为 TIMESTAMP WITH TIME ZONE?

我认为这些问题是相关的,因为我们选择使用的新列类型将定义我们如何操作这些列。

您断言:

every TIME column represents a moment during the day specified in REPORT_DATE.

所以你永远不会在同一行内跨越日期变更线。我建议保存 1x date 3x time 时区 (如 text 或 FK 列):

CREATE TABLE legacy_table (
   event_id      bigint PRIMARY KEY NOT NULL
 , report_date   date NOT NULL
 , start_hour    time
 , end_hour      time
 , expected_hour time
 , tz            text  -- time zone
);

正如您已经发现的那样,timetz (time with time zone) should generally be avoided。它无法正确处理 DST 规则 (daylight saving time).

所以基本上你已经拥有的东西。只需删除 start_hour 中的日期部分,即空运费。将 timestamp 转换为 time 以截断日期。喜欢:(timestamp '2018-03-25 1:00:00')::time

tz 可以是 AT TIME ZONE construct, but to deal with different time zones reliably, it's best to use time zone names exclusively. Any name you find in the system catalog pg_timezone_names.

接受的任何字符串

要优化存储,您可以在小型查找中收集允许的时区名称 table 并将 tz text 替换为 tz_id int REFERENCES my_tz_table

包含和不包含 DST 的两个示例行:

INSERT INTO legacy_table VALUES
   (1, '2018-03-25', '1:00', '3:00', '2:00', 'Europe/Vienna')  -- sadly, with DST
 , (2, '2018-03-25', '1:00', '3:00', '2:00', 'Europe/Moscow'); -- Russians got rid of DST

出于表示目的或计算目的,您可以执行以下操作:

SELECT (report_date + start_hour)    AT TIME ZONE tz AT TIME ZONE 'UTC' AS start_utc
     , (report_date + end_hour)      AT TIME ZONE tz AT TIME ZONE 'UTC' AS end_utc
     , (report_date + expected_hour) AT TIME ZONE tz AT TIME ZONE 'UTC' AS expected_utc
     -- START_HOUR - END_HOUR
     , (report_date + start_hour) AT TIME ZONE tz
     - (report_date + end_hour)   AT TIME ZONE tz AS start_minus_end
FROM   legacy_table;

您可以创建一个或多个 views 以便根据需要随时显示字符串。 table 用于存储您 需要 的信息。

注意括号!否则运算符 + 将在 AT TIME ZONE 之前绑定,因为 operator precedence.

看看结果:

db<>fiddle here

由于时间在维也纳被操纵(就像任何应用愚蠢的 DST 规则的地方一样),您会得到“令人惊讶”的结果。

相关:

  • Accounting for DST in Postgres, when selecting scheduled items
  • Ignoring time zones altogether in Rails and PostgreSQL