← Alle Beiträge

Timestamps in Datenbanken speichern: DATETIME vs. INT vs. BIGINT

Den falschen Spaltentyp für Timestamps zu wählen führt zu Zeitzonen-Drift, dem Jahr-2038-Überlauf, kaputten Bereichsabfragen und verwirrender API-Ausgabe. Vergleiche native datetime-Typen, BIGINT-Epoch-Werte und Strings in MySQL und PostgreSQL.

Drei Wege, einen Timestamp zu speichern

Die meisten Datenbanken bieten mindestens drei Optionen: einen nativen datetime-Typ (TIMESTAMP, DATETIME, TIMESTAMPTZ), eine einfache Ganzzahl (INT oder BIGINT) oder einen String (VARCHAR). Jede hat andere Kompromisse bei Speichergröße, Abfrage-Ergonomie, Zeitzonen-Handhabung und Zukunftssicherheit. Für die meisten Produktdatenbanken ist eine native datetime-Spalte der beste Standard, weil die Datenbank den Wert als Zeit statt als anonyme Zahl vergleichen, indizieren, kürzen, gruppieren und formatieren kann.

  • Nativer datetime-Typ — am besten für Datumsarithmetik, Zeitzonen-Umwandlung und Lesbarkeit
  • BIGINT-Ganzzahl — gut für Inserts mit hohem Durchsatz und einfache numerische Bereichsabfragen
  • VARCHAR-String — fast immer falsch: String-Vergleich von Daten funktioniert nur mit striktem ISO-8601-Format
  • INT-Ganzzahl — für zukünftige Timestamps vermeiden, außer du hast die Jahr-2038-Grenze vollständig geprüft

MySQL: TIMESTAMP vs. DATETIME vs. INT

MySQL hat zwei Datum-Zeit-Typen, die ähnlich aussehen, sich aber sehr unterschiedlich verhalten — und einer hat ein hartes Ablaufdatum. TIMESTAMP ist praktisch, wenn du automatische Umwandlung zwischen UTC und Session-Zeitzone willst, aber sein historischer 32-Bit-Bereich macht ihn riskant für zukunftsgerichtete Produktdaten. DATETIME speichert das gelieferte literale Datum und die Uhrzeit, was meist klarer ist, wenn die Anwendung vor dem Schreiben auf UTC standardisiert.

  • TIMESTAMP: intern als 32-Bit-Unix-Sekunden gespeichert — begrenzt von 1970-01-01 bis 2038-01-19
  • TIMESTAMP: wandelt beim Einfügen/Lesen automatisch zwischen UTC und Session-Zeitzone um
  • DATETIME: speichert die literale Datum-Zeit, keine Zeitzone. Bereich 1000-01-01 bis 9999-12-31. Nicht vom Y2038 betroffen.
  • DATETIME: wandelt keine Zeitzonen um — du steuerst UTC auf Anwendungsebene
  • Empfehlung: nutze DATETIME mit expliziten UTC-Werten für neue Tabellen, um die 2038-Grenze zu vermeiden

PostgreSQL: TIMESTAMPTZ ist die richtige Wahl

PostgreSQLs TIMESTAMP WITH TIME ZONE (TIMESTAMPTZ) speichert Timestamps intern als UTC-Mikrosekunden und wandelt bei der Ausgabe in die Session-Zeitzone um. Es ist für die meisten Fälle die sicherste und korrekteste Option, weil es einen echten Moment darstellt. Der Name kann irreführen: TIMESTAMPTZ speichert nicht das ursprüngliche Zeitzonen-Label wie America/New_York. Es speichert den Moment und zeigt ihn dann gemäß der aktuellen Session-Zeitzone.

  • TIMESTAMPTZ: speichert UTC, wandelt bei der Ausgabe in die Session-Zeitzone — portabel und sommerzeitsicher
  • TIMESTAMP (ohne Zeitzone): speichert den literalen Wert ohne Umwandlung — nur für zeitzonen-naive Daten nutzen
  • EXTRACT(EPOCH FROM col): gibt Unix-Sekunden als Float aus jeder TIMESTAMP-Spalte zurück
  • TO_TIMESTAMP(epoch): wandelt Unix-Sekunden zurück in ein TIMESTAMPTZ

Indizierung und Abfrageleistung

Für normale Anwendungstabellen ist der Leistungsunterschied zwischen nativen datetime-Spalten und BIGINT-Epoch-Spalten selten der entscheidende Faktor. Abfrageform, Index-Design, Partitionierung und Zeilenzahl zählen mehr. Wähle zuerst den Typ, der die Bedeutung korrekt hält, dann indiziere ihn für die Bereichsabfragen, die deine Anwendung wirklich ausführt.

  • Alle drei Typen unterstützen B-Tree-Indizes und effiziente Bereichsabfragen
  • BIGINT-Ganzzahlen sind bei Gleichheits- und Bereichsscans auf Tabellen mit sehr hohem Volumen marginal schneller
  • Native datetime-Typen erlauben indizierte Datumsteil-Abfragen: WHERE created_at::date = '2024-01-01'
  • VARCHAR-Timestamps sind am schlechtesten für die Leistung — String-Vergleich versteht keine Daten

