Prima di tutto, la gestione del tempo e l'aritmetica di PostgreSQL sono fantastiche e l'opzione 3 va bene nel caso generale. È, tuttavia, una visione incompleta dell'ora e dei fusi orari e può essere integrata:
- Memorizza il nome del fuso orario di un utente come preferenza utente (ad es.
America/Los_Angeles
, non-0700
). - Fai inviare i dati sugli eventi/ora dell'utente in locale al loro quadro di riferimento (molto probabilmente un offset dall'UTC, come
-0700
). - Nell'applicazione, converti l'ora in
UTC
e memorizzati utilizzando unTIMESTAMP WITH TIME ZONE
colonna. - Richiesta orario di ritorno locale rispetto al fuso orario di un utente (ovvero convertire da
UTC
inAmerica/Los_Angeles
). - Imposta il
timezone
del tuo database aUTC
.
Questa opzione non funziona sempre perché può essere difficile ottenere il fuso orario di un utente e quindi il consiglio di copertura di utilizzare TIMESTAMP WITH TIME ZONE
per applicazioni leggere. Detto questo, permettetemi di spiegare in modo più dettagliato alcuni aspetti di fondo di questa opzione 4.
Come l'opzione 3, il motivo del WITH TIME ZONE
è perché il momento in cui è successo qualcosa è un assoluto momento nel tempo. WITHOUT TIME ZONE
restituisce un parente fuso orario. Non mischiare mai, mai, mai TIMESTAMP assoluti e relativi.
Da una prospettiva programmatica e di coerenza, assicurati che tutti i calcoli vengano eseguiti utilizzando UTC come fuso orario. Questo non è un requisito PostgreSQL, ma aiuta quando si integra con altri linguaggi o ambienti di programmazione. Impostazione di un CHECK
sulla colonna per assicurarsi che la scrittura nella colonna timestamp abbia un offset di fuso orario di 0
è una posizione difensiva che impedisce alcune classi di bug (ad esempio uno script scarica i dati in un file e qualcos'altro ordina i dati temporali usando un ordinamento lessicale). Ancora una volta, PostgreSQL non ne ha bisogno per eseguire correttamente i calcoli della data o per convertire tra fusi orari (ad es. PostgreSQL è molto abile nel convertire gli orari tra due fusi orari arbitrari). Per garantire che i dati che entrano nel database vengano archiviati con un offset pari a 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
Non è perfetto al 100%, ma fornisce una misura anti-footshooting sufficientemente forte che assicura che i dati siano già convertiti in UTC. Ci sono molte opinioni su come farlo, ma questo sembra essere il migliore in pratica dalla mia esperienza.
Le critiche alla gestione del fuso orario del database sono ampiamente giustificate (ci sono molti database che lo gestiscono con grande incompetenza), tuttavia la gestione di timestamp e fusi orari da parte di PostgreSQL è piuttosto impressionante (nonostante alcune "caratteristiche" qua e là). Ad esempio, una di queste funzionalità:
-- 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)
Nota che AT TIME ZONE 'UTC'
elimina le informazioni sul fuso orario e crea un relativo TIMESTAMP WITHOUT TIME ZONE
utilizzando il quadro di riferimento del tuo target (UTC
).
Quando si esegue la conversione da un TIMESTAMP WITHOUT TIME ZONE
incompleto a un TIMESTAMP WITH TIME ZONE
, il fuso orario mancante viene ereditato dalla tua connessione:
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)
La linea di fondo:
- Memorizza il fuso orario di un utente come etichetta denominata (ad es.
America/Los_Angeles
) e non un offset da UTC (ad es.-0700
) - usa UTC per tutto, a meno che non vi sia un motivo convincente per memorizzare un offset diverso da zero
- tratta tutti gli orari UTC diversi da zero come un errore di input
- non combinare mai timestamp relativi e assoluti
- usa anche
UTC
cometimezone
nel database, se possibile
Nota sul linguaggio di programmazione casuale:datetime
di Python il tipo di dati è molto bravo a mantenere la distinzione tra tempi assoluti e relativi (sebbene inizialmente frustrante fino a quando non lo si integra con una libreria come PyTZ).
MODIFICA
Lascia che ti spieghi un po' di più la differenza tra relativo e assoluto.
Il tempo assoluto viene utilizzato per registrare un evento. Esempi:"L'utente 123 ha effettuato l'accesso" o "le cerimonie di laurea iniziano alle 14:00 PST del 28-05-2011". Indipendentemente dal tuo fuso orario locale, se potessi teletrasportarti dove si è verificato l'evento, potresti assistere all'evento. La maggior parte dei dati temporali in un database sono assoluti (e quindi dovrebbero essere TIMESTAMP WITH TIME ZONE
, idealmente con un offset +0 e un'etichetta testuale che rappresenta le regole che regolano il fuso orario particolare, non un offset).
Un evento relativo sarebbe quello di registrare o programmare l'ora di qualcosa dalla prospettiva di un fuso orario ancora da determinare. Esempi:"le porte della nostra attività si aprono alle 8:00 e chiudono alle 21:00", "incontriamoci ogni lunedì alle 7:00 per una colazione settimanale" o "ogni Halloween alle 20:00". In generale, il tempo relativo viene utilizzato in un modello o una fabbrica per gli eventi e il tempo assoluto viene utilizzato per quasi tutto il resto. C'è una rara eccezione che vale la pena sottolineare che dovrebbe illustrare il valore dei tempi relativi. Per gli eventi futuri che sono abbastanza lontani nel futuro in cui potrebbe esserci incertezza sul momento assoluto in cui qualcosa potrebbe verificarsi, utilizzare un timestamp relativo. Ecco un esempio del mondo reale:
Supponiamo che sia l'anno 2004 e devi programmare una consegna il 31 ottobre 2008 alle 13:00 sulla costa occidentale degli Stati Uniti (ad esempio America/Los_Angeles
/PST8PDT
). Se l'hai memorizzato utilizzando l'ora assoluta utilizzando ’2008-10-31 21:00:00.000000+00’::TIMESTAMP WITH TIME ZONE
, la consegna sarebbe avvenuta alle 14:00 perché il governo degli Stati Uniti ha approvato l'Energy Policy Act del 2005 che ha modificato le regole che regolano l'ora legale. Nel 2004 quando era prevista la consegna, la data 10-31-2008
sarebbe stato l'ora solare del Pacifico (+8000
), ma a partire dall'anno 2005+ i database dei fusi orari hanno riconosciuto che 10-31-2008
sarebbe stata l'ora legale del Pacifico (+0700
). La memorizzazione di un timestamp relativo con il fuso orario avrebbe comportato un programma di consegna corretto perché un timestamp relativo è immune alla manomissione mal informata del Congresso. Laddove il limite tra l'utilizzo di tempi relativi e assoluti per la pianificazione delle cose è una linea sfocata, ma la mia regola pratica è che la pianificazione per qualsiasi cosa in futuro oltre 3-6 mesi dovrebbe utilizzare timestamp relativi (programmato =assoluto vs pianificato =parente???).
L'altro/ultimo tipo di tempo relativo è il INTERVAL
. Esempio:"la sessione scadrà 20 minuti dopo l'accesso di un utente". Un INTERVAL
può essere utilizzato correttamente con entrambi i timestamp assoluti (TIMESTAMP WITH TIME ZONE
) o relativi timestamp (TIMESTAMP WITHOUT TIME ZONE
). È altrettanto corretto dire "una sessione utente scade 20 minuti dopo un accesso riuscito (login_utc + session_duration)" o "la nostra riunione mattutina per la colazione può durare solo 60 minuti (recurring_start_time + meeting_length)".
Ultimi frammenti di confusione:DATE
, TIME
, TIME WITHOUT TIME ZONE
e TIME WITH TIME ZONE
sono tutti tipi di dati relativi. Ad esempio:'2011-05-28'::DATE
rappresenta una data relativa poiché non hai informazioni sul fuso orario che potrebbero essere utilizzate per identificare la mezzanotte. Allo stesso modo, '23:23:59'::TIME
è relativo perché non conosci né il fuso orario né la DATE
rappresentato dal tempo. Anche con '23:59:59-07'::TIME WITH TIME ZONE
, non sai qual è la DATE
sarebbe. E infine, DATE
con un fuso orario non è infatti un DATE
, è un 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)
Inserire date e fusi orari nei database è una buona cosa, ma è facile ottenere risultati leggermente errati. È richiesto uno sforzo aggiuntivo minimo per archiviare le informazioni sull'ora in modo corretto e completo, tuttavia ciò non significa che sia sempre necessario uno sforzo aggiuntivo.