1 Punkte von GN⁺ 3 시간 전 | Noch keine Kommentare. | Auf WhatsApp teilen
  • Um Fragen dazu zu beantworten, wie Daten zu einem bestimmten Datum (letzten Dienstag) ausgesehen haben, führt Postgres 19 Unterstützung für native temporale Tabellen (temporal tables) ein. Damit lassen sich Daten vor und nach Änderungen ohne separates Audit-Trigger-System nachverfolgen.
  • Postgres übernimmt die vor über zehn Jahren im SQL:2011-Standard definierten temporalen Tabellen verspätet in den Core und stößt damit später dazu als andere DB-Engines.
  • Statt der bisherigen zwei Spalten valid_from/valid_to und Exclusion Constraints auf Basis der Erweiterung btree_gist bietet Postgres nun mit einer einzelnen Range-Type-Spalte und der Constraint WITHOUT OVERLAPS eine intuitivere Ausdrucksweise.
  • Mit der Syntax FOR PORTION OF werden Zeilen bei UPDATE und DELETE automatisch aufgeteilt; die Engine verhindert Lücken und Überschneidungen auf der Zeitachse.
  • Diese Einführung deckt bei bi-temporalen Systemen die Hälfte der Anwendungszeit (application time) ab. Systemzeit (system time) wird noch nicht unterstützt, doch die Grundlage für künftige Releases ist gelegt.

Der klassische Weg – The Old-Fashioned Way

  • Der erste Versuch, Produktpreise chronologisch zu verfolgen, bestand aus zwei Datumsspalten valid_from, valid_to und einer CHECK-Constraint valid_from < valid_to.
    • Allerdings verhindert das nicht das Einfügen zweier Zeilen mit überlappenden Datumsbereichen für dasselbe Produkt (z. B. Produkt 42 kostet am selben Dienstag sowohl $9.99 als auch $14.99).
  • Die traditionelle Lösung nutzt die Erweiterung btree_gist und eine Exclusion Constraint.
    • In der Form EXCLUDE USING gist (product_id WITH =, daterange(valid_from, valid_to) WITH &&) entsteht beim Einfügen überlappender Zeilen ein Fehler.
  • Probleme dieses Ansatzes
    • GiST ist ein Postgres-spezifischer Index, der Erfahrung erfordert; als optionale Erweiterung stellt er eine Einstiegshürde dar.
    • Die Syntax von Exclusion Constraints ist nicht intuitiv, weshalb sie als Standardansatz schwer naheliegt.
    • In der Tabelle selbst ist kein Zeitbewusstsein eingebaut. Beim Ändern von Zeitbereichen müssen Zeilen manuell aufgeteilt oder zusammengeführt werden, und die Last der zeitlichen Konsistenz wird auf die Anwendung verlagert.

