选择计划项目时在 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 标准定义的,但是 定义展示了导致可疑有用性的属性。 在大多数情况下,date
、time
、timestamp without timezone
和timestamp 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
... 将时间戳放在保存的时区,得到timestamptz
。
AT 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的主要内容,如果未能解决你的问题,请参考以下文章
如何创建一个 Postgres 11 触发器函数,该函数在插入或更新到表“a”时在表“b”中插入一个新行?
使用 Play Framework 和 Postgres 插入时在时间戳字段上获取 TypeDoesNotMatch
获取 PSQLException:错误:在带有 Postgres 的 spark jdbc 中使用查询而不是表名时在“SELECT”处或附近出现语法错误