DanLevy.net

Il tuo timestamp è una menzogna

Cosa mi ha insegnato un biglietto ferroviario sulla gestione del tempo nei database

Stavo prenotando un treno da New York a Chicago quando mi è chiaro perché i tipi di timestamp in Postgres sono così confondenti. Il biglietto mostrava:

Tre modi diversi di esprimere l’ora, tutti sullo stesso biglietto. E ciascuna di queste deve essere memorizzata in modo diverso in un database.

La domanda che nessuno si pone per prima

Sia TIMESTAMP che TIMESTAMPTZ in Postgres occupano esattamente 8 ottetti con la stessa precisione al microsecondo. Perché allora esistono due tipi?

Perché “che ora è?” dipende interamente da ciò che si intende comunicare a qualcuno.

Quando salirò su quel treno a New York, devo sapere che parte alle 8:00 AM Eastern. Quel è il numero sull’orologio della stazione che devo rispettare. Quando la mia amica mi va a prendere a Chicago, lei deve sapere che arrivo alle 7:30 PM Central – quel è il numero sull’orologio della sua stazione. E se sto cercando di capire se avrò tempo di leggere il mio libro, devo sapere che il viaggio dura 11 ore e mezza.

Stesso treno. Stessa gita. Tre rappresentazioni completamente diverse del tempo.

Cosa fa davvero TIMESTAMPTZ

Ecco la parte sottile con TIMESTAMPTZ – e non è ciò che la maggior parte delle persone pensa. Non memorizza il fuso orario. Il nome è fuorviante.

Quello che fa è convertire qualsiasi orario tu gli dia in UTC prima di memorizzarlo, quindi lo converte nuovamente nel tuo fuso orario di sessione quando lo leggi. La parte “TZ” non riguarda la memorizzazione, ma il supporto alle conversioni.

Immagina di memorizzare quell’orario di partenza del treno. Qualcuno a Tokyo che consulta il tuo database vedrà l’orario in JST. Qualcuno a Londra lo vedrà in GMT. Tutti guardano lo stesso momento assoluto, espresso semplicemente nel loro fuso orario configurato. Questo è perfetto per registrare eventi: “quando è avvenuta questa transazione?” o “quando è avvenuta questa richiesta API?”

Ma che dire di quel biglietto per il treno? Non vorresti che l’orario di partenza cambiasse solo perché qualcuno lo consulta da un fuso orario diverso. Il treno parte alle 8:00 AM Eastern, punto e basta. Questo non è un momento assoluto nel tempo — è una promessa su cosa indicherà l’orologio a Grand Central.

Memorizzare ciò che si intende davvero

Per quel viaggio in treno, devi memorizzare cose diverse a seconda dello scopo:

Ora la tua applicazione può fare esattamente ciò che fa il biglietto del treno: mostrare “Partenza alle 8:00 EST” convertendo il momento assoluto nel fuso orario di origine, mostrare “Arrivo alle 19:30 CST” convertendo nel fuso orario di destinazione, e mostrare “Durata: 11h 30m” direttamente dall’intervallo.

La persona che prenota il biglietto da Tokyo vede gli stessi orari locali in ciascuna stazione. Questo è ciò che deve sapere.

Perché il tuo’app di tracciamento voli ha sbagliato

Hai mai notato come alcune app di tracciamento voli mostrino il tuo fuso orario durante il volo? Tipo quando sei sopra l’Atlantico e ti dice “Ora corrente: 4:32 PM GMT”. Chi se ne importa? Non sei a Greenwich, sei a 38.000 piedi sopra l’oceano.

Quello che vorresti davvero vedere:

Nessuna di queste richiede conversioni di fuso orario. Le prime due sono intervalli—durata, non momenti. L’ultima è una conversione di fuso orario, ma verso una destinazione specifica, non “il tuo fuso orario corrente.”

Hai capito? Due calcoli di intervallo (NOW() - actual_departure e estimated_arrival - NOW()), una conversione di fuso orario verso un luogo specifico (AT TIME ZONE destination_timezone). Il tuo fuso orario corrente non entra in gioco.

Quando l’orario sul muro è davvero ciò di cui hai bisogno

Gli alberghi non si curano di momenti assoluti nel tempo. Si curano delle letture dell’orologio al loro luogo.
”Check-in dopo le 15:00” non significa “check-in 15 ore dopo mezzanotte UTC”. Significa “quando l’orologio nella nostra hall dice 15:00, puoi effettuare il check-in”. Se i tuoi server sono in Virginia ma l’albergo è a Parigi, vorrai comunque che questa regola venga attivata alle 15:00 ora di Parigi.

Il tipo TIME (senza data né fuso orario) rappresenta esattamente questo: “una lettura di un orologio”. Accoppiatelo con un campo testuale per il fuso orario (“Europe/Paris”), e potrete applicare politiche basate sull’orologio fisico indipendentemente da dove vivano i vostri server. Ma vorrete anche colonne TIMESTAMPTZ per quando i clienti effettivamente effettuano il check-in e il check-out—questi sono momenti assoluti che il backend deve tracciare.

Il problema del calendario

