Как сравнить два времени в двух разных часовых поясах, если они относятся к одному и тому же моменту времени?

Вот сценарий: пользователи экономят время только в базе данных в виде строки, например. 20:00. Они ожидают, что что-то произойдет каждый день в 8 часов вечера. Но 20:00 означает разное время для разных людей, поскольку каждый из них живет в разных часовых поясах, поэтому сохраненное время относится к каждому человеку.

Каждый час сервер, работающий в формате UTC, выполняет запрос на получение людей, с которыми что-то должно произойти (их часовой пояс мы храним отдельно).

Запрос сводится к сравнению времени сервера UTC (слева) с относительным временем хранения в базе данных (справа):

SELECT '20:00'::time = '12:00'::time AT TIME ZONE 'PST'

Но есть проблема с правой стороны: хотя '12:00'::time является своего рода объектом без часового пояса, '12:00'::time IN TIME ZONE 'PST' возвращает 04:00:00-08, как если бы мы конвертировали время UTC в время PST, а это не то, что нам нужно.

В качестве альтернативы я попытался сравнить их как строки, используя:

SELECT to_char('20:00'::time AT TIME ZONE 'PST', 'HH24:MI') = '12:00'

Но это выдает ошибку function to_char(time with time zone, unknown) does not exist. Кажется, to_char нельзя уделить время зоне?

Итак, пытаясь сравнить время или строки, мне не удалось заставить работать ни один из подходов. Есть идеи, как заставить работать любой подход? Есть ли альтернативные подходы? Или есть какой-нибудь способ заставить postgres обрабатывать 12:00 как 12 часов утра по тихоокеанскому времени?

Обновление: может быть, что-то можно собрать с помощью EXTRACT слева, чтобы иметь возможность сравнивать строки?

SELECT EXTRACT(HOUR FROM TIME '20:00'::time at time zone 'PST');
// => 12
SELECT EXTRACT(MINUTE FROM TIME '20:00'::time at time zone 'PST');
// => 0 

Псевдокод:

SELECT CONCAT(
  LEFTPAD(EXTRACT(HOUR FROM TIME '20:00'::time at time zone 'PST') 2 0)
  LEFTPAD(EXTRACT(MINUTE FROM TIME '20:00'::time at time zone 'PST') 2 0)
) = '12:00'
time с часовым поясом практически бесполезен. Если вы хотите это сделать, вам нужно включить компонент date.
Adrian Klaver 14.08.2024 19:14

