Po pierwsze, obsługa czasu i arytmetyka PostgreSQL jest fantastyczna, a opcja 3 jest w porządku w ogólnym przypadku. Jest to jednak niekompletny obraz czasu i stref czasowych i można go uzupełnić:
- Zapisz nazwę strefy czasowej użytkownika jako preferencję użytkownika (np.
America/Los_Angeles
, a nie-0700
). - Przesyłanie danych o zdarzeniach/czasach użytkownika lokalnie w ich układzie odniesienia (najprawdopodobniej przesunięcie względem czasu UTC, takie jak
-0700
). - W aplikacji przekonwertuj czas na
UTC
i przechowywane przy użyciuTIMESTAMP WITH TIME ZONE
kolumna. - Żądania zwrotu czasu lokalnego na strefę czasową użytkownika (tj. konwersja z
UTC
doAmerica/Los_Angeles
). - Ustaw
timezone
bazy danych doUTC
.
Ta opcja nie zawsze działa, ponieważ ustalenie strefy czasowej użytkownika może być trudne, dlatego zaleca się użycie TIMESTAMP WITH TIME ZONE
do lekkich zastosowań. To powiedziawszy, pozwól mi bardziej szczegółowo wyjaśnić niektóre aspekty tej Opcji 4.
Podobnie jak w przypadku opcji 3, powód WITH TIME ZONE
dzieje się tak, ponieważ czas, w którym coś się wydarzyło, jest absolutny moment w czasie. WITHOUT TIME ZONE
daje krewny strefa czasowa. Nigdy, przenigdy nie mieszaj bezwzględnych i względnych ZNACZNIKÓW CZASU.
Z perspektywy programistycznej i spójności upewnij się, że wszystkie obliczenia są wykonywane przy użyciu czasu UTC jako strefy czasowej. Nie jest to wymóg PostgreSQL, ale pomaga przy integracji z innymi językami programowania lub środowiskami. Ustawianie CHECK
na kolumnie, aby upewnić się, że zapis do kolumny znacznika czasu ma przesunięcie strefy czasowej o wartości 0
to pozycja obronna, która zapobiega kilku klasom błędów (np. skrypt zrzuca dane do pliku, a coś innego sortuje dane czasowe za pomocą sortowania leksykalnego). Ponownie, PostgreSQL nie potrzebuje tego do poprawnego obliczania dat lub konwersji między strefami czasowymi (tj. PostgreSQL jest bardzo biegły w konwersji czasu między dowolnymi dwiema dowolnymi strefami czasowymi). Aby upewnić się, że dane wchodzące do bazy danych są przechowywane z przesunięciem równym zero:
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
Nie jest w 100% doskonały, ale zapewnia wystarczająco silny środek zapobiegający strzelaniu do stóp, który zapewnia, że dane są już przekonwertowane na UTC. Istnieje wiele opinii na temat tego, jak to zrobić, ale z mojego doświadczenia wynika, że jest to najlepsza praktyka.
Krytyka obsługi stref czasowych bazy danych jest w dużej mierze uzasadniona (jest wiele baz danych, które radzą sobie z tym z dużą niekompetencją), jednak obsługa znaczników czasu i stref czasowych przez PostgreSQL jest całkiem niesamowita (pomimo kilku „funkcji” tu i tam). Na przykład jedna z takich funkcji:
-- 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)
Pamiętaj, że AT TIME ZONE 'UTC'
usuwa informacje o strefie czasowej i tworzy względny TIMESTAMP WITHOUT TIME ZONE
używając układu odniesienia celu (UTC
).
Podczas konwersji z niekompletnego TIMESTAMP WITHOUT TIME ZONE
do TIMESTAMP WITH TIME ZONE
, brakująca strefa czasowa jest dziedziczona z Twojego połączenia:
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)
Dolna linia:
- zapisz strefę czasową użytkownika jako nazwaną etykietę (np.
America/Los_Angeles
), a nie przesunięcie względem czasu UTC (np.-0700
) - użyj czasu UTC do wszystkiego, chyba że istnieje ważny powód, aby przechowywać niezerowe przesunięcie
- traktuj wszystkie niezerowe czasy UTC jako błąd wejściowy
- nigdy nie mieszaj i nie dopasowuj względnych i bezwzględnych znaczników czasu
- użyj także
UTC
jakotimezone
w bazie danych, jeśli to możliwe
Uwaga dotycząca losowego języka programowania:datetime
Pythona typ danych jest bardzo dobry w utrzymywaniu rozróżnienia między czasami bezwzględnymi a względnymi (choć początkowo frustrujące, dopóki nie uzupełnisz go biblioteką taką jak PyTZ).
EDYTUJ
Pozwólcie, że wyjaśnię nieco więcej różnicy między wartościami względnymi a absolutnymi.
Do rejestracji zdarzenia używany jest czas bezwzględny. Przykłady:„Zalogowany użytkownik 123” lub „ceremonia wręczenia dyplomów rozpoczyna się 28.05.2011 o 14:00 czasu PST”. Bez względu na lokalną strefę czasową, gdybyś mógł teleportować się do miejsca, w którym miało miejsce zdarzenie, mógłbyś być jego świadkiem. Większość danych czasowych w bazie danych jest bezwzględna (i dlatego powinna być TIMESTAMP WITH TIME ZONE
, najlepiej z przesunięciem +0 i etykietą tekstową reprezentującą zasady rządzące konkretną strefą czasową - nie przesunięcie).
Względnym wydarzeniem byłoby nagranie lub zaplanowanie czasu czegoś z perspektywy strefy czasowej, która jeszcze nie została określona. Przykłady:„drzwi naszej firmy otwierają się o 8 rano i zamykają o 21:00”, „spotkajmy się w każdy poniedziałek o 7 rano na cotygodniowym spotkaniu śniadaniowym” lub „w każde Halloween o 20:00”. Ogólnie rzecz biorąc, czas względny jest używany w szablonie lub fabryce dla zdarzeń, a czas bezwzględny jest używany dla prawie wszystkiego innego. Jest jeden rzadki wyjątek, na który warto zwrócić uwagę, który powinien zilustrować wartość czasów względnych. W przypadku przyszłych zdarzeń, które są wystarczająco odległe w przyszłości, w których może istnieć niepewność co do bezwzględnego czasu, w którym coś może się wydarzyć, użyj względnego znacznika czasu. Oto przykład z prawdziwego świata:
Załóżmy, że jest rok 2004 i musisz zaplanować dostawę 31 października 2008 r. o godzinie 13:00 na zachodnim wybrzeżu Stanów Zjednoczonych (tj. America/Los_Angeles
/PST8PDT
). Jeśli zapisałeś to przy użyciu czasu bezwzględnego za pomocą ’2008-10-31 21:00:00.000000+00’::TIMESTAMP WITH TIME ZONE
, dostawa pojawiłaby się o godzinie 14.00, ponieważ rząd Stanów Zjednoczonych uchwalił ustawę o polityce energetycznej z 2005 r., która zmieniła zasady regulujące czas letni. W 2004 roku, kiedy zaplanowano dostawę, data 10-31-2008
byłby czas pacyficzny standardowy (+8000
), ale począwszy od roku 2005+ bazy danych stref czasowych rozpoznały, że 10-31-2008
byłby czas letni na Pacyfiku (+0700
). Przechowywanie względnego znacznika czasu ze strefą czasową skutkowałoby prawidłowym harmonogramem dostaw, ponieważ względny znacznik czasu jest odporny na manipulacje ze strony Kongresu ze strony niewłaściwych informacji. Gdzie granica między używaniem względnych i bezwzględnych czasów do planowania jest rozmyta, ale moją praktyczną zasadą jest to, że planowanie czegokolwiek w przyszłości dalej niż 3-6 miesięcy powinno korzystać ze względnych znaczników czasu (zaplanowane =bezwzględne vs planowane =względny ???).
Innym/ostatnim typem czasu względnego jest INTERVAL
. Przykład:„sesja wygaśnie 20 minut po zalogowaniu się użytkownika”. INTERVAL
mogą być używane poprawnie z bezwzględnymi znacznikami czasu (TIMESTAMP WITH TIME ZONE
) lub względne znaczniki czasu (TIMESTAMP WITHOUT TIME ZONE
). Równie poprawne jest powiedzenie „sesja użytkownika wygasa po 20 minutach od pomyślnego zalogowania (login_utc + session_duration)” lub „nasze poranne spotkanie przy śniadaniu może trwać tylko 60 minut (recurring_start_time + meeting_length)”.
Ostatnie zamieszanie:DATE
, TIME
, TIME WITHOUT TIME ZONE
i TIME WITH TIME ZONE
to wszystkie względne typy danych. Na przykład:'2011-05-28'::DATE
reprezentuje datę względną, ponieważ nie masz informacji o strefie czasowej, które mogłyby posłużyć do zidentyfikowania północy. Podobnie '23:23:59'::TIME
jest względne, ponieważ nie znasz strefy czasowej ani DATE
reprezentowany przez czas. Nawet z '23:59:59-07'::TIME WITH TIME ZONE
, nie wiesz, jaka jest DATE
byłoby. I na koniec DATE
ze strefą czasową nie jest w rzeczywistości DATE
, jest to 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)
Umieszczanie dat i stref czasowych w bazach danych to dobra rzecz, ale łatwo jest uzyskać nieco niepoprawne wyniki. Prawidłowe i pełne przechowywanie informacji o czasie wymaga minimalnego dodatkowego wysiłku, jednak nie oznacza to, że dodatkowy wysiłek jest zawsze wymagany.