PostgreSQL timestamp vs timestamptz: cosa viene davvero memorizzato sotto il cofano?
PostgreSQL gestisce sia timestamp sia timestamptz come un singolo intero a 64 bit: il numero di microsecondi a partire dal 1970-01-01 00:00:00 UTC. La distinzione emerge solo quando i dati vengono formattati per essere letti dagli umani.
Perché questo confonde così tante persone?
- Due colonne, una sola data… due risultati di query diversi
- La tua applicazione inserisce
2025-07-29 10:00, ma un altro team vede02:00 - Il frontend mostra una stringa ISO che non corrisponde al log del backend
Due barattoli di pesche: uno semplice, uno etichettato
| Tipo di dato | Nome ufficiale | Valore memorizzato | Cosa succede con SELECT |
|---|---|---|---|
timestamp | timestamp without time zone | conteggio grezzo dei microsecondi | Restituito invariato — Postgres non indovina mai un fuso orario |
timestamptz | timestamp with time zone | stesso conteggio dei microsecondi | Postgres applica l’impostazione TimeZone della sessione subito prima di inviare il testo |
Analogia
timestamp= un barattolo di pesche senza etichetta d’origine. Sai che è frutta, ma non dove è stata conservata.timestamptz= un barattolo con timbro orgoglioso “Made in UTC+8.” Chi lo apre può decidere se convertire la tabella nutrizionale.
Sotto il cofano: è solo un numero gigante
2000-01-01 00:00:00 UTC → 0
2000-01-01 00:00:01 UTC → 1 000 000
- Unità: microsecondi (un milionesimo di secondo)
- Intervallo: 4713 a.C. – 294276 d.C. — approvato da Indiana Jones
- L’archiviazione di
timestampetimestamptzè identica; cambia solo l’interpretazione
Una demo da 15 secondi
-- Il client ragiona in ora di Shanghai
SET TimeZone = 'Asia/Shanghai';
CREATE TABLE demo (
created_ts timestamp,
created_tz timestamptz
);
INSERT INTO demo VALUES ('2025-07-29 10:00', '2025-07-29 10:00');
| Query | Risultato | Perché |
|---|---|---|
SELECT created_ts FROM demo; | 2025-07-29 10:00:00 | Valore grezzo, nessun calcolo TZ |
SELECT created_tz FROM demo; | 2025-07-29 10:00:00+08 | Etichetta applicata in output |
SET TimeZone = 'UTC'; poi select | 2025-07-29 02:00:00+00 | Stesso istante, lente diversa |
Aritmetica dei timestamp e intervalli
Uno degli aspetti più pratici dei timestamp di PostgreSQL è l’aritmetica con gli intervalli. Poiché entrambi i tipi memorizzano conteggi di microsecondi, puoi sommare e sottrarre intervalli direttamente:
-- Aggiungi 3 ore e 30 minuti
SELECT '2025-07-29 10:00'::timestamptz + INTERVAL '3 hours 30 minutes';
-- → 2025-07-29 13:30:00+08
-- Trova la differenza tra due timestamp
SELECT '2025-07-30 09:00'::timestamptz - '2025-07-29 10:00'::timestamptz;
-- → 23:00:00 (un intervallo)
-- Estrai campi specifici
SELECT EXTRACT(EPOCH FROM '2025-07-29 10:00:00+08'::timestamptz);
-- → 1753768800 (timestamp Unix in secondi)
-- Tronca al limite del giorno (utile per aggregazioni giornaliere)
SELECT date_trunc('day', '2025-07-29 15:42:19+08'::timestamptz);
-- → 2025-07-29 00:00:00+08
La funzione EXTRACT(EPOCH FROM ...) torna molto utile quando devi passare timestamp a sistemi esterni che si aspettano secondi epoch Unix. Al contrario, puoi convertire un epoch in un timestamp:
SELECT to_timestamp(1753768800);
-- → 2025-07-29 10:00:00+08 (in sessione Asia/Shanghai)
Un dettaglio sottile ma importante: l’aritmetica con intervalli su timestamp (senza fuso orario) ignora completamente le transizioni dell’ora legale, mentre timestamptz le rispetta. Questo significa che aggiungere INTERVAL '1 day' a un valore timestamptz che attraversa un confine di ora legale restituirà correttamente la stessa ora di parete — non esattamente 24 ore dopo.
Indicizzazione e considerazioni sulle prestazioni
Sia timestamp sia timestamptz vengono memorizzati come interi a 8 byte, quindi non c’è differenza di prestazioni tra loro per archiviazione o indicizzazione. Gli indici B-tree funzionano in modo identico su entrambi i tipi perché il confronto sottostante è solo un confronto tra interi.
Tuttavia, ci sono alcune considerazioni pratiche:
- Query di intervallo:
WHERE created_at > '2025-07-01'funziona efficacemente con un indice su entrambi i tipi. Contimestamptz, PostgreSQL converte il letterale in UTC prima del confronto, quindi l’indice viene comunque usato. - Chiavi di partizione: quando si usa il partizionamento per intervalli su colonne timestamp,
timestamptzè generalmente più sicuro perché i confini di partizione sono inequivocabili (sempre UTC). Contimestamp, un confine come'2025-07-01 00:00'potrebbe significare cose diverse per sessioni diverse. - Indici funzionali: se interroghi spesso solo per data (ignorando l’ora), valuta un indice su
date_trunc('day', created_at)per accelerare le query di aggregazione giornaliera.
Insidie comuni e soluzioni rapide
1. Utenti diversi, orologi diversi
- Causa: i client usano impostazioni
TimeZonediverse contimestamptz - Soluzione: o mantieni tutto come
timestamp+ concorda un fuso unico, oppure imponiSET TimeZone = 'UTC'all’inizializzazione della connessione
Un pattern comune nel codice applicativo è impostare il fuso orario una sola volta all’inizializzazione del pool di connessioni:
-- Nel setup della tua connessione (es., config del pool pg)
SET timezone = 'UTC';
Questo garantisce che tutte le sessioni vedano la stessa rappresentazione UTC, e il livello applicativo gestisce la conversione all’ora locale per la visualizzazione.
2. Memorizzi “ora di parete” ma hai scelto il tipo sbagliato
- I calendari aziendali (orari di apertura, scadenze) dovrebbero usare
timestamp - I flussi cross-border (ordini, log) dovrebbero memorizzare UTC in
timestamptz
Il test è semplice: se la domanda è “in quale momento nel tempo è successo?” usa timestamptz. Se la domanda è “cosa segna l’orologio sul muro?” usa timestamp.
3. API che vanno alla deriva
- Trasmetti sempre
timestamptzcome stringhe ISO-8601 con l’offset (Zo+08:00) - Lascia che la UI formatti localmente
4. Confrontare timestamp di tipi diversi
Mescolare timestamp e timestamptz in confronti o join è una fonte comune di bug sottili:
-- Pericoloso: il cast implicito applica il fuso orario di sessione
SELECT * FROM orders o
JOIN schedules s ON o.created_tz = s.start_ts;
-- PostgreSQL fa il cast di s.start_ts a timestamptz usando il fuso di sessione
-- Sessioni diverse possono ottenere risultati di join diversi!
Soluzione: fai sempre il cast esplicito quando confronti tipi diversi, oppure standardizza su un solo tipo per dominio.
5. Insidie predefinite degli ORM
Molti ORM (Django, SQLAlchemy, ActiveRecord) usano per default timestamp senza fuso orario. Verifica i tuoi file di migrazione — se la tua app serve utenti su più fusi, sovrascrivi il default a timestamptz. In Django, imposta USE_TZ = True nelle settings. In SQLAlchemy, usa DateTime(timezone=True).
Cheat Sheet: quale dovrei usare?
Solo calendario locale → timestamp
Qualsiasi cosa globale → timestamptz (memorizza UTC)
- Report finanziari, orari delle lezioni →
timestamp - Log di audit, ordini e-commerce →
timestamptz
Verifica in pochi secondi con Go Tools
| Necessità | Strumento | Come |
|---|---|---|
| Ispezionare il valore epoch da SQL | Convertitore Epoch | Incolla 1690622400, premi Convert |
| Confrontare due fusi orari a colpo d’occhio | Convertitore Fuso Orario | Inserisci 10:00 Asia/Shanghai |
| Riordinare JSON di massa con campi temporali | JSON Formatter | Incolla il payload, abbelliscilo e analizzalo |
Tutte le utility girano interamente nel tuo browser — nessun dato lascia mai la tua macchina.
Domande frequenti
Qual è la differenza tra timestamp e timestamptz in PostgreSQL?
timestamp (senza fuso orario) memorizza un valore data-ora così com’è, senza alcun contesto di fuso orario. timestamptz (con fuso orario) converte l’input in UTC per l’archiviazione e lo riconverte nel fuso orario della sessione al recupero. Usa timestamptz per quasi tutti i casi — previene bug legati ai fusi orari nei sistemi distribuiti.
PostgreSQL memorizza davvero il fuso orario in timestamptz?
No — nonostante il nome, PostgreSQL non memorizza il fuso orario stesso. Converte l’input in UTC e memorizza solo il valore UTC (un conteggio di microsecondi dal 2000-01-01). Al recupero, converte da UTC a qualunque fuso orario specificato dall’impostazione timezone della sessione. L’informazione originale sul fuso orario viene scartata.
Come cambio il fuso orario per una sessione PostgreSQL?
Esegui SET timezone = 'America/New_York'; per cambiare il fuso orario di sessione. Questo influisce su come i valori timestamptz vengono visualizzati e interpretati. Per i default a livello di server, imposta timezone in postgresql.conf. Usa sempre nomi di fuso orario IANA (come Asia/Shanghai) anziché abbreviazioni (come CST) per evitare ambiguità.
Dovrei usare timestamp o timestamptz per memorizzare gli orari degli eventi?
Usa timestamptz per quasi tutto — azioni utente, chiamate API, log di audit ed eventi pianificati. Usa timestamp (senza fuso orario) solo per orari astratti che non sono legati a un momento specifico, come “il negozio apre alle 09:00” che significa le 9 del mattino in qualunque sia il fuso orario locale, non un istante UTC specifico.
Come gestisce PostgreSQL l’ora legale con timestamptz?
PostgreSQL gestisce l’ora legale correttamente quando si usa timestamptz perché memorizza tutto in UTC internamente. Quando recuperi un valore, PostgreSQL converte da UTC usando le regole di ora legale correnti per il fuso orario di sessione. Questo significa che lo stesso istante UTC memorizzato mostra correttamente orari locali diversi prima e dopo una transizione di ora legale.
Per una guida completa ai timestamp Unix — inclusi gestione della precisione, best practice sui fusi orari ed esempi di codice in JavaScript, Python e Go — leggi la nostra Guida ai Timestamp Unix.
Riepilogo
- Entrambi i tipi temporali di Postgres sono contatori di microsecondi; l’etichetta fa tutta la differenza
- Scegliere quello sbagliato significa timestamp enigmatici e calcoli rotti
- Verifica, converti e fai sanity-check con gli strumenti giusti per risparmiare ore di debug