Я так и думал, что из этого получится time, но было бы здорово, если бы это сработало! Это было бы идеально для этого случая использования :(

Michal Krejčí 14.08.2024 19:18

Также, похоже, не существует простого способа конвертировать time в поддельный timestamp в postgres. Есть ли другой способ использовать строку 12:00 в столбце для сравнения, где postgres обрабатывает обработку часового пояса/летнего времени?

Michal Krejčí 14.08.2024 19:23

Сохранять время пользователя в формате UTC

Bohemian 14.08.2024 19:54

Согласно Postgres datetime он поставляется с time как time [ (p) ] with time zone. Далее в документации говорится, что тип времени с часовым поясом определяется стандартом SQL, но это определение демонстрирует свойства, которые приводят к сомнительной полезности. Это вежливый способ сказать, что это в основном бесполезно.

Adrian Klaver 14.08.2024 20:08

@Bohemian К сожалению, сохранение времени в формате UTC прерывается при перемещении между часовыми поясами.

Michal Krejčí 16.08.2024 08:26

Что именно вы имеете в виду? Можете ли вы привести пример?

Bohemian 16.08.2024 09:08

@bohemian Это приложение для отслеживания привычек, и желательное поведение: когда я устанавливаю напоминание на 10:00, находясь на западном побережье, а затем улетаю на несколько дней на восточное побережье, я все равно хочу получить напоминание в 10:00. время восточного побережья. Итак, 10:00 всегда относительно текущего известного часового пояса.

Michal Krejčí 16.08.2024 09:44

Я обновил вопрос еще одним предложением — возможно, вместо использования to_char, который не будет работать после преобразования UTC -> PST, мы могли бы использовать EXTRACT и объединить результаты в строку (т. е. 12:00), которую затем можно легко сравнить с сохраненным напоминанием пользователя. время?

Michal Krejčí 16.08.2024 09:49

Хорошо, я нашел решение, и оно оказалось на удивление простым. Смотрите мой ответ ниже.

Michal Krejčí 16.08.2024 10:04
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
1
10
60
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

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

запрос сводится к сравнению времени сервера UTC (левая сторона)

Строго говоря, вы сравниваете тип без учета часового пояса слева с типом с учетом часового пояса справа, что приводит к преобразованию типа слева, предполагая значение по умолчанию TimeZone.

Все сводится к правилам преобразования и текущим настройкам (по умолчанию):

  1. Вы можете использовать в часовом поясе столько раз, сколько вам нужно, каждый раз переключая ввод между with и withouttime zone. В этом случае вам, вероятно, понадобится еще по одному с каждой стороны уравнения:
    приведите левый операнд к времени с учетом часового пояса в 'PST':

    SELECT '20:00'::time at time zone 'PST' = '12:00'::time AT TIME ZONE 'PST';
    

    или добавьте еще один сдвиг с 'PST' на 'UTC' справа, чтобы сместить его и после этого удалить часовой пояс:

    SELECT '20:00'::time = ('12:00'::time AT TIME ZONE 'PST' at time zone 'UTC')::time;
    

    См. этот пример:

    SELECT '20:00'::time AS "C"
          ,'20:00'::time::timetz AS "D"
          ,'21:00'::time AT TIME ZONE 'PST' AT TIME ZONE 'UTC' AS "E"
          ,('21:00'::time AT TIME ZONE 'PST' AT TIME ZONE 'UTC')::time AS "F"
          ,'20:00'::time = ('21:00'::time AT TIME ZONE 'PST' AT TIME ZONE 'UTC')::time AS "C=F"
          ,'20:00'::time::timetz = ('21:00'::time AT TIME ZONE 'PST' AT TIME ZONE 'UTC')::time AS "D=F"
          ,'21:00'::time AT TIME ZONE 'PST' AT TIME ZONE 'UTC' = ('21:00'::time AT TIME ZONE 'PST' AT TIME ZONE 'UTC')::time AS "E=F";
    
    С Д Э Ф С=F Д=Ф Э=Ф 20:00:00 20:00:00+01 20:00:00+00 20:00:00 Т Т ж
  2. При сравнении time с timetz Postgres использует правила приведения/преобразования типа , чтобы найти соответствующий оператор =, поскольку нет встроенного time=timetz:

    select oprleft::regtype,oprright::regtype,* 
    from pg_operator 
    where '{time,timetz}'::regtype[] && array[oprleft,oprright]::regtype[]
    and oprname='=';
    
    опрлевый правильно оид имя_опра пространство имен собственник опркинд слияние опрканхэш опрлевый правильно результат опрком опрнегат код операции опррест присоединиться время без часового пояса время без часового пояса 1108 = 11 10 б т т 1083 1083 16 1108 1109 time_eq эксел eqjoinsel время с часовым поясом время с часовым поясом 1550 г. = 11 10 б т т 1266 1266 16 1550 г. 1551 г. timetz_eq эксел eqjoinsel

    Есть только time=time и timetz=timetz. Если вы попытаетесь смешать их, эти правила выберут вариант с учетом часового пояса как предпочтение:

    select typname 
    from pg_type 
    where typname ilike '%time%'
      and typispreferred;
    
    Типовое имя временная метка

    В итоге вам предстоит time::timetz=timetz операция.

  3. Когда происходит приведение time::timetz, Postgres принимает значение по умолчанию TimeZone без учета контекста этого приведения. Несмотря на то, что вы четко указали 'PST' справа, слева будет использоваться любой TimeZone, установленный по умолчанию на уровне вашей транзакции, сеанса, пользователя, базы данных или системы прямо сейчас - если не переопределен.

  4. Это действительно выполняет свою работу:

    SELECT to_char(('20:00'::time AT TIME ZONE 'PST')::time, 'HH24:MI') = '12:00';
    

    Postgres находит вариант interval_to_char для to_char() посредством приведения time::interval: demo в db<>fiddle

    explain verbose SELECT to_char(('20:00'::time AT TIME ZONE 'PST')::time, 'HH24:MI');
    
    Вывод: to_char(((('20:00:00'::время без часового пояса AT TIME ZONE 'PST'::text))::время без часового пояса)::interval, 'HH24:MI'::text )
    SELECT p.pronamespace::regnamespace as "Schema",
      p.proname,
      pg_catalog.pg_get_function_result(p.oid) as "Result data type",
      pg_catalog.pg_get_function_arguments(p.oid) as "Argument data types",
      prosrc,
      pg_catalog.pg_function_is_visible(p.oid)
    FROM pg_catalog.pg_proc p
    WHERE p.proname ~ '^(to_char)$'
    ORDER BY 1, 2, 4;
    
    Схема проназвание Тип данных результата Типы данных аргументов Просрк pg_function_is_visible pg_catalog to_char текст биинт, текст int8_to_char т pg_catalog to_char текст двойная точность, текст float8_to_char т pg_catalog to_char текст целое число, текст int4_to_char т pg_catalog to_char текст интервал, текст интервал_to_char т pg_catalog to_char текст числовой, текстовый numeric_to_char т pg_catalog to_char текст настоящий, текст float4_to_char т pg_catalog to_char текст временная метка без часового пояса, текст timestamp_to_char т pg_catalog to_char текст временная метка с часовым поясом, текст timestamptz_to_char т

    Однако это слишком много шагов (time::timetz::time::interval::text) только для того, чтобы в конечном итоге получить немного менее эффективное text=text сравнение, в то время как выравнивание типа и часового пояса из пункта 1. дает вам собственное числовое time=time после выполнения всего лишь time::timetz::time.
    Демо показывает, насколько велика может быть разница — она невелика, но она есть, если вам интересно.

В настоящее время я использую PostgreSQL 13 от Heroku.

Michal Krejčí 16.08.2024 08:29

Спасибо за подробное объяснение. Все эти рассуждения имеют смысл. Однако пробовали ли вы выполнить запросы из пункта 1? Проблема в том, что '12:00'::time AT TIME ZONE 'PST' at time zone 'UTC' — это снова 12:00:00+00, который не равен '20:00'::time или 20:00::time AT TIME ZONE 'PST'. Кажется, невозможно построить запрос SELECT left = right // => true, используя 20:00 с одной стороны и 12:00 с другой, поскольку база данных всегда будет рассматривать оба значения как UTC, и в этом проблема.

Michal Krejčí 16.08.2024 09:32

В лучшем случае вы получите SELECT 12:00:00-08 = 12:00:00+00 или наоборот, что всегда неверно, и именно эту проблему я поднимаю.

Michal Krejčí 16.08.2024 09:34

@MichalKrejčí Да, да. Примеры были предназначены просто для того, чтобы показать добавление еще одного at time zone слева или справа, чтобы избежать тихого ::timetz приведения, которое молча добавляет ваш TimeZone, ведущий к несоответствию. При этом во втором примере отсутствовало понижение до ::time справа или должно было быть включено еще одно at time zone 'UTC' слева. Сейчас я это исправил и добавил больше демо и примеров.

Zegarek 16.08.2024 13:22

Ого, спасибо большое за очень подробный ответ. Я думаю, это здорово, что вы опубликовали это, и я уверен, что многие люди найдут этот пост и помогут им в будущем. Тем временем я тоже нашел решение, но отмечу ваш пост как ответ просто из-за его понимания. Спасибо!

Michal Krejčí 19.08.2024 13:23

Хорошо, думаю, я понял. Часовой пояс можно легко удалить после разговора о часовом поясе обратно во время без часового пояса для сравнения строк:

SELECT to_char(('20:00'::time AT TIME ZONE 'PST')::time, 'HH24:MI') = '12:00';

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