För det första är PostgreSQL:s tidshantering och aritmetik fantastisk och alternativ 3 är bra i det allmänna fallet. Det är dock en ofullständig bild av tid och tidszoner och kan kompletteras:
- Lagra namnet på en användares tidszon som en användarpreferens (t.ex.
America/Los_Angeles
, inte-0700
). - Låt data från användarhändelser/tid skickas lokalt till deras referensram (mest troligt en förskjutning från UTC, t.ex.
-0700
). - Konvertera tiden till
UTC
i programmet och lagras med enTIMESTAMP WITH TIME ZONE
kolumn. - Returtidsförfrågningar lokalt för en användares tidszon (dvs. konvertera från
UTC
tillAmerica/Los_Angeles
). - Ställ in databasens
timezone
tillUTC
.
Det här alternativet fungerar inte alltid eftersom det kan vara svårt att få en användares tidszon och därför råden att använda TIMESTAMP WITH TIME ZONE
för lätta applikationer. Som sagt, låt mig förklara några bakgrundsaspekter av detta alternativ 4 mer i detalj.
Liksom alternativ 3, orsaken till WITH TIME ZONE
beror på att tidpunkten då något hände är absolut ögonblick i tiden. WITHOUT TIME ZONE
ger en släkting tidszon. Blanda aldrig, aldrig, aldrig absoluta och relativa TIDSTÄMPLAR.
Ur ett programmatiskt och konsistent perspektiv, se till att alla beräkningar görs med UTC som tidszon. Detta är inte ett PostgreSQL-krav, men det hjälper när man integrerar med andra programmeringsspråk eller miljöer. Ställa in en CHECK
på kolumnen för att se till att skrivningen till tidsstämpelkolumnen har en tidszonförskjutning på 0
är en defensiv position som förhindrar ett fåtal klasser av buggar (t.ex. ett skript dumpar data till en fil och något annat sorterar tidsdata med hjälp av en lexikal sortering). Återigen, PostgreSQL behöver inte detta för att göra datumberäkningar korrekt eller för att konvertera mellan tidszoner (dvs. PostgreSQL är mycket skicklig på att konvertera tider mellan två godtyckliga tidszoner). För att säkerställa att data som går in i databasen lagras med en offset på noll:
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
Det är inte 100% perfekt, men det ger en tillräckligt stark anti-fotskjutningsåtgärd som ser till att data redan konverterats till UTC. Det finns många åsikter om hur man gör detta, men det här verkar vara det bästa i praktiken enligt min erfarenhet.
Kritik av databastidszonshantering är till stor del berättigad (det finns gott om databaser som hanterar detta med stor inkompetens), men PostgreSQL:s hantering av tidsstämplar och tidszoner är ganska fantastisk (trots några "funktioner" här och där). Till exempel en sådan funktion:
-- 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)
Observera att AT TIME ZONE 'UTC'
tar bort tidszonsinformation och skapar en relativ TIMESTAMP WITHOUT TIME ZONE
med hjälp av ditt måls referensram (UTC
).
Vid konvertering från en ofullständig TIMESTAMP WITHOUT TIME ZONE
till en TIMESTAMP WITH TIME ZONE
, den saknade tidszonen ärvs från din anslutning:
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)
Summan av kardemumman:
- lagra en användares tidszon som en namngiven etikett (t.ex.
America/Los_Angeles
) och inte en offset från UTC (t.ex.-0700
) - använd UTC för allt om det inte finns en övertygande anledning att lagra en offset som inte är noll
- behandla alla UTC-tider som inte är noll som ett inmatningsfel
- mixa och matcha aldrig relativa och absoluta tidsstämplar
- använd även
UTC
somtimezone
i databasen om möjligt
Anmärkning om slumpmässigt programmeringsspråk:Pythons datetime
datatypen är mycket bra på att upprätthålla skillnaden mellan absoluta och relativa tider (även om det är frustrerande i början tills du kompletterar det med ett bibliotek som PyTZ).
REDIGERA
Låt mig förklara skillnaden mellan relativ vs absolut lite mer.
Absolut tid används för att spela in en händelse. Exempel:"Användare 123 inloggad" eller "en examensceremoni startar 2011-05-28 14:00 PST." Oavsett din lokala tidszon, om du kunde teleportera till där händelsen inträffade, kan du se händelsen hända. De flesta tidsdata i en databas är absoluta (och bör därför vara TIMESTAMP WITH TIME ZONE
, helst med en +0 offset och en textetikett som representerar reglerna som styr den specifika tidszonen - inte en offset).
En relativ händelse skulle vara att spela in eller schemalägga tiden för något ur perspektivet av en ännu inte fastställd tidszon. Exempel:"vårt företags dörrar öppnar kl. 08.00 och stänger kl. 21.00", "låt oss träffas varje måndag kl. 07.00 för ett frukostmöte varje vecka" eller "varje Halloween kl. 20.00." Generellt sett används relativ tid i en mall eller fabrik för evenemang, och absolut tid används för nästan allt annat. Det finns ett sällsynt undantag som är värt att påpeka som borde illustrera värdet av relativa tider. För framtida händelser som ligger tillräckligt långt fram i tiden där det kan finnas osäkerhet om den absoluta tidpunkten då något kan inträffa, använd en relativ tidsstämpel. Här är ett exempel från verkligheten:
Anta att det är år 2004 och du måste schemalägga en leverans den 31 oktober 2008 kl. 13.00 på USA:s västkust (dvs. America/Los_Angeles
/PST8PDT
). Om du sparade det med absolut tid med ’2008-10-31 21:00:00.000000+00’::TIMESTAMP WITH TIME ZONE
, skulle leveransen ha dykt upp klockan 14.00 eftersom den amerikanska regeringen antog Energy Policy Act från 2005 som ändrade reglerna för sommartid. 2004 när leveransen var planerad, datumet 10-31-2008
skulle ha varit Pacific Standard Time (+8000
), men från och med år 2005+ upptäckte tidszonsdatabaser att 10-31-2008
skulle ha varit Pacific Daylight Savings-tid (+0700
). Att lagra en relativ tidsstämpel med tidszonen skulle ha resulterat i ett korrekt leveransschema eftersom en relativ tidsstämpel är immun mot kongressens dåligt informerade manipulation. Var gränsen mellan att använda relativa vs absoluta tider för att schemalägga saker är en luddig linje, men min tumregel är att schemaläggning för allt i framtiden längre än 3-6 månader bör använda relativa tidsstämplar (schemalagt =absolut vs planerat =släkting ???).
Den andra/sista typen av relativ tid är INTERVAL
. Exempel:"sessionen kommer att avbrytas 20 minuter efter att en användare loggat in". En INTERVAL
kan användas korrekt med antingen absoluta tidsstämplar (TIMESTAMP WITH TIME ZONE
) eller relativa tidsstämplar (TIMESTAMP WITHOUT TIME ZONE
). Det är lika korrekt att säga, "en användarsession går ut 20 minuter efter en lyckad inloggning (login_utc + session_duration)" eller "vårt morgonfrukostmöte kan bara vara i 60 minuter (återkommande_starttid + möteslängd)".
Sista bitarna av förvirring:DATE
, TIME
, TIME WITHOUT TIME ZONE
och TIME WITH TIME ZONE
är alla relativa datatyper. Till exempel:'2011-05-28'::DATE
representerar ett relativt datum eftersom du inte har någon tidszonsinformation som kan användas för att identifiera midnatt. På samma sätt, '23:23:59'::TIME
är relativt eftersom du inte vet vare sig tidszonen eller DATE
representerad av tiden. Även med '23:59:59-07'::TIME WITH TIME ZONE
, du vet inte vad DATE
är skulle vara. Och slutligen, DATE
med en tidszon är i själva verket inte ett DATE
, det är en 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)
Det är bra att lägga in datum och tidszoner i databaser, men det är lätt att få subtilt felaktiga resultat. Minimal extra ansträngning krävs för att lagra tidsinformation korrekt och fullständigt, men det betyder inte att den extra ansträngningen alltid krävs.