选择计划项目时在 Postgres 中考虑 DST

Posted

技术标签:

【中文标题】选择计划项目时在 Postgres 中考虑 DST【英文标题】:Accounting for DST in Postgres, when selecting scheduled items 【发布时间】:2012-10-25 17:27:11 【问题描述】:

我有一个 Postgres 表,其中包含 时钟闹钟(不是真的,但这是类似的,并且更容易解释)。警报由用户设置,分辨率为 1 小时,用户可以来自许多不同的时区。警报每天都在重复。我想可靠地获取应该在一天中的特定时间响起的警报,但我遇到了夏令时问题。我该如何以最好的方式做到这一点?

示例

Alfred 和 Lotta 都住在斯德哥尔摩(距 UTC +1 小时,但 +2h 当它是 DST 时)。 Sharon 住在新加坡(距 UTC +8 小时,没有 夏令时)

在冬天,阿尔弗雷德设置了凌晨 4 点的闹钟。警报应该响起 全年当地时间凌晨 4 点。 在夏季,洛塔会设置闹钟 早上 5 点。同样,它应该全年在早上 5 点开始。 与此同时,Sharon 已经设置了上午 11 点的闹钟。

所有这些都可以作为 03:00 UTC 存储在数据库中。

如果我在冬天查询数据库以获取应该在 03:00 UTC,我想要 Alfred 和 Sharon 的闹钟。新加坡现在是+7h 来自瑞典,所以新加坡的上午 11 点是瑞典的凌晨 4 点。洛塔的警报 再过一个小时就不会再响了。

相反,如果我在夏天查询数据库中的警报, 应该在 03:00 UTC 响起,我想要 Lotta 和 Sharon 的闹钟。 新加坡现在距离瑞典 +6h,所以新加坡的上午 11 点是上午 5 点 现在的瑞典。斯文的闹钟在一小时前响了。

如何存储并查询数据库?

如有必要,我可以更改数据库架构。目前,我们根本不针对 DST 进行调整,实际上只有一个“小时”整数字段(看起来很愚蠢,时间字段会更好)。

似乎我需要同时存储 UTC 时间和时区信息,但我不知道如何在 Postgres 中最好地实现这一点。我发现 Postgres 有某种时区概念,但据我所知没有时区字段类型。另外,我想我需要在 SQL 中进行一些计算,以确定如何根据时区数据和创建日期在选择中偏移 UTC 时间。我不太擅长 SQL……

我确实想在 Postgres 中解决这个问题,因为可能会有很多“警报”,并且我想避免将所有警报都提取到 Ruby 中并在那里进行过滤所带来的性能问题。 (是的,这是一个 Rails 应用程序。)

【问题讨论】:

Postgres 具有强大的时间类型。您是否尝试过使用它们,如果是,它们在哪里失败了? 我用time with time zone 字段类型尝试了一些东西。 1)我还需要存储时区以使用它。 Postgres 可以以某种方式促进这一点,还是我将其存储为字符串? 2)即使我有一个时区,我也没有找到如何提取 DST 信息。我的想法:我从时间字段中提取小时,如果需要根据创建日期补偿 dst,则添加/减去 1h ......但我想出的一切都非常复杂,以至于我的经验告诉我我做错了什么. @MartinSvalin 日期和时间痛苦而复杂的,可悲的是。 【参考方案1】:

您可以使用完整的时区名称,例如America/New_York 而不是 EDT/EST,并将小时存储在该时区而不是 UTC。然后,您可以幸福地忽略夏令时的偏移变化。

类似下面的东西应该可以工作:

-- CREATE TABLE time_test (
--   user_to_alert CHARACTER VARYING (30),
--   alarm_hour TIME,
--   user_timezone CHARACTER VARYING (30)
-- );

SELECT user_to_alert, 
  CASE
    WHEN EXTRACT(HOUR FROM CURRENT_TIME AT TIME ZONE user_timezone) = EXTRACT(HOUR FROM alarm_hour) THEN TRUE
  ELSE FALSE
END AS raise_alarm
FROM time_test;

或者:

SELECT user_to_alert
FROM time_test
WHERE EXTRACT(HOUR FROM CURRENT_TIME AT TIME ZONE user_timezone) = EXTRACT(HOUR FROM alarm_hour);

【讨论】:

比我自己想出的要优雅得多。谢谢!【参考方案2】:

给定:

SET timezone = 'UTC';

CREATE TABLE tzdemo (
    username text not null,
    alarm_time_utc time not null,
    alarm_tz_abbrev text not null,
    alarm_tz text not null
);