Ho un promemoria ricorrente alle 9:00: “Rivedi le priorità quotidiane”. Voglio quel promemoria alle 9:00 dove mi trovo. Se sto viaggiando, dovrebbe comunque scattare alle 9:00 locali.

Ma ho anche un evento nel calendario: “Standup di squadra alle 10:00 EST”. Il mio collega a Berlino deve vedere “16:00 CET” per lo stesso evento. Stessa riunione, diversi orari visualizzati, perché questa è un momento assoluto al quale tutti ci uniamo.

Due tipi diversi di eventi, due strategie di archiviazione diverse. L’incontro riceve un TIMESTAMPTZ. Il promemoria riceve un TIME più il mio attuale fuso orario. Evita di forzare entrambi nello stesso campo.

Le cose che rompono in produzione

Anche con i tipi corretti, la precisione può farti traboccare. Postgres memorizza i microsecondi: 10:00:00.123456. L’oggetto Date di JavaScript usa i millisecondi: 10:00:00.123.

Quindi questa query potrebbe misteriosamente non restituire righe:

SELECT * FROM orders WHERE created_at = '2026-01-15 10:00:00.123';

Il database ha 10:00:00.123456 e il tuo codice passa 10:00:00.123. A seconda di come il tuo driver lo gestisce, quei valori potrebbero non corrispondere.

Non usare l’uguaglianza esatta sui timestamp. Usa query a intervalli, o - meglio - non cercare affatto record in base al loro timestamp di creazione. Usa un vincolo unico o una chiave di idempotenza appropriata.

Regole pratiche

Preferire TIMESTAMPTZ. Quando in dubbio, utilizza TIMESTAMPTZ. Gestisce automaticamente le distribuzioni multi-regione, l’ora legale e i futuri cambi di fuso orario. Occupa la stessa dimensione di archiviazione di TIMESTAMP, quindi non c’è alcuna penalità.

Archiviare il contesto separatamente. Se devi mostrare “Partenza alle 8:00 AM EST” insieme al momento effettivo, memorizza sia il TIMESTAMPTZ che il origin_timezone come colonne separate. Non cercare di codificare tutto in un unico campo.

Pensare agli intervalli. Molte richieste legate al tempo riguardano in realtà la durata, non i momenti. “Da quanto tempo è in sospeso?” “Quando scadrà?” Utilizza operazioni INTERVAL, non conversioni di fuso orario.

Eseguire tutto in UTC. I tuoi server dovrebbero essere configurati in UTC. Le sessioni del database dovrebbero predefinitivamente utilizzare UTC. Converte nel fuso orario locale solo quando mostri i dati agli utenti, e solo quando sai quale fuso orario è rilevante.

Richiedere informazioni sul fuso orario dagli utenti. Se un client invia 2026-01-15T10:00:00 senza offset, rifiutalo. Richiedi il formato ISO-8601 con Z oppure un offset esplicito come -05:00. Non indovinare.

Imporre buoni valori predefiniti

Se TIMESTAMPTZ è il tuo valore predefinito (e dovrebbe esserlo), considera di imporlo a livello di database. Un trigger che rifiuti le colonne TIMESTAMP WITHOUT TIME ZONE sembra estremo, ma catturare il problema “dimenticato di aggiungere TZ” al momento della creazione dello schema è meglio che doverne debuggare le conseguenze sei mesi dopo, quando qualcuno aggiunge una nuova tabella e dimentica di nuovo.

Cosa mi ha insegnato quel biglietto del treno

Il tempo nei database non è complicato perché i timestamp sono complessi. È complicato perché spesso stiamo memorizzando più preoccupazioni in un unico campo, o non stiamo considerando davvero cosa stiamo cercando di mostrare agli utenti.

Quel biglietto del treno aveva ragione: l’orario di partenza nel fuso orario di origine, l’orario di arrivo nel fuso orario di destinazione, e la durata come cosa completamente separata. Tre informazioni diverse, ciascuna significativa a modo suo.

Il tuo database può fare lo stesso. Memorizza i momenti assoluti come TIMESTAMPTZ. Memorizza il contesto di visualizzazione (fusi orari, località) in colonne separate. Usa i tipi INTERVAL per le durate. Lascia che Postgres faccia le conversioni quando le hai bisogno, ma sii esplicito riguardo a quale fuso orario è rilevante per quale scopo.

La maggior parte delle volte, ciò significa utilizzare TIMESTAMPTZ e UTC ovunque, con conversioni dei fusi orari solo al momento della visualizzazione. Ma quando hai bisogno di orari locali o di programmi ricorrenti, i tipi TIMESTAMP o TIME esistono proprio per questo motivo.

La chiave è conoscere quale domanda stai cercando di rispondere: “Quando è successo?” rispetto a “Che ora devo essere lì?” rispetto a “Quanto tempo ci vorrà?” Sono tutte domande diverse riguardanti il tempo, e spesso richiedono strategie di memorizzazione diverse.

Pensa a ciò che i tuoi utenti devono vedere. Poi memorizza i dati che ti permettono di mostrare esattamente ciò.

Risorse