Wann BIGINT-Epoch-Speicherung sinnvoll ist

BIGINT ist sinnvoll, wenn die Daten ereignisartig, einfügelastig und bereits von einem anderen System als Unix-Zeit erzeugt sind. Analytics-Pipelines, Telemetrie-Streams, Queues und kompakte Binärprotokolle nutzen oft Epoch-Millisekunden, weil numerische Werte schnell zu vergleichen und sprachneutral sind. Der Kompromiss ist Lesbarkeit: Menschen brauchen einen Konverter und SQL-Datumsarithmetik wird ausführlicher.

  • Nutze BIGINT für Unix-Millisekunden, wenn JavaScript-Clients die Ereignisse direkt erzeugen
  • Nutze BIGINT für Unix-Sekunden, wenn das Quellsystem Unix-artig ist und Sekundengenauigkeit reicht
  • Dokumentiere die Einheit im Spaltennamen: created_at_ms ist klarer als created_at_epoch
  • Füge eine generierte datetime-Spalte hinzu, wenn Analysten lesbare SQL-Abfragen brauchen
  • Vermeide INT für moderne zukunftsgerichtete Timestamps wegen der 32-Bit-Bereichsgrenzen

Empfohlene Schema-Muster

Für die meisten Webanwendungen speichere einen Moment in UTC und speichere die bevorzugte Zeitzone des Nutzers nur dann separat, wenn du die lokale Wanduhr-Absicht rekonstruieren musst. Ein für 9:00 Uhr America/New_York geplantes Meeting unterscheidet sich von einem zu einem präzisen UTC-Moment erstellten Ereignis-Log; modelliere diese Fälle unterschiedlich.

  • Ereignis-Logs: created_at TIMESTAMPTZ in PostgreSQL oder created_at DATETIME in UTC für MySQL
  • JavaScript-Ereignis-Ingestion: created_at_ms BIGINT plus klare API-Dokumentation
  • Wiederkehrende lokale Pläne: local_date, local_time und timezone_id, dann den nächsten Moment berechnen
  • Ablauf-Timestamps: expires_at als nativer datetime oder expires_at_seconds mit expliziten Unix-Sekunden
  • Audit-Tabellen: behalte created_at und updated_at als native datetime-Spalten für lesbares Debugging

FAQ zu Datenbank-Timestamps

MongoDB stores dates natively as the BSON Date type — a signed 64-bit integer of milliseconds since the Unix epoch. The driver maps it to a Date object in JavaScript, datetime in Python, ISODate in the mongo shell.

  • BSON Date: 8 bytes, signed 64-bit, milliseconds since 1970 UTC
  • Indexes natively — range queries on Date are fast and idiomatic
  • Use ISODate("2024-01-01T00:00:00Z") in the shell to create comparable values
  • For multi-precision needs, use Decimal128 or a sub-document with seconds + nanos; Date itself is millisecond precision
  • TTL indexes operate on Date fields specifically — integer epoch fields will not work with expireAfterSeconds

DynamoDB: epoch attributes and TTL

DynamoDB has no dedicated date type. Two patterns dominate: ISO 8601 strings for human readability, or epoch numbers for compactness and TTL support.

  • TTL requires the attribute to be a Number representing Unix seconds (not milliseconds)
  • Strings store ISO 8601 with full timezone info but cost more bytes and sort lexicographically (so include the Z suffix consistently)
  • Range queries on a sort-key epoch number are extremely cheap and align with DynamoDB’s query model
  • Avoid mixing seconds and milliseconds across tables — DynamoDB will not protect you from the unit drift
  • DynamoDB Streams emit an ApproximateCreationDateTime in ISO 8601 regardless of how you stored the original value

FAQ

Should I store UTC or local time in a database?
Store UTC for event timestamps and convert to local time when displaying. Store a timezone identifier separately when the user's local wall-clock intent matters, such as recurring meetings or business hours.
Is BIGINT better than TIMESTAMP?
Not generally. BIGINT is useful for numeric epoch pipelines, but native datetime types are easier for SQL date arithmetic, readable debugging, and timezone-aware output.
Should MySQL use TIMESTAMP or DATETIME?
For new application tables, DATETIME with UTC values is often safer because it avoids the 2038 range limit and does not silently depend on session timezone conversion.
Should I store timestamps as UTC or with a timezone?
Store the instant in UTC (TIMESTAMPTZ in PostgreSQL, or DATETIME with UTC values in MySQL) and convert to local time on display. Keep a separate IANA timezone column only when you must reconstruct a user's local wall-clock intent, such as recurring meetings.