Во-первых, обработка времени и арифметика PostgreSQL великолепны, а вариант 3 в общем случае подходит. Однако это неполное представление о времени и часовых поясах, и его можно дополнить:
- Сохраните название часового пояса пользователя в качестве предпочтения пользователя (например
America/Los_Angeles
, нет -0700
).
- Отправляйте данные о пользовательских событиях / времени локально в их систему отсчета (скорее всего, смещение от UTC, например
-0700
).
- В приложении преобразуйте время в столбец
UTC
и сохраните его TIMESTAMP WITH TIME ZONE
.
- Возвращать запросы времени, локальные для часового пояса пользователя (т. Е. Преобразовать из
UTC
вAmerica/Los_Angeles
).
- Установите для вашей базы данных
timezone
значение UTC
.
Этот параметр не всегда работает, потому что может быть сложно получить часовой пояс пользователя и, следовательно, совет по хеджированию TIMESTAMP WITH TIME ZONE
для легких приложений. Тем не менее, позвольте мне более подробно объяснить некоторые основные аспекты этого варианта 4.
Как и в варианте 3, причина в том WITH TIME ZONE
, что время, когда что-то произошло, является абсолютным моментом времени. WITHOUT TIME ZONE
дает относительный часовой пояс. Никогда и никогда не смешивайте абсолютные и относительные значения TIMESTAMP.
С точки зрения программирования и согласованности убедитесь, что все расчеты производятся с использованием UTC в качестве часового пояса. Это не требование PostgreSQL, но помогает при интеграции с другими языками программирования или средами. Установка CHECK
в столбце, чтобы убедиться, что запись в столбец отметки времени имеет смещение часового пояса, 0
является защитной позицией, которая предотвращает несколько классов ошибок (например, сценарий выгружает данные в файл, а что-то еще сортирует данные времени, используя лексическая сортировка). Опять же, PostgreSQL не нуждается в этом для правильного вычисления даты или для преобразования между часовыми поясами (т.е. PostgreSQL очень искусен в преобразовании времени между любыми двумя произвольными часовыми поясами). Чтобы данные, поступающие в базу данных, сохранялись со смещением нуля:
CREATE TABLE my_tbl (
my_timestamp TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
CHECK(EXTRACT(TIMEZONE FROM my_timestamp) = '0')
);
test=> SET timezone = 'America/Los_Angeles';
SET
test=> INSERT INTO my_tbl (my_timestamp) VALUES (NOW());
ERROR: new row for relation "my_tbl" violates check constraint "my_tbl_my_timestamp_check"
test=> SET timezone = 'UTC';
SET
test=> INSERT INTO my_tbl (my_timestamp) VALUES (NOW());
INSERT 0 1
Он не идеален на 100%, но он обеспечивает достаточно сильную меру защиты от сбоев, которая гарантирует, что данные уже преобразованы в UTC. Существует множество мнений о том, как это сделать, но, судя по моему опыту, это лучший вариант на практике.
Критика обработки часовых поясов базы данных в значительной степени оправдана (существует множество баз данных, которые справляются с этим с большой некомпетентностью), однако обработка временных меток и часовых поясов в PostgreSQL довольно хороша (несмотря на несколько "особенностей" здесь и там). Например, одна такая функция:
-- Make sure we're all working off of the same local time zone
test=> SET timezone = 'America/Los_Angeles';
SET
test=> SELECT NOW();
now
-------------------------------
2011-05-27 15:47:58.138995-07
(1 row)
test=> SELECT NOW() AT TIME ZONE 'UTC';
timezone
----------------------------
2011-05-27 22:48:02.235541
(1 row)
Обратите внимание, что информация о AT TIME ZONE 'UTC'
часовом поясе удаляется и создается относительный, TIMESTAMP WITHOUT TIME ZONE
используя систему отсчета вашей цели (UTC
).
При преобразовании из неполного TIMESTAMP WITHOUT TIME ZONE
в a TIMESTAMP WITH TIME ZONE
отсутствующий часовой пояс наследуется от вашего соединения:
test=> SET timezone = 'America/Los_Angeles';
SET
test=> SELECT EXTRACT(TIMEZONE_HOUR FROM NOW());
date_part
-----------
-7
(1 row)
test=> SELECT EXTRACT(TIMEZONE_HOUR FROM TIMESTAMP WITH TIME ZONE '2011-05-27 22:48:02.235541');
date_part
-----------
-7
(1 row)
-- Now change to UTC
test=> SET timezone = 'UTC';
SET
-- Create an absolute time with timezone offset:
test=> SELECT NOW();
now
-------------------------------
2011-05-27 22:48:40.540119+00
(1 row)
-- Creates a relative time in a given frame of reference (i.e. no offset)
test=> SELECT NOW() AT TIME ZONE 'UTC';
timezone
----------------------------
2011-05-27 22:48:49.444446
(1 row)
test=> SELECT EXTRACT(TIMEZONE_HOUR FROM NOW());
date_part
-----------
0
(1 row)
test=> SELECT EXTRACT(TIMEZONE_HOUR FROM TIMESTAMP WITH TIME ZONE '2011-05-27 22:48:02.235541');
date_part
-----------
0
(1 row)
Суть:
- сохранять часовой пояс пользователя как именованную метку (например
America/Los_Angeles
), а не смещение от UTC (например,-0700
)
- используйте UTC для всего, если нет веской причины хранить ненулевое смещение
- рассматривать все ненулевое время UTC как ошибку ввода
- никогда не смешивайте и не сопоставляйте относительные и абсолютные отметки времени
- также используйте
UTC
как timezone
в базе данных, если возможно
Примечание по случайному языку программирования: datetime
тип данных Python очень хорошо поддерживает различие между абсолютным и относительным временем (хотя поначалу это разочаровывает, пока вы не добавите в него такую библиотеку, как PyTZ ).
РЕДАКТИРОВАТЬ
Позвольте мне немного подробнее объяснить разницу между относительным и абсолютным.
Абсолютное время используется для записи события. Примеры: «Пользователь 123 вошел в систему» или «церемония вручения дипломов начинается в 28 мая 2011 года в 2 часа дня по тихоокеанскому стандартному времени». Независимо от вашего местного часового пояса, если бы вы могли телепортироваться туда, где произошло событие, вы могли бы стать его свидетелем. В большинстве случаев данные в базе данных являются абсолютными (и поэтому должны бытьTIMESTAMP WITH TIME ZONE
идеале смещение +0 и текстовую метку, представляющую правила, регулирующие конкретный часовой пояс, а не смещение).
Относительным событием может быть запись или планирование времени чего-либо с точки зрения часового пояса, который еще предстоит определить. Примеры: «двери нашего бизнеса открываются в 8:00 и закрываются в 21:00», «давайте встречаться каждый понедельник в 7:00 для еженедельного завтрака» или «каждый Хэллоуин в 20:00». В общем, относительное время используется в шаблоне или фабрике для событий, а абсолютное время используется почти для всего остального. Стоит указать на одно редкое исключение, которое должно проиллюстрировать ценность относительного времени. Для будущих событий, которые находятся достаточно далеко в будущем, когда может возникнуть неопределенность относительно абсолютного времени, в которое что-то может произойти, используйте относительную временную метку. Вот пример из реального мира:
Предположим, сейчас 2004 год, и вам нужно запланировать доставку на 31 октября 2008 года в 13:00 на западном побережье США (т.е. America/Los_Angeles
/ PST8PDT
). Если вы сохранили это, используя абсолютное время ’2008-10-31 21:00:00.000000+00’::TIMESTAMP WITH TIME ZONE
, доставка должна была появиться в 14:00, потому что правительство США приняло Закон об энергетической политике 2005 года, который изменил правила, регулирующие переход на летнее время. В 2004 году, когда была запланирована доставка, датой 10-31-2008
должно было быть тихоокеанское стандартное время ( +8000
), но, начиная с 2005 года, базы данных часовых поясов признали, что 10-31-2008
это было тихоокеанское летнее время (+0700
). Сохранение относительной временной метки с часовым поясом привело бы к правильному расписанию доставки, потому что относительная временная метка невосприимчива к необоснованному вмешательству Конгресса. Граница между использованием относительного и абсолютного времени для планирования является нечеткой линией, но мое практическое правило состоит в том, что при планировании чего-либо в будущем, кроме 3-6 месяцев, следует использовать относительные временные метки (запланированное = абсолютное vs запланированное = родственник ???).
Другой / последний тип относительного времени - это INTERVAL
. Пример: «время ожидания сеанса истекает через 20 минут после входа пользователя в систему». INTERVAL
Может быть корректно использоваться либо с временными метками (абсолютными TIMESTAMP WITH TIME ZONE
) или относительными временными метками (TIMESTAMP WITHOUT TIME ZONE
). В равной степени правильно сказать, «сеанс пользователя истекает через 20 минут после успешного входа в систему (login_utc + session_duration)» или «наша утренняя встреча за завтраком может длиться только 60 минут (recurring_start_time + meeting_length)».
Последние биты путаницы: DATE
, TIME
, TIME WITHOUT TIME ZONE
и TIME WITH TIME ZONE
все относительные типы данных. Например: '2011-05-28'::DATE
представляет собой относительную дату, поскольку у вас нет информации о часовом поясе, которая могла бы использоваться для определения полуночи. Точно так же '23:23:59'::TIME
относительно, потому что вы не знаете ни часовой пояс, ни DATE
время. Даже '23:59:59-07'::TIME WITH TIME ZONE
если вы не знаете, что это DATE
будет за. И наконец, DATE
часовой пояс на самом деле не a DATE
, а TIMESTAMP WITH TIME ZONE
:
test=> SET timezone = 'America/Los_Angeles';
SET
test=> SELECT '2011-05-11'::DATE AT TIME ZONE 'UTC';
timezone
---------------------
2011-05-11 07:00:00
(1 row)
test=> SET timezone = 'UTC';
SET
test=> SELECT '2011-05-11'::DATE AT TIME ZONE 'UTC';
timezone
---------------------
2011-05-11 00:00:00
(1 row)
Ввод даты и часовых поясов в базы данных - это хорошо, но легко получить слегка неверные результаты. Для правильного и полного хранения информации о времени требуются минимальные дополнительные усилия, однако это не означает, что дополнительные усилия требуются всегда.