Вот сценарий: пользователи экономят время только в базе данных в виде строки, например. 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
, но было бы здорово, если бы это сработало! Это было бы идеально для этого случая использования :(
Также, похоже, не существует простого способа конвертировать time
в поддельный timestamp
в postgres. Есть ли другой способ использовать строку 12:00
в столбце для сравнения, где postgres обрабатывает обработку часового пояса/летнего времени?
Сохранять время пользователя в формате UTC
Согласно Postgres datetime он поставляется с time
как time [ (p) ] with time zone
. Далее в документации говорится, что тип времени с часовым поясом определяется стандартом SQL, но это определение демонстрирует свойства, которые приводят к сомнительной полезности. Это вежливый способ сказать, что это в основном бесполезно.
@Bohemian К сожалению, сохранение времени в формате UTC прерывается при перемещении между часовыми поясами.
Что именно вы имеете в виду? Можете ли вы привести пример?
@bohemian Это приложение для отслеживания привычек, и желательное поведение: когда я устанавливаю напоминание на 10:00, находясь на западном побережье, а затем улетаю на несколько дней на восточное побережье, я все равно хочу получить напоминание в 10:00. время восточного побережья. Итак, 10:00 всегда относительно текущего известного часового пояса.
Я обновил вопрос еще одним предложением — возможно, вместо использования to_char
, который не будет работать после преобразования UTC -> PST, мы могли бы использовать EXTRACT и объединить результаты в строку (т. е. 12:00
), которую затем можно легко сравнить с сохраненным напоминанием пользователя. время?
Хорошо, я нашел решение, и оно оказалось на удивление простым. Смотрите мой ответ ниже.
запрос сводится к сравнению времени сервера UTC (левая сторона)
Строго говоря, вы сравниваете тип без учета часового пояса слева с типом с учетом часового пояса справа, что приводит к преобразованию типа слева, предполагая значение по умолчанию TimeZone.
Все сводится к правилам преобразования и текущим настройкам (по умолчанию):
Вы можете использовать в часовом поясе столько раз, сколько вам нужно, каждый раз переключая ввод между with
и without
time 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";
При сравнении time
с timetz
Postgres использует правила приведения/преобразования типа , чтобы найти соответствующий оператор =
, поскольку нет встроенного time=timetz
:
select oprleft::regtype,oprright::regtype,*
from pg_operator
where '{time,timetz}'::regtype[] && array[oprleft,oprright]::regtype[]
and oprname='=';
Есть только time=time
и timetz=timetz
. Если вы попытаетесь смешать их, эти правила выберут вариант с учетом часового пояса как предпочтение:
select typname
from pg_type
where typname ilike '%time%'
and typispreferred;
В итоге вам предстоит time::timetz=timetz
операция.
Когда происходит приведение time::timetz
, Postgres принимает значение по умолчанию TimeZone без учета контекста этого приведения. Несмотря на то, что вы четко указали 'PST'
справа, слева будет использоваться любой TimeZone, установленный по умолчанию на уровне вашей транзакции, сеанса, пользователя, базы данных или системы прямо сейчас - если не переопределен.
Это действительно выполняет свою работу:
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');
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;
Однако это слишком много шагов (time::timetz::time::interval::text
) только для того, чтобы в конечном итоге получить немного менее эффективное text=text
сравнение, в то время как выравнивание типа и часового пояса из пункта 1. дает вам собственное числовое time=time
после выполнения всего лишь time::timetz::time
.
Демо показывает, насколько велика может быть разница — она невелика, но она есть, если вам интересно.
В настоящее время я использую PostgreSQL 13 от Heroku.
Спасибо за подробное объяснение. Все эти рассуждения имеют смысл. Однако пробовали ли вы выполнить запросы из пункта 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, и в этом проблема.
В лучшем случае вы получите SELECT 12:00:00-08 = 12:00:00+00
или наоборот, что всегда неверно, и именно эту проблему я поднимаю.
@MichalKrejčí Да, да. Примеры были предназначены просто для того, чтобы показать добавление еще одного at time zone
слева или справа, чтобы избежать тихого ::timetz
приведения, которое молча добавляет ваш TimeZone
, ведущий к несоответствию. При этом во втором примере отсутствовало понижение до ::time
справа или должно было быть включено еще одно at time zone 'UTC'
слева. Сейчас я это исправил и добавил больше демо и примеров.
Ого, спасибо большое за очень подробный ответ. Я думаю, это здорово, что вы опубликовали это, и я уверен, что многие люди найдут этот пост и помогут им в будущем. Тем временем я тоже нашел решение, но отмечу ваш пост как ответ просто из-за его понимания. Спасибо!
Хорошо, думаю, я понял. Часовой пояс можно легко удалить после разговора о часовом поясе обратно во время без часового пояса для сравнения строк:
SELECT to_char(('20:00'::time AT TIME ZONE 'PST')::time, 'HH24:MI') = '12:00';
time
с часовым поясом практически бесполезен. Если вы хотите это сделать, вам нужно включить компонентdate
.