По-перше, обробка часу та арифметика 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
в а 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 ).
EDIT
Дозвольте мені трохи більше пояснити різницю між відносними та абсолютними.
Абсолютний час використовується для запису події. Приклади: "Користувач 123 увійшов" або "Церемонії випуску починаються з 2011-05-28 14:00 PST." Незалежно від місцевого часового поясу, якщо ви могли телепортувати місце, де відбулася подія, ви можете стати свідком того, що відбувається. Дані більшості часу в базі даних є абсолютними (і тому в TIMESTAMP WITH TIME ZONE
ідеалі мають бути зміщення +0 та текстова мітка, що представляє правила, що регулюють певний часовий пояс, а не зміщення).
Відносною подією було б записувати або планувати час чогось з точки зору ще не визначеного часового поясу. Приклади: "двері нашого бізнесу відкриваються о 8 ранку та закриваються о 21:00", "зустрічаємось кожного понеділка о 7 ранку на щотижневій зустрічі зі сніданком" або "кожного Хеллоуїна о 20 вечорі". Взагалі, відносний час використовується в шаблоні чи фабриці для подій, а абсолютний час використовується майже для всього іншого. Є один рідкісний виняток, на який варто звернути увагу, який повинен ілюструвати значення відносного часу. Для майбутніх подій, які є досить далекими в майбутньому, де може бути невизначеність абсолютного часу, в який щось може статися, використовуйте відносну мітку часу. Ось приклад із реального світу:
Припустимо, це 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 місяців, слід використовувати відносні часові позначки (запланований = абсолютний проти запланованого = родич ???).
Інший / останній тип відносного часу - це 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
часовий пояс насправді не є 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)
Внесення дат і часових поясів у бази даних - це гарна річ, але легко отримати тонко невірні результати. Необхідні мінімальні додаткові зусилля для правильного та повного зберігання інформації про час, однак це не означає, що додаткові зусилля завжди потрібні.