Za prvé, zpracování času a aritmetika PostgreSQL je fantastické a možnost 3 je v obecném případě v pořádku. Je to však neúplný pohled na čas a časová pásma a lze jej doplnit:
- Uložte název časového pásma uživatele jako uživatelské preference (např.
America/Los_Angeles
, nikoli-0700
). - Nechat si odesílat údaje o událostech/čase uživatele lokálně podle jejich referenčního rámce (nejpravděpodobněji odchylka od UTC, například
-0700
). - V aplikaci převeďte čas na
UTC
a uloženy pomocíTIMESTAMP WITH TIME ZONE
sloupec. - Vrátit požadavky na čas v místním časovém pásmu uživatele (tj. převést z
UTC
doAmerica/Los_Angeles
). - Nastavte
timezone
databáze doUTC
.
Tato možnost nefunguje vždy, protože může být obtížné získat časové pásmo uživatele, a proto je doporučeno použít TIMESTAMP WITH TIME ZONE
pro lehké aplikace. Dovolte mi proto vysvětlit některé základní aspekty této možnosti 4 podrobněji.
Stejně jako možnost 3, důvod pro WITH TIME ZONE
protože čas, kdy se něco stalo, je absolutní chvíle v čase. WITHOUT TIME ZONE
dává příbuzného časové pásmo. Nikdy, nikdy, nikdy nesměšujte absolutní a relativní TIMESTAMP.
Z programového a konzistentního hlediska zajistěte, aby byly všechny výpočty prováděny s použitím časového pásma UTC. Toto není požadavek PostgreSQL, ale pomáhá to při integraci s jinými programovacími jazyky nebo prostředími. Nastavení CHECK
na sloupci, abyste se ujistili, že zápis do sloupce časového razítka má posun časového pásma 0
je obranná pozice, která zabraňuje několika třídám chyb (např. skript ukládá data do souboru a něco jiného třídí časová data pomocí lexikálního řazení). Opět platí, že PostgreSQL to nepotřebuje ke správnému výpočtu data nebo k převodu mezi časovými pásmy (tj. PostgreSQL je velmi zběhlý v převodu časů mezi libovolnými dvěma libovolnými časovými pásmy). Aby bylo zajištěno, že data vstupující do databáze budou uložena s offsetem nula:
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
Není to 100% dokonalé, ale poskytuje dostatečně silné opatření proti střelbě, které zajišťuje, že data jsou již převedena na UTC. Existuje mnoho názorů na to, jak to udělat, ale toto se z mé zkušenosti zdá být v praxi nejlepší.
Kritika zpracování databázových časových pásem je z velké části oprávněná (existuje spousta databází, které to zvládají s velkou nekompetencí), nicméně PostgreSQL nakládání s časovými razítky a časovými pásmy je docela úžasné (i přes několik „funkcí“ tu a tam). Například jedna taková funkce:
-- 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)
Všimněte si, že AT TIME ZONE 'UTC'
odebere informace o časovém pásmu a vytvoří relativní TIMESTAMP WITHOUT TIME ZONE
pomocí referenčního rámce vašeho cíle (UTC
).
Při převodu z neúplného TIMESTAMP WITHOUT TIME ZONE
na TIMESTAMP WITH TIME ZONE
, chybějící časové pásmo je zděděno z vašeho připojení:
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)
Závěr:
- uložit časové pásmo uživatele jako pojmenovaný štítek (např.
America/Los_Angeles
) a nikoli odchylka od UTC (např.-0700
) - použijte UTC pro vše, pokud neexistuje pádný důvod pro uložení nenulového offsetu
- všechny nenulové časy UTC považovat za chybu vstupu
- nikdy nekombinujte relativní a absolutní časová razítka
- používejte také
UTC
jakotimezone
v databázi, pokud je to možné
Poznámka k náhodnému programovacímu jazyku:datetime
Pythonu datový typ je velmi dobrý v udržování rozdílu mezi absolutními a relativními časy (i když zpočátku frustrující, dokud jej nedoplníte knihovnou, jako je PyTZ).
UPRAVIT
Dovolte mi vysvětlit rozdíl mezi relativním a absolutním trochu více.
Pro záznam události se používá absolutní čas. Příklady:„Uživatel 123 přihlášen“ nebo „slavnostní promoce začínají 28. 5. 2011 14:00 PST.“ Bez ohledu na vaše místní časové pásmo, pokud byste se mohli teleportovat do místa, kde k události došlo, mohli byste být svědky, jak se událost odehrává. Většina časových údajů v databázi je absolutní (a proto by měla být TIMESTAMP WITH TIME ZONE
, ideálně s posunem +0 a textovým štítkem představujícím pravidla pro konkrétní časové pásmo – nikoli posun).
Relativní událostí by bylo zaznamenat nebo naplánovat čas něčeho z perspektivy dosud neurčeného časového pásma. Příklady:„dveře naší firmy se otevírají v 8:00 a zavírají ve 21:00“, „sejdeme se každé pondělí v 7:00 na týdenní snídani“ nebo „každý Halloween ve 20:00“. Obecně platí, že relativní čas se používá v šabloně nebo továrně pro události a absolutní čas se používá téměř pro všechno ostatní. Existuje jedna vzácná výjimka, která stojí za zmínku a která by měla ilustrovat hodnotu relativních časů. Pro budoucí události, které jsou dostatečně daleko v budoucnosti, kde by mohla existovat nejistota ohledně absolutního času, kdy by k něčemu mohlo dojít, použijte relativní časové razítko. Zde je příklad ze skutečného světa:
Předpokládejme, že je rok 2004 a potřebujete naplánovat dodávku na 31. října 2008 ve 13 hodin na západním pobřeží USA (tj. America/Los_Angeles
/PST8PDT
). Pokud jste to uložili pomocí absolutního času pomocí ’2008-10-31 21:00:00.000000+00’::TIMESTAMP WITH TIME ZONE
, dodávka by se objevila ve 14 hodin, protože vláda USA schválila zákon o energetické politice z roku 2005, který změnil pravidla upravující letní čas. V roce 2004, kdy byla dodávka naplánována, datum 10-31-2008
by byl tichomořský standardní čas (+8000
), ale počínaje rokem 2005+ databáze časových pásem uznaly, že 10-31-2008
by byl tichomořský letní čas (+0700
). Uložení relativního časového razítka s časovým pásmem by vedlo ke správnému harmonogramu doručení, protože relativní časové razítko je imunní vůči špatně informované manipulaci Kongresu. Tam, kde je hranice mezi používáním relativních a absolutních časů pro plánování věcí, je fuzzy čára, ale moje pravidlo je, že plánování pro cokoli v budoucnosti delší než 3–6 měsíců by mělo využívat relativní časová razítka (plánované =absolutní vs plánované =relativní ???).
Druhý/poslední typ relativního času je INTERVAL
. Příklad:"relace vyprší 20 minut po přihlášení uživatele". INTERVAL
lze správně použít buď s absolutními časovými razítky (TIMESTAMP WITH TIME ZONE
) nebo relativní časová razítka (TIMESTAMP WITHOUT TIME ZONE
). Stejně tak je správné říci:„uživatelská relace vyprší 20 minut po úspěšném přihlášení (login_utc + session_duration)“ nebo „naše ranní schůzka při snídani může trvat pouze 60 minut (recurring_start_time + meeting_length)“.
Poslední zmatky:DATE
, TIME
, TIME WITHOUT TIME ZONE
a TIME WITH TIME ZONE
jsou všechny relativní datové typy. Například:'2011-05-28'::DATE
představuje relativní datum, protože nemáte žádné informace o časovém pásmu, které by bylo možné použít k identifikaci půlnoci. Podobně '23:23:59'::TIME
je relativní, protože neznáte ani časové pásmo, ani DATE
reprezentovaný dobou. I s '23:59:59-07'::TIME WITH TIME ZONE
, nevíte, co je DATE
bylo by. A nakonec DATE
s časovou zónou není ve skutečnosti DATE
, je 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)
Vkládání dat a časových pásem do databází je dobrá věc, ale je snadné získat mírně nesprávné výsledky. Správné a úplné uložení časových informací vyžaduje minimální dodatečné úsilí, ale to neznamená, že je vždy nutné vynaložit další úsilí.