14

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

例子

阿尔弗雷德和洛塔都住在斯德哥尔摩(距世界标准时间 +1 小时,但在夏令时 +2 小时)。
Sharon 住在新加坡(距 UTC +8 小时,无 DST)

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

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

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

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

我如何存储它并查询数据库?

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

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

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

4

3 回答 3

14

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

永远不要使用time with time zone( timetz) 它是一种逻辑错误的类型,PostgreSQL 不鼓励使用它。手册:

该类型time with time zone由 SQL 标准定义,但该定义显示的属性会导致有用性受到质疑。在大多数情况下, 、 、 和 的组合datetime提供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。有关的:

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

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)... assembletimestamp [without time zone] 也可能只是now()::date + t“今天”。
  • AT WITH TIME ZONE tz...将时间戳放在保存的时区,结果是timestamptz.
  • AT WITH TIME ZONE 'UTC'... 根据 UTC 获取timestamp
  • ::time...提取时间分量的最简单方法。

在这里您可以查找时区名称

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

db<>fiddle here - 演示夏季/冬季
sqlfiddle

于 2012-11-06T01:16:46.300 回答
7

您可以使用完整的时区名称,例如 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);
于 2012-11-06T00:56:27.917 回答
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 时间规则的事实来生成该区域当前时区的时间。与创建警报时的时区中存储的时间戳进行比较。

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

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

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

于 2012-11-06T01:11:21.157 回答