Какой тип отметки времени выбрать в базе данных PostgreSQL?

119

Я хотел бы определить передовой метод хранения временных меток в моей базе данных Postgres в контексте проекта с несколькими часовыми поясами.

Я могу

  1. выберите TIMESTAMP WITHOUT TIME ZONEи запомните, какой часовой пояс использовался при вставке этого поля
  2. выберите TIMESTAMP WITHOUT TIME ZONEи добавьте еще одно поле, которое будет содержать имя часового пояса, который использовался во время вставки
  3. выберите TIMESTAMP WITH TIME ZONEи вставьте метки времени соответственно

Я немного предпочитаю вариант 3 (отметка времени с часовым поясом), но хотел бы получить обоснованное мнение по этому поводу.

Джером ВАГНЕР
источник

Ответы:

142

Во-первых, обработка времени и арифметика PostgreSQL великолепны, а вариант 3 в общем случае подходит. Однако это неполное представление о времени и часовых поясах, и его можно дополнить:

  1. Сохраните название часового пояса пользователя в качестве предпочтения пользователя (например America/Los_Angeles, нет -0700).
  2. Отправляйте данные о пользовательских событиях / времени локально в их систему отсчета (скорее всего, смещение от UTC, например -0700).
  3. В приложении преобразуйте время в столбец UTCи сохраните его TIMESTAMP WITH TIME ZONE.
  4. Возвращать запросы времени, локальные для часового пояса пользователя (т. Е. Преобразовать из UTCвAmerica/Los_Angeles ).
  5. Установите для вашей базы данных 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)

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

Шон
источник
2
Если вы точно укажете postgresql правильный часовой пояс, в котором находится метка времени пользователя, postgresql сделает всю тяжелую работу за кулисами. Самостоятельное преобразование - всего лишь проблема.
Сет Робертсон
1
@Sean - с вашим ограничением проверки, как вы вообще вставляете временную метку без set timezone to 'UTC'? Вы знаете, что все даты с учетом часовых поясов хранятся внутри в формате UTC ?
2
Цель проверки - убедиться, что данные хранятся с нулевым смещением от UTC. Сортировка и поиск информации и сравнение времен с ненулевыми смещениями подвержены ошибкам. Установив нулевое смещение по всемирному координированному времени, вы можете последовательно взаимодействовать с данными с единой точки зрения практически без риска, что ведет себя предсказуемо во всех сценариях. Если бы отметки времени поддерживали текстовые представления часовых поясов, мои мысли по этому поводу были бы другими. : ~]
Шон
6
@Sean: Но, как указывает Джек, все временные метки с учетом часовых поясов в основном хранятся внутри в формате UTC и конвертируются в ваш местный часовой пояс при использовании; фактически, extract (timezone from ...) всегда будет возвращать, какой бы ни был локальный часовой пояс соединения: это не имеет никакого отношения к тому, как была «сохранена» временная метка. Иными словами, часовой пояс вообще не является частью типа и не может быть сохранен: «с часовым поясом» - это просто свойство того, как данные будут преобразованы при взаимодействии с другими типами. Таким образом, данные вообще не имеют представления часовых поясов, текстовых или иных.
Jay Freeman -saurik-
@ JayFreeman-saurik-: ты абсолютно прав. «CHECK ()» используется как мера защиты от выстрела ногами для защиты от, возможно, хитрого кода. Обеспечение данных в формате UTC при записи дает скромную гарантию того, что код продуман или среда выполнения настроена правильно.
Шон
59

Ответ Шона слишком сложен и вводит в заблуждение.

Дело в том, что и «WITH TIME ZONE», и «WITHOUT TIME ZONE» сохраняют значение как unix-подобную абсолютную временную метку UTC. Вся разница в том, как отображается метка времени. Когда "С часовым поясом", то отображаемое значение является сохраненным значением UTC, переведенным в зону пользователя. Когда "БЕЗ часового пояса" сохраненное значение UTC перекручивается, чтобы показывать тот же циферблат независимо от того, какую зону установил пользователь ».

Единственная ситуация, когда можно использовать «БЕЗ часового пояса», - это когда номинал часов применим независимо от фактического пояса. Например, отметка времени указывает, когда кабины для голосования могут закрываться (т. Е. Они закрываются в 20:00 независимо от часового пояса человека).

Используйте вариант 3. Всегда используйте «С часовым поясом», если нет особой причины не делать этого.

сойка
источник
10
Дэвид Э. Уиллер, крупный эксперт по Postgres, согласится с вашей оценкой в ​​соответствии с его сообщением « Всегда использовать TIMESTAMP WITH TIME ZONE» .
Basil Bourque
2
Что, если вы попросите браузер преобразовать метку времени UTC в местный часовой пояс? Таким образом, db никогда не будет выполнять преобразование и будет содержать только UTC. Будет ли приемлемым вариант "БЕЗ часового пояса"?
dman
5

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

GordonM
источник
19
Неправильно. Никаких накладных расходов… Postgres не сохраняет часовой пояс (правильный термин - «смещение», а не часовой пояс, кстати). TIMESTAMP WITH TIME ZONEНазвание вводит в заблуждение. На самом деле это означает «обратите внимание на любое указанное смещение при вставке / обновлении и используйте это смещение, чтобы настроить дату и время на UTC». В TIMESTAMP WITHOUT TIME ZONEозначает имя «игнорировать любое смещение , которое может присутствовать во время вставки / обновления, рассмотрят часть даты и времени , как в UTC без необходимости регулировки». Внимательно прочтите документ .
Basil Bourque
1
@BasilBourque благодарим вас за эту информацию. Невероятно полезно. Для других, читающих это, строка из документа говорит: «В литерале, который был определен как временная метка без часового пояса, PostgreSQL будет молча игнорировать любое указание часового пояса. То есть результирующее значение получается из полей даты / времени в
вводимое