Quale tipo di timestamp dovrei scegliere in un database PostgreSQL?


119

Vorrei definire una best practice per memorizzare i timestamp nel mio database Postgres nel contesto di un progetto multi-timezone.

io posso

  1. scegli TIMESTAMP WITHOUT TIME ZONEe ricorda quale fuso orario è stato utilizzato al momento dell'inserimento per questo campo
  2. scegli TIMESTAMP WITHOUT TIME ZONEe aggiungi un altro campo che conterrà il nome del fuso orario utilizzato al momento dell'inserimento
  3. scegli TIMESTAMP WITH TIME ZONEe inserisci i timestamp di conseguenza

Ho una leggera preferenza per l'opzione 3 (timestamp con fuso orario) ma vorrei avere un'opinione istruita in merito.

Risposte:


142

Prima di tutto, la gestione del tempo e l'aritmetica di PostgreSQL è fantastica e l'opzione 3 va bene nel caso generale. Tuttavia, è una visione incompleta dell'ora e dei fusi orari e può essere completata:

  1. Memorizza il nome del fuso orario di un utente come preferenza dell'utente (ad es America/Los_Angeles. No -0700).
  2. Fai in modo che i dati sugli eventi / orari degli utenti vengano inviati localmente al loro frame di riferimento (molto probabilmente un offset dall'UTC, come -0700).
  3. Nell'applicazione, convertire l'ora in UTCe memorizzata utilizzando una TIMESTAMP WITH TIME ZONEcolonna.
  4. L'ora di ritorno richiede locale al fuso orario di un utente (ad esempio, conversione da UTC a America/Los_Angeles).
  5. Imposta il tuo database timezonesu UTC.

Questa opzione non funziona sempre perché può essere difficile ottenere il fuso orario di un utente e quindi il consiglio di copertura da utilizzare TIMESTAMP WITH TIME ZONE per applicazioni leggere. Detto questo, lasciatemi spiegare alcuni aspetti di fondo di questa opzione 4 in modo più dettagliato.

Come l'opzione 3, il motivo WITH TIME ZONEè perché il momento in cui è successo qualcosa è un momento assoluto nel tempo. WITHOUT TIME ZONEproduce 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 effettuati utilizzando UTC come fuso orario. Questo non è un requisito di PostgreSQL, ma aiuta quando si integra con altri linguaggi o ambienti di programmazione. Impostazione di unCHECK sulla colonna per assicurarsi che la scrittura nella colonna timestamp abbia uno scostamento del fuso orario 0è una posizione difensiva che previene alcune classi di bug (ad esempio uno script scarica i dati in un file e qualcos'altro ordina i dati dell'ora usando un ordinamento lessicale). Ancora una volta, PostgreSQL non ha bisogno di questo per eseguire correttamente i calcoli della data o per convertire tra fusi orari (ad esempio PostgreSQL è molto abile nel convertire gli orari tra due fusi orari arbitrari). Per garantire che i dati che entrano nel database siano 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-colpo abbastanza forte che assicura che i dati siano già convertiti in UTC. Ci sono molte opinioni su come farlo, ma questa sembra essere la migliore nella pratica dalla mia esperienza.

Le critiche alla gestione del fuso orario del database sono ampiamente giustificate (ci sono molti database che gestiscono questo con grande incompetenza), tuttavia la gestione dei timestamp e dei fusi orari di PostgreSQL è piuttosto impressionante (nonostante alcune "caratteristiche" qua e là). Ad esempio, una di queste caratteristiche:

-- 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)

Tieni presente che AT TIME ZONE 'UTC'elimina le informazioni sul fuso orario e crea un parente TIMESTAMP WITHOUT TIME ZONEutilizzando il quadro di riferimento del target (UTC ).

Quando si converte da incompleto TIMESTAMP WITHOUT TIME ZONEa a TIMESTAMP WITH TIME ZONE, il fuso orario mancante viene ereditato dalla 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:

  • memorizzare il fuso orario di un utente come etichetta con nome (ad es America/Los_Angeles ) e non come un offset dall'UTC (ad es-0700 )
  • usa UTC per tutto a meno che non ci sia un motivo valido per memorizzare uno scostamento diverso da zero
  • considera tutti gli orari UTC diversi da zero come un errore di input
  • non mischiare e abbinare timestamp relativi e assoluti
  • utilizzare anche UTCcome timezonenel database, se possibile

