Учет DST в Postgres при выборе запланированных элементов

У меня есть таблица Postgres сигналов будильника (на самом деле это не так, но это аналогично и проще объяснить). Сигналы тревоги устанавливаются пользователями с разрешением 1 час, и пользователи могут быть из разных разных часовых поясов. Тревоги повторяются ежедневно. Я хочу надежно получить тревоги, которые должны уходить в определенный час дня, и я имею проблемы с летним временем. Как это сделать наилучшим образом?

Пример

Альфред и Лотта живут в Стокгольме (+1 час от UTC, но + 2h когда это DST).
Шарон живет в Сингапуре (+8 часов от UTC, нет ДСТ)

В течение зимы Альфред устанавливает будильник в течение 4 часов. Сигнал тревоги должен погаснуть в 4 часа по местному времени, в течение всего года.
Летом Лотта устанавливает будильник на 5 утра. Опять же, он должен уходить в 5 утра круглый год.
Между тем, Шарон установил будильник на 11 часов утра.

Все они могут быть сохранены в базе данных как 03:00 UTC.

Если я запрашиваю базу данных зимой для аварийных сигналов, которые должны 03:00 UTC, мне нужны сигналы тревоги Альфреда и Шарона. Сингапур теперь + 7 часов из Швеции, поэтому 11 часов в Сингапуре - 4 часа в Швеции. Тревога Лотта не должен уходить в течение еще одного часа.

И наоборот, если я запрошу базу данных летом для аварийных сигналов, которые должен уйти в 03:00 UTC, я хочу, чтобы сигналы тревоги Лотты и Шарона. Сингапур сейчас + 6 часов из Швеции, поэтому 11 часов в Сингапуре - 5 утра в Швеция. Сигнал Свена ушел час назад.

Как сохранить это и запросить базу данных?

При необходимости я могу изменить схему db. На данный момент мы вообще не настраиваемся на DST и на самом деле просто имеем целое число "час" (что кажется глупым, поле времени было бы лучше).

Кажется, мне нужно хранить информацию о времени и времени часового пояса, но я не знаю, как добиться этого в Postgres. Я обнаружил, что Postgres имеет какую-то концепцию часовых поясов, но, насколько я могу судить, не имеет типа поля часового пояса. Кроме того, я думаю, мне нужно сделать некоторые вычисления в SQL, чтобы определить, как компенсировать время UTC в выборе, основанное на данных часового пояса и дате создания. Я не очень хорошо разбираюсь в SQL...

Я хочу решить это в Postgres, так как может быть много "аварийных сигналов", и я хочу избежать проблем с производительностью, связанных с их загрузкой в ​​Ruby и фильтром там. (Да, это приложение Rails.)

Ответ 1

Используйте timestamp with time zone (timestamptz) для вычислений.
Время для будильника может быть time [without time zone].
Но вы должны сохранить часовой пояс явно для каждой строки.

Никогда использовать time with time zone Это логически сломанный тип, его использование не рекомендуется PostgreSQL. Я цитирую здесь руководство:

Тип времени с часовым поясом определяется стандартом SQL, но определение показывает свойства, которые приводят к сомнительной полезности. В большинстве случаев комбинация даты, времени, временной метки без времени зона и временная метка с часовым поясом должны обеспечивать полный диапазон функциональность даты/времени, требуемую любым приложением.

Демо-настройка:

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)... собрать timestamp [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

SQL Fiddle, демонстрирующий лето/зиму.

Ответ 2

Вы сделали бы это, используя полное имя часового пояса, например. Америка /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);

Ответ 3

Дано:

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

Try:

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, чтобы время создавало время, которое в текущем часовом поясе для региона. Сравните с сохраненной меткой времени в том, что было в часовом поясе, когда был создан этот сигнал.

Это также позволяет вам справляться с ситуациями, когда пользователь меняет местоположение и, следовательно, изменяет часовой пояс.

Этот подход может быть расширен по дате - присвоение временных меток, когда вы хотите выполнить прогностическое задание, например "в какое местное время будет звучать сигнал тревоги в местоположении".

Я не уверен в этом решении и рекомендую тщательное тестирование.