在 PostgreSQL 中正确处理 TIME WITH TIME ZONE

Posted

技术标签:

【中文标题】在 PostgreSQL 中正确处理 TIME WITH TIME ZONE【英文标题】:Properly handle TIME WITH TIME ZONE in PostgreSQL 【发布时间】:2018-10-20 23:23:47 【问题描述】:

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

这是表格的缩写结构:

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
);

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

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 是 TIMESTAMP 格式。但我们会按照自己的方式导入数据。 报告中的字段是根据客户端的时区格式化的,因此要重构此表,我们需要结合客户端的时区(我们有此信息)以正确插入 UTC 中的时间戳/时间。李>

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

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) 类型,现在我有点困惑,什么是重构此表以处理不同时区和处理的最佳方法我们需要的不同列操作。

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

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

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

【问题讨论】:

我个人倾向于使用包含 unix 时间戳的整数列,尽管在 2038 年您需要更改该列类型 IIUC,您目前忽略了START_HOUR 的日期部分吗?或者你有一个 CHECK 约束强制它与 REPORT_DATE 相同? @ErwinBrandstetter:目前我们保存日期部分,但对于所有当前计算,我们只使用时间部分。当我们处理旧报告时,我们会确保日期部分和REPORT_DATE 相同。 we need to combine the timezone of the client (we have this info)。如果不保存,您究竟是如何获得这些信息的? 我们保存它。在客户表中。我们知道什么客户正在导入报告,因此也知道时区。 【参考方案1】:

你断言:

每个 TIME 列代表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 规则(dalight saving time)。

所以基本上是你已经拥有的。只需从start_hour 中删除日期组件,这就是死货。将timestamp 投射到time 以切断日期。点赞:(timestamp '2018-03-25 1:00:00')::time

tz 可以是AT TIME ZONE 构造所接受的任何字符串,但是为了可靠地处理不同的时区,最好只使用时区名称。您在system catalog pg_timezone_names 中找到的任何name

为了优化存储,您可以在一个小型查找表中收集允许的时区名称,并将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,以便根据需要轻松显示字符串。该表用于存储您需要的信息。

注意括号!否则,由于operator precedence,运算符+ 将在AT TIME ZONE 之前绑定。

看看结果:

db小提琴here

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

相关:

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

【讨论】:

感谢您的完整回答!不过,我有一个感觉问题:1)为什么在 tz 'Europe/Vienna' 中添加 report_date + end_hourreport_date + expected_hour 会产生相同的结果?考虑到 start 和 expected 与您的 dbfiddle 中的不同。 2) 为什么使用UTC 再次申请AT TIME ZONE?当我们第一次使用客户端 tz 应用 tz 时,我们还没有客户端时区中的时间戳吗?这对我来说有点困惑,因为在执行 START_HOUR - END_HOUR 时,您不会两次应用 AT TIME ZONE @Luiz: 1) 因为愚蠢的夏令时规则,在欧盟2018-03-25 02:00 的时间提前了一个小时。因此,凌晨 3 点与这一天的凌晨 2 点相同。 2)关注my added link获取详细说明。我们不需要第二个AT TIME ZONE 进行计算,所以我不在那里使用它。 我不相信我是第一个支持答案的人!感谢 Erwin 为您提供宝贵的时间和精力 @ErwinBrandstetter 只是为了检查:如果我需要从@987654364 中减去TIME(因为我现在使用的是TIME WITHOUT TIME ZONETIME 已经保存在客户端的tz 中) @正确的方法是首先将AT TIME ZONE(在保存TIME的内容中使用相同的TZ)应用到TIMESTAMPTZ,对(假设我没有绑定到TIME部分的日期) ?从我的测试来看,这似乎是正确的方法,但我并不完全相信这将涵盖所有情况...... 这取决于 "subtract a TIME" 应该是什么意思。时间应该知道它所在的时区,你必须减去timestamptz,而不是timestamp。请参阅此基本比较:dbfiddle.uk/…。

以上是关于在 PostgreSQL 中正确处理 TIME WITH TIME ZONE的主要内容,如果未能解决你的问题,请参考以下文章

Twitter提要w/x-time-ago和错误处理

如何在没有 Joda Time 的情况下在 Java 7 中正确处理夏令时?

我正在尝试进行大规模 PostgreSQL 更新,但无法找出正确的方法

如何以正确的方式将 PostgreSQL 连接到 NodeJS? [复制]

在批处理文件中使用%time%时,永远不会存储时间

postgresql pg 库采用 windows 用户名而不是 postgresql