Правильная обработка времени с часовым поясом в PostgreSQL

У нас есть таблица, заполненная данными из устаревшего отчета другой системы. Столбцы этой таблицы отражают ту же структуру отчета.

Вот сокращенная структура таблицы:

CREATE TABLE IF NOT EXISTS LEGACY_TABLE (
  REPORT_DATE DATE NOT NULL,
  EVENT_ID BIGINT PRIMARY KEY NOT NULL,
  START_HOUR TIMESTAMP WITHOUT TIME ZONE,
  END_HOUR TIME WITHOUT TIME ZONE,
  EXPECTED_HOUR TIME WITHOUT TIME ZONE
);

Мы реорганизуем эту таблицу, чтобы иметь дело с разными часовыми поясами разных клиентов. Новая структура будет примерно такой:

CREATE TABLE IF NOT EXISTS LEGACY_TABLE (
  REPORT_DATE DATE NOT NULL,
  EVENT_ID BIGINT PRIMARY KEY NOT NULL,
  START_HOUR TIMESTAMP WITH TIME ZONE,
  END_HOUR TIME WITH TIME ZONE,
  EXPECTED_HOUR TIME WITH TIME ZONE
);

Эти поля часов представляют собой определенный момент времени в течение дня, представленный столбцом REPORT_DATE. Я имею в виду, что каждый столбец TIME представляет момент в течение дня, указанный в REPORT_DATE.

Некоторые другие моменты, которые следует учитывать:

  • Мы не знаем, почему START_HOUR находится в формате TIMESTAMP в отчете, который мы получаем из устаревшей системы. Но мы импортируем данные так, как они поступают к нам.
  • Поля в отчете отформатированы в соответствии с часовым поясом клиента, поэтому для рефакторинга этой таблицы нам нужно объединить часовой пояс клиента (у нас есть эта информация), чтобы правильно вставить временные метки / время в UTC.

Но теперь к проблеме. Значения этих столбцов используются для многократного вычисления других значений в нашей системе, примерно так:

START_HOUR - END_HOUR (the result of this operation is currently being casted to TIME WITHOUT TIME ZONE)
START_HOUR < END_HOUR
START_HOUR + EXPECTED_HOUR
EXPECTED_HOUR - END_HOUR
EXPECTED_HOUR < '05:00' 

После некоторого исследования я обнаружил, что не рекомендуется использовать тип TIME WITH TIME ZONE (Время Postgres с равенством часовых поясов), и теперь я немного запутался в том, как лучше всего реорганизовать эту таблицу для работы с разными часовыми поясами и обработки различных операций с столбцами, которые нам нужны. к.

Кроме того, я уже знаю, что можно безопасно вычесть два столбца типа TIMESTAMP WITH TIME ZONE. Эта операция вычитания учитывает изменения DST (Вычитание двух столбцов типа timestamp с часовым поясом), но как насчет других? И тот, который вычитает ВРЕМЯ из TIMESTAMP ?.

А что касается рефакторинга таблиц, стоит ли нам в любом случае использовать TIME WITH TIME ZONE? Стоит ли нам продолжать использовать TIME WITHOUT TIME ZONE? Или лучше вообще забыть тип TIME и объединить ДАТУ с ВРЕМЯ и изменить столбцы на TIMESTAMP WITH TIME ZONE?

Я думаю, что эти вопросы связаны между собой, потому что новые типы столбцов, которые мы выбираем для использования, будут определять, как мы будем работать с ними.

Я лично предпочитаю использовать целочисленный столбец, содержащий временную метку unix, хотя в 2038 году вам нужно будет изменить этот тип столбца.

BShaps 10.05.2018 19:01

IIUC, в настоящее время вы игнорируете часть даты START_HOUR? Или у вас есть ограничение CHECK, обеспечивающее то же самое, что и REPORT_DATE?

Erwin Brandstetter 11.05.2018 00:18

@ErwinBrandstetter: В настоящее время мы сохраняем часть даты, но для всех текущих вычислений мы используем только часть времени. Когда мы обрабатываем устаревший отчет, мы гарантируем, что часть даты и REPORT_DATE совпадают.

Luiz 11.05.2018 00:40
we need to combine the timezone of the client (we have this info). Как именно получить эту информацию без сохранения?
Erwin Brandstetter 11.05.2018 01:58

Мы сохраняем это. В таблице клиентов. Мы знаем, какой клиент импортирует отчет, а также знаем часовой пояс.

Luiz 11.05.2018 04:02
Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
5
5
1 073
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Вы утверждали, что:

every TIME column represents a moment during the day specified in REPORT_DATE.

