Dein Zeitstempel ist eine Lüge
Was eine Zugfahrkarte über das Speichern von Zeit in Datenbanken lehrt
Ich war dabei, ein Zugticket von New York nach Chicago zu buchen, als mir plötzlich klar wurde, warum Zeitstempeltypen in Postgres so verwirrend sind. Das Ticket zeigte:
- Abfahrt: 8:00 Uhr EST
- Ankunft: 19:30 Uhr CST
- Dauer: 11 Stunden 30 Minuten
Drei verschiedene Arten, über Zeit zu sprechen, alle auf demselben Ticket. Und jede davon muss in einer Datenbank anders gespeichert werden.
Die Frage, die niemand zuerst stellt
Sowohl TIMESTAMP als auch TIMESTAMPTZ in Postgres belegen exakt 8 Byte mit derselben Mikrosekunden-Genauigkeit. Warum also zwei Typen?
Weil „Wie spät ist es?” vollständig davon abhängt, was du erzählen möchtest.
Wenn ich in New York in diesen Zug steige, muss ich wissen, dass er um 8:00 Uhr Eastern abfährt. Das ist die Zahl auf der Bahnhofsuhr, die ich abgleichen muss. Wenn mich mein Freund in Chicago abholt, muss sie wissen, dass ich um 19:30 Uhr Central ankomme – das ist die Zahl auf ihrer Uhr. Und wenn ich herausfinden will, ob ich noch Zeit habe, mein Buch zu lesen, muss ich wissen, dass es eine elfeinhalbstündige Reise ist.
Derselbe Zug. Dieselbe Reise. Drei völlig unterschiedliche Darstellungen von Zeit.
Was TIMESTAMPTZ tatsächlich tut
Hier ist der Trick mit TIMESTAMPTZ – und er ist nicht das, was die meisten denken. Er speichert nicht die Zeitzone. Der Name ist irreführend.
Was er tut: Er konvertiert jede eingegebene Zeit in UTC, bevor er sie speichert, und konvertiert sie beim Lesen zurück in die Zeitzone deiner Sitzung. Das „TZ” bezieht sich nicht auf die Speicherung, sondern auf die Konvertierungsunterstützung.
Nehmen wir diese Zugabfahrt. Jemand in Tokio fragt deine Datenbank ab und sieht die Abfahrt in JST. Jemand in London sieht sie in GMT. Alle betrachten denselben absoluten Zeitpunkt, nur ausgedrückt in ihrer konfigurierten Zeitzone. Das ist perfekt für die Aufzeichnung von Ereignissen: „Wann wurde diese Zahlung verarbeitet?” oder „Wann erfolgte diese API-Anfrage?”
Aber was ist mit diesem Zugticket? Du möchtest nicht, dass sich die Abfahrtszeit ändert, nur weil jemand von einer anderen Zeitzone darauf zugreift. Der Zug fährt um 8:00 Uhr Eastern ab, Punkt. Das ist kein absoluter Zeitpunkt – es ist ein Versprechen darüber, was die Uhr in Grand Central anzeigen wird.
Speichere das, was du tatsächlich meinst
Für diese Zugreise musst du verschiedene Dinge für verschiedene Zwecke speichern:
- Die absoluten Zeitpunkte (
departs_atundarrives_atalsTIMESTAMPTZ) - Den Anzeigekontext (
origin_timezoneunddestination_timezoneals Text) - Die Dauer (ein
INTERVALzwischen den beiden Zeitpunkten)
Jetzt kann deine Anwendung das tun, was das Zugticket tut: „Abfahrt 8:00 Uhr EST” anzeigen, indem der absolute Zeitpunkt in die Ursprungszeitzone konvertiert wird, „Ankunft 19:30 Uhr CST” durch Konvertierung in die Zielzeitzone und „Dauer: 11 Std. 30 Min.” direkt aus dem Intervall anzeigen.
Die Person, die das Ticket von Tokio aus bucht, sieht dieselben lokalen Zeiten an jedem Bahnhof. Das ist es, was sie wissen muss.
Warum deine Flugtracking-App es falsch gemacht hat
Schon mal aufgefallen, dass einige Flugtracking-Apps deine Zeitzone während des Fluges anzeigen? Als ob du über dem Atlantik wärst und es heißt „Aktuelle Zeit: 16:32 Uhr GMT.” Wen interessiert das? Du bist nicht in Greenwich, du bist irgendwo über dem Ozean in 11.000 Metern Höhe.
Was du tatsächlich sehen möchtest:
- Verstrichene Zeit seit dem Start
- Verbleibende Zeit bis zur Ankunft
- Wie spät es dort bei der Landung sein wird
Keines davon sind Zeitzonenkonvertierungen. Die ersten zwei sind Intervalle – Dauern, keine Zeitpunkte. Das letzte ist eine Zeitzonenkonvertierung, aber an einen bestimmten Ort, nicht in „deine aktuelle Zeitzone.”
Siehst du? Zwei Intervallberechnungen (NOW() - actual_departure und estimated_arrival - NOW()), eine Zeitzonenkonvertierung an einen bestimmten Ort (AT TIME ZONE destination_timezone). Deine aktuelle Zeitzone spielt dabei keine Rolle.
Wenn die Wanduhr-Zeit das ist, was du wirklich brauchst
Hotels interessieren sich nicht für absolute Zeitpunkte. Sie interessieren sich für Uhrzeit-Anzeigen an ihrem Standort.
„Check-in nach 15:00 Uhr” bedeutet nicht „Check-in 15 Stunden nach Mitternacht UTC.” Es bedeutet: „Wann immer die Uhr in unserer Lobby 15:00 Uhr anzeigt, kannst du einchecken.” Wenn deine Server in Virginia stehen, aber das Hotel in Paris ist, möchtest du trotzdem, dass diese Regel um 15:00 Uhr Pariser Zeit ausgelöst wird.
Der TIME-Typ (ohne Datum oder Zeitzone) repräsentiert genau das: „Eine Anzeige auf einer Uhr.” Kombiniere ihn mit einem Textfeld für die Zeitzone („Europe/paris”), und du kannst Wall-Clock-Richtlinien durchsetzen, egal wo deine Server leben. Aber du wirst auch TIMESTAMPTZ-Spalten wollen, um zu speichern, wann bestimmte Gäste tatsächlich ein- und ausgecheckt haben – das sind absolute Zeitpunkte, die dein Backend verfolgen muss.
Das Kalenderproblem
Ich habe eine wiederkehrende Erinnerung um 9:00 Uhr eingestellt: „Tägliche Prioritäten prüfen.” Ich möchte diese Erinnerung um 9:00 Uhr, egal wo ich bin. Wenn ich unterwegs bin, sollte sie trotzdem um 9:00 Uhr lokaler Zeit ausgelöst werden.
Aber ich habe auch ein Kalenderereignis: „Team-Standup um 10:00 Uhr EST.” Mein Teamkollege in Berlin muss für dasselbe Ereignis „16:00 Uhr CET” sehen. Dasselbe Meeting, unterschiedliche Anzeigezeiten, weil dies ein absoluter Zeitpunkt ist, an dem wir alle teilnehmen.
Zwei verschiedene Arten von Ereignissen, zwei verschiedene Speicherstrategien. Das Meeting bekommt ein TIMESTAMPTZ. Die Erinnerung bekommt ein TIME plus meine aktuelle Zeitzoneneinstellung. Versuche nicht, beides in dasselbe Feld zu zwängen.
Dinge, die in der Produktion kaputtgehen
Selbst mit den richtigen Typen kann Präzion dich beißen. Postgres speichert Mikrosekunden: 10:00:00.123456. JavaScripts Date-Objekt verwendet Millisekunden: 10:00:00.123.
Diese Abfrage könnte also mysteriöserweise keine Zeilen zurückgeben:
SELECT * FROM orders WHERE created_at = '2026-01-15 10:00:00.123';Die Datenbank hat 10:00:00.123456 und dein Code übergibt 10:00:00.123. Je nachdem, wie dein Treiber damit umgeht, könnten diese nicht übereinstimmen.
Verwende keine exakte Gleichheit für Zeitstempel. Verwende Bereichsabfragen oder – besser – suche nicht nach Datensätzen anhand ihres Erstellungszeitstempels. Verwende eine ordnungsgemäße Unique-Constraint oder einen Idempotenz-Key.
Praktische Regeln
Standardmäßig TIMESTAMPTZ verwenden. Im Zweifelsfall verwende TIMESTAMPTZ. Es handhabt Multi-Region-Deployments, Sommerzeit und zukünftige Zeitzonenänderungen automatisch. Es hat dieselbe Speichergröße wie TIMESTAMP, also gibt es keinen Nachteil.
Kontext separat speichern. Wenn du „Abfahrt 8:00 Uhr EST” neben dem tatsächlichen Zeitpunkt anzeigen musst, speichere sowohl das TIMESTAMPTZ als auch die origin_timezone als separate Spalten. Versuche nicht, alles in ein Feld zu kodieren.
Denke an Intervalle. Viele zeibezogene Anforderungen sind tatsächlich Dauern, keine Zeitpunkte. „Wie lange ist dies noch ausstehend?” „Wann läuft dies ab?” Verwende INTERVAL-Operationen, keine Zeitzonenkonvertierungen.
Betreiben alles in UTC. Deine Server sollten auf UTC eingestellt sein. Deine Datenbanksitzungen sollten standardmäßig UTC verwenden. Konvertiere nur bei der Anzeige für Benutzer in lokale Zeitzonen – und nur wenn du weißt, welche Zeitzone relevant ist.
Zeitzone-Info von Clients einfordern. Wenn ein Client 2026-01-15T10:00:00 ohne Offset sendet, lehne es ab. Erfordere ISO-8601-Format mit entweder Z oder einem expliziten Offset wie -05:00. Rate nicht.
Gute Standards durchsetzen
Wenn TIMESTAMPTZ dein Standard ist (und das sollte er sein), erwäge, dies auf Datenbankebene durchzusetzen. Ein Trigger, der TIMESTAMP WITHOUT TIME ZONE-Spalten ablehnt, klingt extrem, aber „TZ vergessen” schon bei der Schema-Erstellung zu fangen, ist besser, als es sechs Monate später zu debuggen, wenn jemand eine neue Tabelle hinzufügt und es vergisst.
Was mich diese Zugfahrkarte gelehrt hat
Zeit in Datenbanken ist nicht schwer, weil Zeitstempel kompliziert sind. Sie ist schwer, weil wir normalerweise mehrere Anliegen in einem Feld speichern oder nicht darüber nachdenken, was wir den Benutzern tatsächlich zeigen wollen.
Diese Zugfahrkarte hatte es richtig: Abfahrtszeit in der Ursprungszeitzone, Ankunftszeit in der Zielzeitzone und Dauer als separates Element. Drei verschiedene Informationen, jede auf ihre eigene Art bedeutsam.
Deine Datenbank kann dasselbe tun. Speichere die absoluten Zeitpunkte als TIMESTAMPTZ. Speichere den Anzeigekontext (Zeitzonen, Standorte) als separate Spalten. Verwende INTERVAL-Typen für Dauern. Lass Postgres die Konvertierungen durchführen, wenn du sie brauchst, aber sei explizit darüber, welche Zeitzone für welchen Zweck relevant ist.
Meistens bedeutet das TIMESTAMPTZ und UTC überall, mit Zeitzonenkonvertierungen nur zur Anzeigezeit. Aber wenn du Wanduhr-Zeiten oder wiederkehrende Zeitpläne brauchst, existieren TIMESTAMP- oder TIME-Typen genau aus diesem Grund.
Der Schlüssel ist zu wissen, welche Frage du beantworten willst: „Wann ist das passiert?” vs. „Um wie viel Uhr soll ich dort sein?” vs. „Wie lange wird das dauern?” Es sind alles verschiedene Fragen über Zeit, und sie brauchen oft unterschiedliche Speicherstrategien.
Denke darüber nach, was deine Benutzer sehen müssen. Dann speichere die Daten, die es dir ermöglichen, ihnen genau das zu zeigen.
Ressourcen
- PostgreSQL Date/Time Types Documentation
- PostgreSQL Timestamp Best Practices
- ISO 8601 Date and Time Format
- Time Zone Database (IANA)
- Dealing with Timestamps in Distributed Systems