INSERT INTO tzdemo (username, alarm_time_utc, alarm_tz_abbrev, alarm_tz) VALUES
('Alfred', TIME '04:00' AT TIME ZONE '+01:00', 'CET', 'Europe/Stockholm'),
('Lotta', TIME '05:00' AT TIME ZONE '+02:00', 'CEST', 'Europe/Stockholm'),
('Sharon', TIME '11:00' AT TIME ZONE '+08:00', 'SGT', 'Singapore');

试试:

SELECT username 
FROM tzdemo 
WHERE alarm_time_utc AT TIME ZONE alarm_tz_abbrev = TIME '03:00' AT TIME ZONE alarm_tz;

结果:

 username 
----------
 Alfred
 Sharon
(2 rows)

原理:

存储创建警报的时区偏移量,包括当时是否为 DST 还存储转换为 UTC 的时钟时间 查询时,使用完整时区名称遵循当前 UTC 时间规则的事实来生成该区域当前时区中的时间。与创建警报时的时区中存储的时间戳进行比较。

这还允许您处理用户更改位置并因此更改时区的情况。

当您想要进行预测查询时,可以通过对时间戳进行日期限定来扩展此方法,例如“在什么当地时间会在某个位置发出警报声”。

我对这个解决方案并不完全有信心,建议仔细测试。

【讨论】:

【参考方案3】:

使用timestamp with time zone (timestamptz) 进行计算。 报警时间可以是time [without time zone]。 但是您必须为每一行明确保存时区

从不使用time with time zone (timetz) 这是一个逻辑错误的类型,PostgreSQL 不鼓励使用它。 The manual:

time with time zone 类型是由 SQL 标准定义的,但是 定义展示了导致可疑有用性的属性。 在大多数情况下,datetimetimestamp without timezonetimestamp with time zone 的组合应该提供完整的 任何应用程序所需的日期/时间功能范围。

演示设置:

CREATE TABLE alarm(name text, t time, tz text);
INSERT INTO alarm VALUES
  ('Alfred', '04:00', 'Europe/Stockholm') -- Alfred sets an alarm for 4 AM.
, ('Lotta',  '05:00', 'Europe/Stockholm') -- Lotta sets an alarm for 5 AM. 
, ('Sharon', '11:00', 'Asia/Singapore');  -- Sharon has set an alarm for 11 AM.

必须是时区名称(不是缩写)来说明 DST。相关:

Time zone names with identical properties yield different result when applied to timestamp

获取“今天”的匹配警报:

SELECT *
FROM   alarm
WHERE  (('2012-07-01'::date + t) AT TIME ZONE tz AT TIME ZONE 'UTC')::time
       = '03:00'::time
('2012-7-1'::date + t) ...组装timestamp [without time zone] 也可以只是 now()::date + t 表示“今天”。 AT WITH TIME ZONE tz ... 将时间戳放在保存的时区,得到timestamptzAT WITH TIME ZONE 'UTC' ... 根据 UTC 获取 timestamp ::time ... 提取时间分量的最简单方法。

这里可以查time zone names:

SELECT *
FROM   pg_timezone_names
WHERE  name ~~* '%sing%'
LIMIT  10;

dbfiddle here - 展示夏季/冬季旧 sqlfiddle

【讨论】:

谢谢!我将使用这种方法。 :) 这种方法需要全表扫描。是否有替代方法可以让您为某些内容编制索引,以便您可以有效地提取每小时需要通知的人员? @BenDowling 这很难实现,因为每行当前有效的绝对时间取决于保存的时区tz,及其当前的规则集(DST 等)。索引只能建立在 IMMUTABLE 数据上。一种解决方法可能是在(tz, time) 上创建一个索引(或每个tz 的部分索引),计算每个涉及的tz 的有效电流time 并过滤(tz, time) 对,可能在横向子查询中。 .. 开销只为大桌子买单。

以上是关于选择计划项目时在 Postgres 中考虑 DST的主要内容,如果未能解决你的问题,请参考以下文章

如何按日期分组,考虑时区和 DST?

如何创建一个 Postgres 11 触发器函数,该函数在插入或更新到表“a”时在表“b”中插入一个新行?

使用 Play Framework 和 Postgres 插入时在时间戳字段上获取 TypeDoesNotMatch

获取 PSQLException:错误:在带有 Postgres 的 spark jdbc 中使用查询而不是表名时在“SELECT”处或附近出现语法错误

使用 ng-content 时在表单验证中不考虑输入

如何在 swiftUI 中选择项目时在选择器上添加操作?