Таким образом, вы никогда пересекаете линию дат в той же строке. Я предлагаю сохранить 1x date, 3x time и часовой пояс (как столбец text или FK):

CREATE TABLE legacy_table (
   event_id      bigint PRIMARY KEY NOT NULL
 , report_date   date NOT NULL
 , start_hour    time
 , end_hour      time
 , expected_hour time
 , tz            text  -- time zone
);

Как вы уже нашли, timetz (time with time zone) обычно следует избегать. Он не может правильно работать с правилами DST (daylight saving ттrong>ime).

Итак, в основном то, что у вас уже было. Просто отбросьте компонент даты из start_hour, это мертвый груз. Отобразите timestamp на time, чтобы обрезать дату. Нравится: (timestamp '2018-03-25 1:00:00')::time

tz может быть любой строкой, принятой конструкцией AT TIME ZONE, но для надежной работы с разными часовыми поясами лучше использовать исключительно имена часовых поясов. Любой name, который вы найдете в системный каталог pg_timezone_names.

Чтобы оптимизировать хранилище, вы можете собрать имена разрешенных часовых поясов в небольшой поисковой таблице и заменить tz text на tz_id int REFERENCES my_tz_table.

Два примера строк с DST и без него:

INSERT INTO legacy_table VALUES
   (1, '2018-03-25', '1:00', '3:00', '2:00', 'Europe/Vienna')  -- sadly, with DST
 , (2, '2018-03-25', '1:00', '3:00', '2:00', 'Europe/Moscow'); -- Russians got rid of DST

В целях представления или расчетов вы можете делать такие вещи, как:

SELECT (report_date + start_hour)    AT TIME ZONE tz AT TIME ZONE 'UTC' AS start_utc
     , (report_date + end_hour)      AT TIME ZONE tz AT TIME ZONE 'UTC' AS end_utc
     , (report_date + expected_hour) AT TIME ZONE tz AT TIME ZONE 'UTC' AS expected_utc
     -- START_HOUR - END_HOUR
     , (report_date + start_hour) AT TIME ZONE tz
     - (report_date + end_hour)   AT TIME ZONE tz AS start_minus_end
FROM   legacy_table;

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

Обратите внимание на круглые скобки! В противном случае оператор + будет связываться до AT TIME ZONE из-за приоритет оператора.

И вот результаты:

db <> рабочий пример здесь

Поскольку в Вене манипулируют временем (как и в любом другом месте, где действуют глупые правила летнего времени), вы получаете "удивительные" результаты.

Связанный:

Спасибо за исчерпывающий ответ! Однако у меня есть некоторые вопросы: 1) Почему добавление report_date + end_hour и report_date + expected_hour в tz 'Europe/Vienna' дает тот же результат? Учитывая, что начало и ожидание отличаются, как в вашем db <> рабочий пример. 2) Зачем снова применять AT TIME ZONE, используя UTC? Когда мы впервые применяем tz с помощью клиента tz, у нас уже нет метки времени в часовом поясе клиента? Это немного сбивает меня с толку, поскольку при выполнении START_HOUR - END_HOUR вы не применяете AT TIME ZONE дважды.

Luiz 11.05.2018 16:33

@Luiz: 1) Потому что время на 2018-03-25 02:00 в ЕС продвинулось на час вперед из-за глупых правил перехода на летнее время. Таким образом, 3 часа ночи - это то же самое, что 2 часа ночи в этот день. 2) Следуйте моя добавленная ссылка для подробного объяснения. Второй AT TIME ZONE нам не нужен для расчетов, поэтому я его там не использую.

Erwin Brandstetter 12.05.2018 04:46

Не думаю, что я первый, кто проголосует за ответ! Спасибо, Эрвин, за драгоценное время и усилия

Vao Tsun 16.05.2018 10:28

@ErwinBrandstetter Просто чтобы проверить: если мне нужно вычесть TIME (поскольку я сейчас использую TIME WITHOUT TIME ZONE, TIME уже сохранен в клиентском tz) из TIMESTAMPTZ, правильный подход - сначала применить AT TIME ZONE (используя тот же TZ, в каком TIME был сохранен) на TIMESTAMPTZ, верно (предполагается, что у меня нет ДАТЫ для привязки к части ВРЕМЕНИ)? Судя по моим тестам, это кажется правильным подходом, но я не совсем уверен, что он охватит все случаи ...

Luiz 22.05.2018 14:51

Это зависит от того, что именно должно означать "вычесть TIME". Если предполагается, что время учитывает часовой пояс, в котором оно находится, вы должны вычесть из timestamptz, а не из timestamp. См. Это базовое сравнение: dbfiddle.uk/….

Erwin Brandstetter 22.05.2018 23:55

Другие вопросы по теме