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.