Nota sul linguaggio di programmazione casuale: il datetimetipo di dati di Python è molto bravo a mantenere la distinzione tra tempi assoluti e relativi (anche se all'inizio frustrante finché non lo si integra con una libreria come PyTZ ).


MODIFICARE

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 "una cerimonia di consegna dei diplomi inizia alle 2pm PST 2011-05-28". Indipendentemente dal fuso orario locale, se potessi teletrasportarti nel luogo in cui si è verificato l'evento, potresti assistere all'evento. La maggior parte dei dati temporali in un database è assoluta (e quindi dovrebbe essereTIMESTAMP WITH TIME ZONE , idealmente con un offset +0 e un'etichetta testuale che rappresenta le regole che governano il particolare fuso orario - non un offset).

Un evento relativo sarebbe registrare o programmare l'ora di qualcosa dal punto di vista di un fuso orario ancora da determinare. Esempi: "le porte della nostra attività aprono alle 8:00 e chiudono alle 21:00", "incontriamoci ogni lunedì alle 7:00 per un incontro settimanale per la colazione" o "ogni Halloween alle 20:00". In generale, il tempo relativo viene utilizzato in un modello o 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 eventi futuri sufficientemente lontani nel futuro in cui potrebbe esserci incertezza sul momento assoluto in cui potrebbe verificarsi qualcosa, utilizzare un timestamp relativo. Ecco un esempio del mondo reale:

Supponiamo che sia l'anno 2004 e che sia necessario programmare una consegna il 31 ottobre 2008 alle 13:00 sulla costa occidentale degli Stati Uniti (ovvero America/Los_Angeles/ PST8PDT). Se lo hai memorizzato utilizzando l'ora assoluta ’2008-10-31 21:00:00.000000+00’::TIMESTAMP WITH TIME ZONE, la consegna sarebbe arrivata 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-2008sarebbe stata Pacific Standard Time (+8000 solare del ), ma a partire dall'anno 2005+ i database del fuso orario hanno riconosciuto che 10-31-2008sarebbe stata l'ora legale del Pacifico (+0700). La memorizzazione di un timestamp relativo con il fuso orario avrebbe portato a un programma di consegna corretto perché un timestamp relativo è immune alle manomissioni mal informate del Congresso. Dove il limite tra l'utilizzo dei tempi relativi e assoluti per la pianificazione è, è una linea sfocata, ma la mia regola pratica è che la pianificazione per qualsiasi cosa in futuro oltre 3-6 mesi dovrebbe fare uso di timestamp relativi (programmato = assoluto vs pianificato = parente ???).

L'altro / ultimo tipo di tempo relativo è il INTERVAL. Esempio: "la sessione scadrà 20 minuti dopo che un utente ha effettuato l'accesso". Un INTERVALpuò essere utilizzato correttamente con timestamp assoluti ( TIMESTAMP WITH TIME ZONE) o relativi (TIMESTAMP WITHOUT TIME ZONE ). È altrettanto corretto dire "una sessione utente scade 20 minuti dopo un accesso riuscito (login_utc + session_duration)" o "la nostra colazione mattutina può durare solo 60 minuti (recurring_start_time + meeting_length)".

Ultimi pezzi di confusione: DATE, TIME, TIME WITHOUT TIME ZONEe TIME WITH TIME ZONEsono tutti i tipi di dati relativi. Ad esempio: '2011-05-28'::DATErappresenta una data relativa poiché non si dispone di 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é quello DATErappresentato dall'ora. Anche con '23:59:59-07'::TIME WITH TIME ZONE, non sai cosa DATEsarebbe. Infine, DATEcon un fuso orario in realtà non è a 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)

Mettere date e fusi orari nei database è una buona cosa, ma è facile ottenere risultati leggermente errati. È richiesto uno sforzo aggiuntivo minimo per memorizzare le informazioni sul tempo in modo corretto e completo, tuttavia ciò non significa che sia sempre richiesto uno sforzo aggiuntivo.


2
Se indichi accuratamente a postgresql il fuso orario corretto in cui si trova il timestamp dell'utente, postgresql farà il lavoro pesante dietro le quinte. Convertirlo da soli è solo prendere in prestito guai.
Seth Robertson

1
@Sean - con il tuo vincolo di controllo, come fai a inserire un timestamp senza set timezone to 'UTC'? Sai che tutte le date che riconoscono il fuso orario sono archiviate internamente in UTC ?

2
Lo scopo del controllo è assicurarsi che i dati vengano memorizzati con offset zero rispetto a UTC. L'ordinamento e il recupero delle informazioni e il confronto dei tempi con offset diversi da zero sono soggetti a errori. Applicando uno scostamento UTC pari a zero, è possibile interagire in modo coerente con i dati da un'unica prospettiva in un modo quasi a rischio zero che si comporta in modo prevedibile in tutti gli scenari. Se fosse pratico per i timestamp supportare le rappresentazioni testuali dei fusi orari, i miei pensieri sull'argomento sarebbero diversi. : ~]
Sean

6
@Sean: Ma, come indica Jack, tutti i timestamp che riconoscono il fuso orario sono fondamentalmente memorizzati internamente in UTC e vengono convertiti nel fuso orario locale quando vengono utilizzati; in effetti, extract (timezone from ...) restituirà sempre qualunque sia il fuso orario locale della connessione: non ha alcuna relazione con il modo in cui il timestamp è stato "memorizzato". In altre parole, il fuso orario non fa affatto parte del tipo e non può essere memorizzato: "con fuso orario" è solo una proprietà di come i dati verranno convertiti quando interagiscono con altri tipi. I dati quindi non hanno alcuna rappresentazione dei fusi orari, testuali o altro.
Jay Freeman -saurik-

@ JayFreeman-saurik-: hai assolutamente ragione. Il '' CHECK () '' è lì come misura anti-tiro a piedi per proteggersi da codice potenzialmente pericoloso. Garantire che i dati siano UTC in scrittura fornisce una modesta garanzia che il codice sia stato pensato o che l'ambiente di esecuzione sia impostato correttamente.
Sean

59

La risposta di Sean è eccessivamente complessa e fuorviante.

Il fatto è che sia "WITH TIME ZONE" che "WITHOUT TIME ZONE" memorizzano il valore come un timestamp UTC assoluto di tipo unix. La differenza è tutta nel modo in cui viene visualizzato il timestamp. Quando "CON fuso orario", il valore visualizzato è il valore UTC memorizzato tradotto nella zona dell'utente. Quando "SENZA fuso orario" il valore UTC memorizzato viene ruotato in modo da mostrare lo stesso quadrante dell'orologio indipendentemente dalla zona impostata dall'utente ".

L'unica situazione in cui è utilizzabile un "SENZA fuso orario" è quando il valore del quadrante di un orologio è applicabile indipendentemente dalla zona effettiva. Ad esempio, quando un timestamp indica quando le cabine di votazione potrebbero chiudere (cioè chiudono alle 20:00 indipendentemente dal fuso orario di una persona).

Usa la scelta 3. Usa sempre "CON fuso orario" a meno che non ci sia un motivo molto specifico per non farlo.


10
David E. Wheeler, uno dei maggiori esperti di Postgres, concorderebbe con la tua valutazione in base al suo messaggio, Usa sempre TIMESTAMP CON FUSO ORARIO .
Basil Bourque

2
E se il browser convertisse il timestamp UTC nel fuso orario locale? Quindi, il db non eseguirà mai la conversione e conterrà solo UTC. Sarebbe accettabile "SENZA fuso orario"?
dman

5

La mia preferenza è verso l'opzione 3, poiché Postgres può quindi fare gran parte del lavoro ricalcolando i timestamp relativi al fuso orario per te, mentre con gli altri due dovrai farlo tu stesso. Il sovraccarico di archiviazione aggiuntivo per l'archiviazione del timestamp con un fuso orario è davvero trascurabile a meno che non si parli di milioni di record, nel qual caso probabilmente si hanno già requisiti di archiviazione piuttosto sostanziosi.


19
Non corretto. Non ci sono costi generali ... Postgres non memorizza il fuso orario ("offset" è il termine corretto, non fuso orario, a proposito). Il TIMESTAMP WITH TIME ZONEnome è fuorviante. Significa davvero "prestare attenzione a qualsiasi offset specificato durante l'inserimento / aggiornamento e utilizzare tale offset per regolare la data e l'ora in UTC". Il TIMESTAMP WITHOUT TIME ZONEnome significa "ignora qualsiasi offset che potrebbe essere presente durante l'inserimento / aggiornamento, considera le porzioni di data e ora come in UTC senza bisogno di aggiustamenti". Leggi attentamente il documento .
Basil Bourque

1
@BasilBourque grazie per questa informazione. Incredibilmente utile. Per gli altri che leggono questo, la riga del documento dice: "In un letterale che è stato determinato come timestamp senza fuso orario, PostgreSQL ignorerà silenziosamente qualsiasi indicazione di fuso orario. Cioè, il valore risultante è derivato dai campi data / ora in il valore di input e non è regolato per il fuso orario. "
Aidan Rosswood
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.