Eine kurze Geschichte der Zeit – A Brief History of Time

  • Der SQL:2011-Standard führte Gültigkeitszeit-Perioden (APPLICATION TIME), die Constraint WITHOUT OVERLAPS und die Syntax FOR PORTION OF zur Manipulation temporaler Daten ein.
  • Henrietta Dombrovskaya (Hetti) entwickelte gemeinsam mit Chad Slaughter die Erweiterung pg_bitemporal, ein Framework zur Verwaltung bi-temporaler Tabellen innerhalb von Postgres mit PL/pgSQL.
    • Seit 2015 stellte sie das Konzept auf mehreren Konferenzen vor und demonstrierte, wie sich Gültigkeitszeit (valid time) (wann eine Tatsache in der realen Welt wahr ist) und Transaktionszeit (transaction time) (wann die DB eine Tatsache aufgezeichnet hat) gleichzeitig verfolgen lassen.
  • Unterscheidung der beiden Zeitdimensionen
    • Gültigkeitszeit bedeutet: „Dieser Preis gilt von Januar bis Juni.“
    • Transaktionszeit ist die DB-Perspektive: „Diese Zeile wurde am 12. März um 15:47 Uhr eingefügt und am 3. April um 09:01 Uhr ersetzt.“
    • Kombiniert beantworten bi-temporale Tabellen die Frage: „Was glaubten wir auf Basis der damals bekannten Informationen, was der Preis letzten Dienstag war?“
  • pg_bitemporal verwendet EXCLUDE USING gist doppelt, jeweils einmal für den Bereich effective (Gültigkeitszeit) und asserted (Transaktionszeit).
    • Es bietet Funktionen für bi-temporales Einfügen, Aktualisieren, Korrigieren, Deaktivieren und Löschen sowie eine Implementierung der Allen’schen Intervallbeziehungen (Allen's interval relationships) für zeitliche Inferenz.
  • Grenzen der Erweiterung
    • Sie kann den Query Planner nicht so ändern, dass er temporale Bedingungen erkennt, sich nicht in das Constraint-System auf Engine-Ebene integrieren und keine native Manipulationssyntax bereitstellen → dafür muss die Funktionalität in den Core.
    • Postgres 19 übernimmt die Gültigkeitszeit-Hälfte eines bi-temporalen Systems – nicht das Ganze, aber ein großer Fortschritt.

Ranges als Rettung – Ranges to the Rescue

  • Der Ansatz in Postgres 19 verwendet statt separater valid_from/valid_to-Spalten eine einzelne Range-Type-Spalte valid_at DATERANGE.
    • Mit PRIMARY KEY (product_id, valid_at WITHOUT OVERLAPS) sind die Erweiterung btree_gist und Exclusion Constraints nicht mehr erforderlich.
    • WITHOUT OVERLAPS sorgt dafür, dass product_id zu jedem beliebigen Zeitpunkt eindeutig ist, erlaubt aber mehrere Zeilen für dasselbe Produkt, solange sich die Bereiche nicht überschneiden.
  • Intern werden weiterhin GiST-Indizes verwendet, und für die nicht-temporalen Spalten des Schlüssels ist btree_gist nötig; Postgres behandelt die Abhängigkeiten beim Initialisieren der Constraint jedoch automatisch.
  • Range-Notation: [ bedeutet inklusiv, ) exklusiv → [2025-01-01, 2025-07-01) schließt den 1. Januar ein, den 1. Juli aber aus.
    • Die letzte Gadget-Zeile [2026-01-01,) ist ein offener (open-ended) Bereich ohne definiertes Enddatum für den aktuellen Preis.
  • Verhalten beim Schutz vor Überschneidungen
    • Beim Einfügen eines ungültigen Bereichs [2025-03-01, 2025-01-01) erscheint der Fehler, dass die untere Grenze eines Bereichs kleiner oder gleich der oberen Grenze sein muss.
    • Beim Einfügen des überlappenden Bereichs [2025-03-01, 2025-09-01) erscheint ein Verstoß gegen die Exclusion Constraint products_pkey.
    • Durch die Nutzung von Ranges erhält man zwei Validierungen auf einmal.

Zuschneiden und aufteilen – Slicing and Dicing

  • Soll der Preis eines Produkts nur von März bis September 2025 auf $10.99 geändert werden, musste man beim bisherigen Ansatz Zeilen manuell aufteilen und einfügen; Fehler führten zu Lücken oder Überschneidungen.
  • In temporalen Tabellen lässt sich die Absicht direkt ausdrücken:
    • UPDATE products FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-09-01' SET price = 10.99 WHERE product_id = 1
  • In der Folge wächst die Zahl der Widget-Zeilen von 3 auf 5.
    • Die bisherige $9.99-Zeile wird auf [2025-01-01, 2025-03-01) verkürzt.
    • Eine neue $10.99-Zeile wird für den verbleibenden Bereich hinzugefügt.
    • Die bisherige $12.99-Zeile wird auf [2025-09-01, 2026-01-01) verkürzt.
    • Eine neue $10.99-Zeile wird für den verbleibenden Bereich [2025-07-01, 2025-09-01) hinzugefügt.
  • Der Grund, warum $10.99 in zwei Zeilen aufgeteilt wird: FOR PORTION OF verarbeitet jede passende Zeile unabhängig und führt angrenzende Bereiche danach nicht zusammen (coalesce).
    • Das Endergebnis hat keine Lücken und keine Überschneidungen – ein Vorteil, den reine Exclusion-Logik nicht bot.
  • Grenzfälle (edge cases)
    • Liegt der Bereich von FOR PORTION OF vollständig innerhalb einer einzelnen bestehenden Zeile, entstehen bis zu zwei Restzeilen (vorne und hinten).
    • Stimmt er exakt mit bestehenden Grenzen überein, sind keine Restzeilen nötig.
  • Für neu entstehende temporale Restzeilen ist keine INSERT-Berechtigung erforderlich, bestehende INSERT-Trigger werden jedoch ausgelöst → Vorsicht bei Audit-Logging oder SECURITY DEFINER-Triggerfunktionen.

Historie löschen – Erasing History

  • FOR PORTION OF funktioniert auch bei DELETE, etwa wenn ein bestimmtes Produkt von Juni bis Oktober 2025 vorübergehend aus dem Katalog entfernt werden soll:
    • DELETE FROM products FOR PORTION OF valid_at FROM '2025-06-01' TO '2025-10-01' WHERE product_id = 2
  • Ergebnis
    • Der Zeitraum Juni bis Oktober wird herausgeschnitten, und die $22.99-Zeile, die [2025-04-01, 2026-01-01) abdeckte, wird in zwei Restzeilen aufgeteilt: eine endet im Juni, die andere beginnt im Oktober.
    • Die Preisdaten vor und nach der Lücke bleiben mit ihren ursprünglichen Werten erhalten; DELETE erhöht die Anzahl der Zeilen.
  • Der Verwaltungsmechanismus temporaler Tabellen übernimmt alles automatisch und beseitigt auf Anwendungsebene das Risiko übermäßiger Löschung oder verwaister Fragmente (orphaned fragments).

Wahrheit in der Werbung – Truth in Advertising

  • Temporale Tabellen sind ohne temporale Foreign Keys (temporal foreign keys) nicht vollständig; Postgres 19 unterstützt sie mit dem Schlüsselwort PERIOD.
    • In der Form FOREIGN KEY (product_id, PERIOD valid_at) REFERENCES products (product_id, PERIOD valid_at)
  • Das Schlüsselwort PERIOD signalisiert, dass der Foreign Key selbst zeitbasiert ist.
    • Das referenzierte Product muss über die gesamte Dauer des valid_at-Bereichs der Variant existieren.
    • Die Kombination aller passenden Zeilen der referenzierten Tabelle muss den Zeitraum der referenzierenden Zeile vollständig abdecken.
  • Der Versuch, eine Variant zu erzeugen, die den Zeitraum des Product überschreitet ([2025-01-01, 2027-01-01)), wird abgelehnt.
    • Eine Variant, die behauptet, bis 2027 gültig zu sein, obwohl der Widget-Preis nur bis Mitte 2026 definiert ist, wird wegen eines Foreign-Key-Constraint-Verstoßes abgelehnt.
  • Eine wichtige Einschränkung
    • Temporale Foreign Keys unterstützen als referenzielle Aktion nur NO ACTION; CASCADE, SET NULL und SET DEFAULT sind ausgeschlossen.
    • Beim Löschen einer Product-Zeile, von der eine Variant abhängt, entsteht immer ein Fehler. Grund ist die Komplexität kaskadierender temporaler Operationen; die Anwendung muss dies explizit behandeln.

Kleine Schritte – Baby Steps

  • Aktuell verfügbare Funktionen: temporale Tabellen auf Basis von Gültigkeitszeit mit Überschneidungsschutz, temporale Datenmanipulation und temporale Foreign Keys.
  • Die größte Lücke ist die auch als Transaktionszeit bezeichnete Systemzeit (system time).
    • Gültigkeitszeit verfolgt, wann eine Tatsache in der realen Welt wahr ist; Systemzeit verfolgt, wann die DB diese Tatsache wahrnimmt. Viele Systeme nutzen beides.
    • Genau diesen Bereich deckt seit 2015 die Erweiterung pg_bitemporal ab.
    • Systemzeit lässt sich zwar mit Triggern emulieren, doch das ist nicht dasselbe wie eine transparente Verwaltung durch die Engine, wie bei den neuen temporalen Funktionen.
  • Auch die Dokumentation zu temporalen Tabellen stellt klar, dass Systemzeit nativ nicht unterstützt wird, aber emuliert werden kann. Ob sie ab Postgres 20 eingeführt wird, ist offen; die Grundlage ist jedoch bereits gelegt.

Schlussgedanken – Final Thoughts

  • Der Ansatz EXCLUDE USING gist funktioniert, ist aber ein vergleichsweise grober Workaround; Erweiterungen wie pg_bitemporal haben das Konzept bewiesen und die Diskussion weitergeführt.
  • Ein deutlich intuitiverer Ansatz als GiST-Exclusion-Constraints
    • WITHOUT OVERLAPS im Primärschlüssel liest sich wie schlichtes Englisch, und FOR PORTION OF beschreibt die Aktion direkt.
    • Die automatische Zeilenaufteilung bei temporalen Updates und Deletes beseitigt eine ganze Klasse potenzieller Bugs.
  • Der Weg von SQL:2011 bis Postgres 19 war lang; Hetti und die Community haben über Jahre die Notwendigkeit und Machbarkeit des Musters gezeigt, nun ist es im Core angekommen.
  • Unterstützung für Systemzeit in künftigen Releases bleibt zu beobachten. Wenn Postgres beide Hälften der Bi-Temporalität beherrscht, erweitert das die Möglichkeiten erheblich.

Noch keine Kommentare.

Noch keine Kommentare.