überlappungsfreie Zeiträume mit gültig_von und gültig_bis
Rabattaktionen, Vertragskonditionen oder Tarifstufen gelten fast nie für alle Zeit, sondern nur für einen bestimmten Zeitraum. Effective Dating bildet genau das mit zwei Spalten pro Zeile ab, einem Start- und einem Endzeitpunkt der Gültigkeit, gesteuert von der Fachlogik statt von der Datenbank automatisch. Die eigentliche Herausforderung liegt selten in den Spalten selbst, sondern darin, zuverlässig zu verhindern, dass sich zwei Gültigkeitszeiträume für dieselbe Entität überlappen. Dieser Artikel zeigt praktikable Constraint-Strategien dafür und arbeitet zwei durchgängige Praxisbeispiele durch.
Inhaltsverzeichnis
- 1. Die Grundidee: gültig_von und gültig_bis als fachlich gesteuertes Intervall
- 2. Warum Überlappungsfreiheit eine kritische Geschäftsregel ist
- 3. Halboffene Intervalle als Konvention gegen Off-by-one-Fehler
- 4. Constraint-Strategien gegen überlappende Gültigkeitszeiträume
- 5. Praxisbeispiel: befristete Rabattaktionen ohne doppelte Gültigkeit
- 6. Praxisbeispiel: Vertragsänderungen als Kette effektiv datierter Versionen
- 7. Den aktuell gültigen Datensatz zuverlässig abfragen
- 8. Indexierung für performante Effective-Dating-Abfragen
- 9. Abgrenzung zu System-Versioning: fachlich gesteuert statt automatisch
- 10. Zusammenfassung
- 11. FAQ
1. Die Grundidee: gültig_von und gültig_bis als fachlich gesteuertes Intervall
Effective Dating ergänzt eine Zeile um zwei Spalten, die einen Zeitraum beschreiben, für den diese Zeile fachlich anwendbar ist. Anders als bei System-Versioning setzt hier nicht die Datenbank automatisch die Werte, sondern die Anwendung oder eine Sachbearbeiterin entscheidet bewusst, ab wann und bis wann ein Rabatt, ein Tarif oder eine Vertragskondition gilt. Die Datenbank speichert diese Entscheidung lediglich, prüft sie aber nicht von sich aus auf fachliche Korrektheit.
Diese Einfachheit ist zugleich Stärke und Schwäche: Effective Dating lässt sich mit zwei DATE-Spalten in jeder relationalen Datenbank ohne Sonderfunktionen umsetzen, verlangt aber von der Anwendung, Überlappungen selbst zu verhindern, sofern die Datenbank nicht durch zusätzliche Constraints eingebunden wird.
2. Warum Überlappungsfreiheit eine kritische Geschäftsregel ist
Zwei gleichzeitig aktive Preise für dasselbe Produkt oder zwei sich überschneidende Vertragskonditionen für denselben Kunden sind selten ein harmloser Datenfehler, sondern führen häufig zu handfesten fachlichen Problemen: doppelte Rabattgewährung, widersprüchliche Abrechnungen oder ein System, das bei einer Abfrage zwei Zeilen liefert, wo eine Anwendung genau eine erwartet und deshalb die erstbeste, möglicherweise falsche, Zeile verwendet.
Solche Fehler entstehen typischerweise nicht durch offensichtlich falsche Eingaben, sondern durch Randfälle: ein Enddatum, das versehentlich einen Tag zu spät gesetzt wird, ein neuer Zeitraum, der beim Anlegen nicht gegen bestehende Zeiträume geprüft wird, oder ein Batch-Import, der mehrere Quellen ungeprüft zusammenführt. Ohne eine erzwungene Regel in der Datenbank verlässt sich das System vollständig auf die Sorgfalt jedes einzelnen Schreibpfads.
3. Halboffene Intervalle als Konvention gegen Off-by-one-Fehler
Eine bewährte Konvention ist das halboffene Intervall: gültig_von ist eingeschlossen, gültig_bis ist ausgeschlossen, geschrieben als [von, bis). Ein Zeitraum vom 1. bis zum 31. Januar endet damit exakt am 1. Februar, nicht am 31. Januar 23:59:59, und der nächste Zeitraum kann nahtlos mit gültig_von gleich dem 1. Februar anschließen, ohne eine Lücke oder Überlappung von wenigen Sekunden zu riskieren.
Wird stattdessen ein geschlossenes Intervall verwendet, bei dem beide Grenzen eingeschlossen sind, entstehen regelmäßig Off-by-one-Fehler beim Berechnen von Anschlusszeiträumen, insbesondere bei Datumsangaben ohne Uhrzeit, wo die Frage, ob ein Tag noch dazugehört, leicht uneinheitlich beantwortet wird. Halboffene Intervalle vermeiden diese Mehrdeutigkeit von Grund auf.
4. Constraint-Strategien gegen überlappende Gültigkeitszeiträume
PostgreSQL bietet mit Exclusion Constraints auf Range-Typen die robusteste Lösung: Eine Spalte vom Typ daterange oder tsrange, kombiniert mit einem GIST-Index, verhindert direkt auf Datenbankebene, dass zwei sich überlappende Zeiträume für denselben Entitäts-Schlüssel eingefügt werden, unabhängig davon, über welchen Codepfad der Insert erfolgt. Ein Verstoß führt zu einem klaren Fehler zum Zeitpunkt des Schreibens, nicht erst bei einer späteren Abfrage.
In Datenbanken ohne native Range-Typen, etwa MySQL oder SQL Server, lässt sich dieselbe Garantie über einen Trigger nachbilden, der vor jedem Insert und Update prüft, ob sich der neue Zeitraum mit einem bestehenden überschneidet, oder pragmatischer über eine CHECK-Constraint-nahe Anwendungslogik innerhalb derselben Transaktion mit einer expliziten SELECT ... FOR UPDATE-Sperre auf betroffene Zeilen, um Race Conditions bei gleichzeitigen Schreibvorgängen auszuschließen.
-- PostgreSQL: Exclusion Constraint gegen überlappende Rabattzeiträume
CREATE TABLE rabattaktion (
aktion_id SERIAL PRIMARY KEY,
produkt_id INT NOT NULL,
rabatt_proz NUMERIC(5,2) NOT NULL,
zeitraum DATERANGE NOT NULL,
EXCLUDE USING GIST (produkt_id WITH =, zeitraum WITH &&)
);
-- Dieser Insert schlägt fehl, weil sich der Zeitraum überschneidet
INSERT INTO rabattaktion (produkt_id, rabatt_proz, zeitraum)
VALUES (100, 10.00, daterange('2026-06-01', '2026-06-15', '[)'));
5. Praxisbeispiel: befristete Rabattaktionen ohne doppelte Gültigkeit
Für ein Produkt sollen mehrere Rabattaktionen im Laufe des Jahres möglich sein, aber niemals zwei gleichzeitig aktive Aktionen für denselben Artikel. Die Tabelle aus dem Beispiel oben erzwingt das über den Exclusion Constraint: Sobald ein Marketing-Team versucht, eine neue Aktion anzulegen, die sich mit einer bestehenden überschneidet, weist die Datenbank den Insert sofort zurück, lange bevor der Fehler im laufenden Betrieb sichtbar würde.
Für die Abfrage des aktuell gültigen Rabatts genügt dann eine einfache Bedingung mit dem Containment-Operator, der prüft, ob ein bestimmter Zeitpunkt innerhalb des gespeicherten Zeitraums liegt, ganz ohne die sonst üblichen zwei separaten Vergleichsoperationen auf gültig_von und gültig_bis.
-- Aktuell gültigen Rabatt für ein Produkt ermitteln
SELECT rabatt_proz
FROM rabattaktion
WHERE produkt_id = 100
AND zeitraum @> CURRENT_DATE;
6. Praxisbeispiel: Vertragsänderungen als Kette effektiv datierter Versionen
Vertragsänderungen lassen sich als Kette von Zeilen modellieren, bei der jede neue Version den Zeitraum der vorherigen automatisch beendet. Ändert sich etwa eine monatliche Vertragsgebühr zum 1. Juli, wird die bestehende Zeile mit gültig_bis auf den 1. Juli begrenzt und eine neue Zeile mit gültig_von zum 1. Juli und dem neuen Betrag eingefügt, innerhalb derselben Transaktion, damit niemals ein Moment ohne oder mit zwei gültigen Konditionen existiert.
Diese Kette lässt sich zusätzlich über eine Fremdschlüsselbeziehung auf die jeweils vorherige Version absichern, sodass sich der komplette Änderungsverlauf eines Vertrags nachvollziehen lässt, ohne auf eine separate Historientabelle angewiesen zu sein, allerdings ohne die automatische Garantie, die System-Versioning für die Erfassungszeit bieten würde.
-- Bestehende Vertragskondition zum Stichtag beenden
UPDATE vertragskondition
SET gueltig_bis = '2026-07-01'
WHERE vertrag_id = 55 AND gueltig_bis IS NULL;
-- Neue Kondition ab demselben Stichtag einfügen
INSERT INTO vertragskondition (vertrag_id, monatsgebuehr_cent, gueltig_von, gueltig_bis)
VALUES (55, 4990, '2026-07-01', NULL);
7. Den aktuell gültigen Datensatz zuverlässig abfragen
Ohne Range-Typen wird der aktuell gültige Datensatz über zwei Bedingungen gefunden: gültig_von kleiner oder gleich dem Stichtag, und gültig_bis entweder NULL, als Kennzeichen für offenes Ende, oder größer als der Stichtag. Diese Formulierung sollte konsequent an jeder Stelle im Code identisch verwendet werden, denn kleine Abweichungen, etwa ein vergessenes NULL-Handling, führen zu inkonsistenten Ergebnissen zwischen verschiedenen Abfragen derselben Tabelle.
Am robustesten lässt sich das in einer View oder einer Datenbankfunktion kapseln, die genau diese Logik einmal zentral definiert, statt sie in jeder einzelnen Abfrage im Anwendungscode zu wiederholen und damit das Risiko widersprüchlicher Implementierungen einzugehen.
-- Aktuell gültige Vertragskondition über eine View kapseln
CREATE VIEW vertragskondition_aktuell AS
SELECT *
FROM vertragskondition
WHERE gueltig_von <= CURRENT_DATE
AND (gueltig_bis IS NULL OR gueltig_bis > CURRENT_DATE);
8. Indexierung für performante Effective-Dating-Abfragen
Für Range-Typen in PostgreSQL bietet sich ein GIST-Index an, der sowohl Containment- als auch Overlap-Prüfungen effizient unterstützt und derselbe Index sein kann, der bereits für den Exclusion Constraint benötigt wird. Für die klassische Zwei-Spalten-Variante mit gültig_von und gültig_bis hilft ein zusammengesetzter Index auf beiden Spalten, wobei die Behandlung von NULL in gültig_bis je nach Datenbank unterschiedlich indexfreundlich ist und gegebenenfalls ein Sentinel-Wert wie das Jahr 9999 statt NULL vorzuziehen ist.
In SQL Server lässt sich zusätzlich ein gefilterter Index einsetzen, der ausschließlich die aktuell gültigen Zeilen abdeckt, also solche mit offenem oder in der Zukunft liegendem gültig_bis, was Abfragen auf den aktuellen Stand deutlich beschleunigt, ohne die vollständige Historie mitindizieren zu müssen.
9. Abgrenzung zu System-Versioning: fachlich gesteuert statt automatisch
Effective Dating und System-Versioning wirken auf den ersten Blick ähnlich, unterscheiden sich aber grundlegend in der Verantwortlichkeit: Bei Effective Dating entscheidet die Fachlogik bewusst über Start und Ende eines Zeitraums, während System-Versioning automatisch bei jeder technischen Änderung eine neue Version erzeugt, unabhängig davon, ob sich fachlich überhaupt etwas geändert hat.
In der Praxis lassen sich beide Konzepte kombinieren: Eine Tabelle mit Effective-Dating-Spalten für die fachliche Gültigkeit kann zusätzlich system-versioniert sein, um auch technische Änderungen an dieser fachlichen Gültigkeit nachvollziehbar zu machen, genau das Muster, das im Artikel zur bitemporalen Modellierung ausführlich behandelt wird.
| Ansatz | Überlappungsschutz | Datenbank | Aufwand |
|---|---|---|---|
| Zwei DATE-Spalten ohne Constraint | Keiner, muss die Anwendung sicherstellen | Alle relationalen Datenbanken | Gering, aber fehleranfällig |
| Exclusion Constraint + Range-Typ | Von der Datenbank garantiert | PostgreSQL | Mittel, sehr robust |
| Trigger-basierte Prüfung | Von der Datenbank garantiert | MySQL, SQL Server, Oracle | Höher, mehr Wartungsaufwand |
| Anwendungsseitige Prüfung mit Sperre | Nur so robust wie der Code | Datenbankunabhängig | Mittel, Race-Condition-Risiko |
| Gefilterter Index auf aktuelle Zeilen | Kein Schutz, nur Performance | SQL Server, PostgreSQL | Gering, reine Optimierung |
Mironsoft
Datenbank-Optimierung, Query-Tuning und Migrationen
SQL-Abfragen, die bei Wachstum immer langsamer werden?
Wir analysieren und optimieren SQL-Datenbanken unabhängig vom eingesetzten System, planen sichere Migrationen und Schema-Änderungen und bringen Teams Query-Optimierung praxisnah bei.
Query-Optimierung
Langsame Abfragen analysieren und mit Indizes und Explain-Plänen gezielt beschleunigen.
Migrations-Planung
Schema-Änderungen und Datenmigrationen sicher und ohne Downtime umsetzen.
Team-Schulung
SQL-Grundlagen und Performance-Denken praxisnah im Entwicklerteam verankern.
10. Zusammenfassung
Effective Dating: das Wichtigste auf einen Blick
Grundprinzip
gültig_von und gültig_bis werden bewusst von der Fachlogik gesetzt, nicht automatisch.
Konvention
Halboffene Intervalle [von, bis) vermeiden Off-by-one-Fehler an den Grenzen.
Bester Schutz
Exclusion Constraints auf Range-Typen verhindern Überlappungen in PostgreSQL zuverlässig.
Abgrenzung
Fachlich gesteuert statt automatisch, ergänzt sich aber gut mit System-Versioning.