关于sql:在Postgres中考虑DST,选择预定项目时

Accounting for DST in Postgres, when selecting scheduled items

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

Alfred and Lotta both live in Stockholm (+1 hour from UTC, but +2h
when it's DST). Sharon lives in Singapore (+8 hours from UTC, no
DST)

During winter, Alfred sets an alarm for 4 AM. The alarm should go off
at 4 AM local time, all year. During summer, Lotta sets an alarm
for 5 AM. Again, it should go off at 5 AM all year round.
Meanwhile, Sharon has set an alarm for 11 AM.

All of these can be stored in the database as 03:00 UTC.

If I query the database in the winter for alarms that should go off at
03:00 UTC, I want Alfred's and Sharon's alarms. Singapore is now +7h
from Sweden, so 11 AM in Singapore is 4 AM in Sweden. Lotta's alarm
should not go off for another hour.

Conversely, if I query the database in the summer for alarms that
should go off at 03:00 UTC, I want Lotta's and Sharon's alarms.
Singapore is +6h from Sweden now, so 11 AM in Singapore is 5 AM in
Sweden now. Sven's alarm went off an hour ago.

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

如有必要,我可以更改数据库架构。目前,我们根本没有调整DST,事实上只有一个"小时"整数字段(看似愚蠢,时间字段会更好)。

我似乎需要存储UTC时间和时区信息,但我不知道如何在Postgres中最好地实现这一点。我发现Postgres有一些时区概念,但据我所知,没有时区字段类型。此外,我想我需要在SQL中进行一些计算,以确定如何根据时区数据和创建日期来偏移select中的UTC时间。我对SQL不太满意......

我确实希望在Postgres中解决这个问题,因为可能存在很多"警报",我想避免将所有这些问题都引入Ruby并在那里过滤所带来的性能问题。 (是的,这是一个Rails应用程序。)


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

永远不要使用time with time zone这是一个逻辑上破碎的类型,PostgreSQL不鼓励使用它。手册:

The type time with time zone is defined by the SQL standard, but the
definition exhibits properties which lead to questionable usefulness.
In most cases, a combination of date, time, timestamp without timezone,
and timestamp with time zone should provide a complete range of
date/time functionality required by any application.

演示设置:

1
2
3
4
5
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。有关:

  • 应用于时间戳时,具有相同属性的时区名称会产生不同的结果

获取"今天"的匹配警报:

1
2
3
4
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 ...提取时间组件的最简单方法。

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

1
2
3
4
SELECT *
FROM   pg_timezone_names
WHERE  name ~~* '%sing%'
LIMIT  10

SQL Fiddle演示夏季/冬季。


您可以使用全时区名称来完成此操作,例如: America / New_York而不是EDT / EST,并且在该时区存储小时而不是UTC。然后,您可以对夏令时的偏移变化保持无知。

像下面这样的东西应该工作:

1
2
3
4
5
6
7
8
9
10
11
12
-- 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;

要么:

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


鉴于:

1
2
3
4
5
6
7
8
9
10
11
12
13
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');

尝试:

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

结果:

1
2
3
4
5
 username
----------
 Alfred
 Sharon
(2 ROWS)

原理:

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

这也允许您应对用户更改位置的情况,从而更改时区。

当您想要进行预测性查询时,可以通过日期限定时间戳来扩展此方法,例如"在当地时间将在位置发出警报声"。

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