Warum das Datenmodell über die SQL-Qualität entscheidet
Viele Fehler in zeitbezogenen SQL-Abfragen entstehen nicht erst bei der Formulierung der Abfrage. Ihre Ursache liegt bereits im Datenmodell.
Wenn Beginn und Ende einer Gültigkeit uneinheitlich gespeichert werden, offene Zeiträume unterschiedlich dargestellt sind oder mehrere Zeilen gleichzeitig für denselben Sachverhalt gelten können, muss jede spätere Abfrage diese Unklarheiten erneut behandeln.
Das führt häufig zu langen Bedingungen, schwer verständlichen Sonderfällen und widersprüchlichen Ergebnissen.
Ein sauberes Zeitmodell beantwortet mindestens folgende Fragen:
- Ab welchem Zeitpunkt gilt ein Datensatz?
- Bis zu welchem Zeitpunkt gilt er?
- Ist die obere Grenze enthalten oder ausgeschlossen?
- Wie wird eine unbegrenzte Gültigkeit dargestellt?
- Dürfen mehrere Datensätze gleichzeitig gültig sein?
- Müssen aufeinanderfolgende Zeiträume lückenlos sein?
- Darf sich ein Wert rückwirkend ändern?
- Wird eine fachliche Historie oder nur der aktuelle Zustand gespeichert?
- Werden reine Kalendertage oder genaue Zeitpunkte betrachtet?
- Welche Zeitzone gilt?
Diese Entscheidungen gehören zur fachlichen Definition des Datenbestands. Sie dürfen nicht bei jeder einzelnen Abfrage neu interpretiert werden.
Eine robuste SQL-Lösung beginnt deshalb mit einem konsistenten Verständnis der gespeicherten Zeiträume.
Ein generisches Historienmodell
Für die folgenden Beispiele verwenden wir ein bewusst neutrales Datenmodell.
Die Tabelle OBJEKT enthält fachliche Objekte. Ein Objekt kann beispielsweise ein Vertrag, ein technisches Element, ein Produkt, eine Zuordnung oder ein organisatorischer Datensatz sein.
CREATE TABLE objekt (
objekt_id NUMBER NOT NULL,
objekt_name VARCHAR2(100) NOT NULL,
aktiv_flag CHAR(1) DEFAULT 'J' NOT NULL,
CONSTRAINT pk_objekt
PRIMARY KEY (objekt_id),
CONSTRAINT ck_objekt_aktiv
CHECK (aktiv_flag IN ('J', 'N'))
);
Die zeitabhängigen Eigenschaften werden nicht direkt in dieser Tabelle gespeichert. Stattdessen existiert eine eigene Historientabelle.
CREATE TABLE objekt_status (
status_id NUMBER NOT NULL,
objekt_id NUMBER NOT NULL,
status_code VARCHAR2(30) NOT NULL,
gueltig_von TIMESTAMP NOT NULL,
gueltig_bis TIMESTAMP,
CONSTRAINT pk_objekt_status
PRIMARY KEY (status_id),
CONSTRAINT fk_status_objekt
FOREIGN KEY (objekt_id)
REFERENCES objekt (objekt_id),
CONSTRAINT ck_status_zeitraum
CHECK (
gueltig_bis IS NULL
OR gueltig_bis > gueltig_von
)
);
Die Tabelle OBJEKT_STATUS beschreibt, welcher Status für ein Objekt innerhalb eines bestimmten Zeitraums gilt.
Ein Beispieldatensatz könnte folgendermaßen aussehen:
| STATUS_ID | OBJEKT_ID | STATUS_CODE | GUELTIG_VON | GUELTIG_BIS |
|---|---|---|---|---|
| 1001 | 10 | NEU | 01.01.2026 00:00 | 15.01.2026 00:00 |
| 1002 | 10 | AKTIV | 15.01.2026 00:00 | 01.06.2026 00:00 |
| 1003 | 10 | GESPERRT | 01.06.2026 00:00 | NULL |
Das Beispiel verwendet ein exklusives Enddatum.
Der erste Datensatz gilt somit ab dem 1. Januar 2026 einschließlich bis zum 15. Januar 2026 ausschließlich. Genau ab dem 15. Januar gilt der zweite Datensatz.
Mathematisch ergeben sich folgende Intervalle:
[01.01.2026 00:00, 15.01.2026 00:00)
[15.01.2026 00:00, 01.06.2026 00:00)
[01.06.2026 00:00, unbegrenzt)
Dieses Modell erlaubt einen direkten Anschluss der Zeiträume. Zwischen dem Ende eines Eintrags und dem Beginn des nächsten Eintrags entsteht weder eine Lücke noch eine Überschneidung.
Fachliche Historie und technische Historie
Bei Historientabellen müssen zwei unterschiedliche Arten von Zeit unterschieden werden.
Fachliche Gültigkeit
Die fachliche Gültigkeit beschreibt, wann ein Wert in der realen oder fachlichen Welt gilt.
Typische Spalten sind:
GUELTIG_VON
GUELTIG_BIS
Beispiel:
Ein Status gilt fachlich vom 1. März 2026 bis zum 1. Juli 2026.
Technische Speicherung
Die technische Historie beschreibt, wann ein Datensatz im System angelegt, geändert oder ersetzt wurde.
Typische Spalten sind:
ERSTELLT_AM
GEAENDERT_AM
ERFASST_VON
VERSION
Ein Datensatz kann beispielsweise am 10. März angelegt werden, obwohl seine fachliche Gültigkeit bereits am 1. März begonnen hat.
Diese beiden Dimensionen dürfen nicht verwechselt werden.
Ein mögliches erweitertes Modell lautet:
CREATE TABLE objekt_status (
status_id NUMBER NOT NULL,
objekt_id NUMBER NOT NULL,
status_code VARCHAR2(30) NOT NULL,
gueltig_von TIMESTAMP NOT NULL,
gueltig_bis TIMESTAMP,
erstellt_am TIMESTAMP DEFAULT SYSTIMESTAMP NOT NULL,
geaendert_am TIMESTAMP,
version_nr NUMBER DEFAULT 1 NOT NULL,
CONSTRAINT pk_objekt_status
PRIMARY KEY (status_id),
CONSTRAINT fk_status_objekt
FOREIGN KEY (objekt_id)
REFERENCES objekt (objekt_id),
CONSTRAINT ck_status_zeitraum
CHECK (
gueltig_bis IS NULL
OR gueltig_bis > gueltig_von
)
);
Die Abfrage des fachlich gültigen Status muss sich an GUELTIG_VON und GUELTIG_BIS orientieren, nicht an ERSTELLT_AM.
Eine häufig fehlerhafte Abfrage wäre:
SELECT *
FROM objekt_status
WHERE erstellt_am <= SYSTIMESTAMP;
Diese Bedingung sagt lediglich, dass der Datensatz bereits im System gespeichert wurde.
Sie sagt nicht, ob er fachlich aktuell gültig ist.
Das empfohlene Intervallmodell
Für die meisten technischen und fachlichen Zeitmodelle ist ein links geschlossenes und rechts offenes Intervall besonders geeignet.
Die Darstellung lautet:
[GUELTIG_VON, GUELTIG_BIS)
Das bedeutet:
GUELTIG_VONist enthalten.GUELTIG_BISist nicht enthalten.
Ein Zeitpunkt p_zeitpunkt liegt innerhalb des Zeitraums, wenn:
p_zeitpunkt >= gueltig_von
AND p_zeitpunkt < gueltig_bis
Bei einem offenen Ende muss die Bedingung erweitert werden:
p_zeitpunkt >= gueltig_von
AND (
gueltig_bis IS NULL
OR p_zeitpunkt < gueltig_bis
)
Diese Darstellung hat mehrere Vorteile.
Direkte Anschlussfähigkeit
Zwei Zeiträume können exakt aneinander anschließen:
A: [01.01.2026, 01.02.2026)
B: [01.02.2026, 01.03.2026)
Der 1. Februar gehört ausschließlich zu Zeitraum B.
Keine künstlichen Endzeitpunkte
Es ist nicht erforderlich, Werte wie 23:59:59 zu speichern.
Unterstützung genauer Zeitstempel
Das Modell funktioniert unabhängig davon, ob mit Tagen, Sekunden, Millisekunden oder feineren Zeitauflösungen gearbeitet wird.
Eindeutige Vergleichsoperatoren
Für den Beginn wird regelmäßig >= verwendet, für das Ende <.
Dadurch werden Grenzfälle leichter nachvollziehbar.
Warum 23:59:59 problematisch ist
In vielen älteren Datenmodellen wird ein vollständiger Tag so dargestellt:
GUELTIG_VON = 01.04.2026 00:00:00
GUELTIG_BIS = 30.04.2026 23:59:59
Diese Darstellung wirkt zunächst verständlich. Sie enthält jedoch mehrere Probleme.
Problem 1: Höhere Zeitauflösung
Ein TIMESTAMP kann Nachkommastellen enthalten.
Der Zeitpunkt
30.04.2026 23:59:59.500
liegt nach 23:59:59 und wäre deshalb nicht mehr abgedeckt.
Problem 2: Unklare Anschlusslogik
Der nächste Zeitraum beginnt möglicherweise am:
01.05.2026 00:00:00
Zwischen beiden Werten liegt zwar nur ein sehr kleiner technischer Abstand, logisch handelt es sich dennoch nicht um direkt identische Grenzen.
Problem 3: Abhängigkeit vom Datentyp
Bei Oracle DATE beträgt die kleinste speicherbare Einheit eine Sekunde. Bei TIMESTAMP können Bruchteile einer Sekunde gespeichert werden.
Eine Bedingung, die bei DATE scheinbar korrekt funktioniert, kann nach einer Umstellung auf TIMESTAMP falsche Ergebnisse liefern.
Problem 4: Schwierige Erweiterbarkeit
Wenn später Messwerte mit höherer Genauigkeit verarbeitet werden, muss das gesamte Zeitmodell überprüft werden.
Die robustere Darstellung lautet deshalb:
GUELTIG_VON = 01.04.2026 00:00:00
GUELTIG_BIS = 01.05.2026 00:00:00
Das Ende ist exklusiv.
Der gesamte April ist damit vollständig abgedeckt.
Oracle DATE und TIMESTAMP
Oracle verwendet für Zeitwerte hauptsächlich die Datentypen DATE und TIMESTAMP.
DATE
Der Oracle-Datentyp DATE speichert:
- Jahr
- Monat
- Tag
- Stunde
- Minute
- Sekunde
Ein Oracle-DATE ist daher nicht nur ein Kalendertag.
Die Bezeichnung kann irreführend sein, weil ein Wert wie
DATE '2026-04-01'
dem Zeitpunkt
01.04.2026 00:00:00
entspricht.
Ein Vergleich wie
gueltig_von = DATE '2026-04-01'
findet nur Datensätze, deren Zeitanteil exakt 00:00:00 beträgt.
Ein Wert wie
01.04.2026 08:15:00
wird nicht gefunden.
TIMESTAMP
TIMESTAMP speichert zusätzlich Bruchteile einer Sekunde.
Beispiel:
01.04.2026 08:15:27.348921
Für viele fachliche Historientabellen reicht DATE aus. Für Ereignisse, Protokolle, Messungen, technische Abläufe und hochauflösende Zeitangaben ist TIMESTAMP häufig geeigneter.
Die Wahl sollte bewusst erfolgen.
Entscheidend ist nicht nur die aktuell benötigte Genauigkeit. Auch die spätere Nutzung des Datenbestands muss berücksichtigt werden.
Datumswerte korrekt als Literale schreiben
Datumswerte sollten in SQL nicht von Sitzungseinstellungen abhängig sein.
Eine problematische Schreibweise lautet:
WHERE gueltig_von = '01.04.2026'
Oracle muss die Zeichenkette implizit in ein Datum umwandeln. Das Ergebnis hängt von der aktuellen NLS_DATE_FORMAT-Einstellung ab.
In einer Sitzung kann die Abfrage funktionieren, in einer anderen kann sie fehlschlagen oder einen anderen Wert interpretieren.
Für reine Datumswerte ist das ANSI-Datumsliteral vorzuziehen:
WHERE gueltig_von = DATE '2026-04-01'
Für Zeitstempel kann ein TIMESTAMP-Literal verwendet werden:
WHERE gueltig_von = TIMESTAMP '2026-04-01 08:30:00'
Wenn eine Zeichenkette umgewandelt werden muss, sollte das Format ausdrücklich angegeben werden:
TO_DATE('01.04.2026', 'DD.MM.YYYY')
beziehungsweise:
TO_TIMESTAMP(
'01.04.2026 08:30:15.125',
'DD.MM.YYYY HH24:MI:SS.FF3'
)
Die explizite Umwandlung macht die erwartete Struktur sichtbar und verhindert eine Abhängigkeit von Sitzungseinstellungen.
Offene Zeiträume mit NULL modellieren
Ein aktuell gültiger oder unbegrenzt gültiger Datensatz besitzt häufig kein festgelegtes Enddatum.
Eine übliche Darstellung lautet:
GUELTIG_BIS = NULL
Beispiel:
| STATUS_CODE | GUELTIG_VON | GUELTIG_BIS |
| AKTIV | 01.01.2026 00:00 | NULL |
Der Datensatz gilt ab dem 1. Januar 2026 ohne bekannte obere Grenze.
Für die Prüfung eines Zeitpunkts wird folgende Bedingung benötigt:
WHERE :p_zeitpunkt >= gueltig_von
AND (
gueltig_bis IS NULL
OR :p_zeitpunkt < gueltig_bis
)
Alternativ kann das offene Ende innerhalb der Abfrage durch einen weit in der Zukunft liegenden Wert ersetzt werden:
WHERE :p_zeitpunkt >= gueltig_von
AND :p_zeitpunkt < COALESCE(
gueltig_bis,
TIMESTAMP '9999-12-31 00:00:00'
)
Beide Varianten können fachlich gleichwertig sein.
Die erste Variante bildet die Datenstruktur unmittelbar ab.
Die zweite Variante erleichtert teilweise die Wiederverwendung allgemeiner Intervallformeln.
NULL ist kein beliebiger großer Wert
NULL bedeutet in SQL nicht automatisch „unendlich“.
NULL bedeutet, dass kein Wert vorhanden oder bekannt ist.
Deshalb ergibt die Bedingung
:p_zeitpunkt < gueltig_bis
bei einem NULL-Ende nicht TRUE, sondern UNKNOWN.
In einer WHERE-Klausel werden nur Zeilen berücksichtigt, für die die Bedingung TRUE ergibt. Ein offener Datensatz würde deshalb ohne zusätzliche Behandlung ausgeschlossen.
Folgende Abfrage ist unvollständig:
SELECT *
FROM objekt_status
WHERE :p_zeitpunkt >= gueltig_von
AND :p_zeitpunkt < gueltig_bis;
Sie findet keine Zeilen mit GUELTIG_BIS IS NULL.
Die korrekte Variante lautet:
SELECT *
FROM objekt_status
WHERE :p_zeitpunkt >= gueltig_von
AND (
gueltig_bis IS NULL
OR :p_zeitpunkt < gueltig_bis
);
Diese Besonderheit betrifft alle Vergleiche mit NULL.
Auch folgende Bedingungen funktionieren nicht wie häufig angenommen:
gueltig_bis = NULL
gueltig_bis <> NULL
Für die Prüfung werden ausschließlich IS NULL und IS NOT NULL verwendet:
gueltig_bis IS NULL
gueltig_bis IS NOT NULL
NULL oder technisches Maximaldatum
Statt NULL verwenden manche Systeme ein festes Maximaldatum.
Beispiel:
31.12.9999 00:00:00
oder:
01.01.4000 00:00:00
Ein offener Datensatz könnte dann so gespeichert sein:
| STATUS_CODE | GUELTIG_VON | GUELTIG_BIS |
| AKTIV | 01.01.2026 | 31.12.9999 |
Dieses Modell hat Vor- und Nachteile.
Vorteile eines Maximaldatums
Allgemeine Vergleiche funktionieren ohne zusätzliche NULL-Behandlung:
:p_zeitpunkt >= gueltig_von
AND :p_zeitpunkt < gueltig_bis
Auch Sortierungen und analytische Funktionen können einfacher sein.
Nachteile eines Maximaldatums
Der Wert ist fachlich kein echtes Ende.
Jede Anwendung muss wissen, dass das Datum lediglich eine technische Bedeutung besitzt.
Außerdem können unterschiedliche Maximalwerte verwendet werden:
31.12.2999
01.01.4000
31.12.9999
Wenn mehrere Konventionen existieren, entstehen erneut Sonderfälle.
Vorteile von NULL
NULL macht sichtbar, dass kein Endzeitpunkt festgelegt ist.
Die Daten enthalten keinen künstlichen Wert.
Nachteile von NULL
Jede zeitliche Prüfung muss offene Enden berücksichtigen.
Ein Wechsel zwischen beiden Modellen sollte nicht beiläufig erfolgen. Innerhalb einer fachlich zusammengehörigen Datenstruktur sollte nur eine Konvention gelten.
Für neue Datenmodelle ist NULL häufig verständlicher, sofern die Abfragen konsequent damit umgehen.
Aktuell gültige Datensätze ermitteln
Ein Datensatz ist aktuell gültig, wenn der aktuelle Zeitpunkt innerhalb seines Gültigkeitsintervalls liegt.
Bei einem offenen Ende lautet die Abfrage:
SELECT
status_id,
objekt_id,
status_code,
gueltig_von,
gueltig_bis
FROM objekt_status
WHERE SYSTIMESTAMP >= gueltig_von
AND (
gueltig_bis IS NULL
OR SYSTIMESTAMP < gueltig_bis
);
Wenn die Spalten vom Typ DATE sind, kann SYSDATE verwendet werden:
SELECT
status_id,
objekt_id,
status_code,
gueltig_von,
gueltig_bis
FROM objekt_status
WHERE SYSDATE >= gueltig_von
AND (
gueltig_bis IS NULL
OR SYSDATE < gueltig_bis
);
Diese beiden Varianten sind nicht vollständig gleichbedeutend.
SYSDATE liefert einen Wert vom Typ DATE.
SYSTIMESTAMP liefert einen Zeitstempel mit Zeitzoneninformation der Datenbankumgebung.
Datentyp und fachliche Zeitzone sollten deshalb zusammenpassen.
Gültigkeit zu einem beliebigen Stichtag
Fachliche Prüfungen sollten nach Möglichkeit nicht fest an das aktuelle Datum gebunden werden.
Eine Abfrage mit einem Parameter ist wiederverwendbarer:
SELECT
s.status_id,
s.objekt_id,
s.status_code
FROM objekt_status s
WHERE :p_stichtag >= s.gueltig_von
AND (
s.gueltig_bis IS NULL
OR :p_stichtag < s.gueltig_bis
);
Der Parameter :p_stichtag kann beispielsweise enthalten:
15.05.2026 12:00:00
Dieses Vorgehen erleichtert:
- historische Auswertungen,
- Tests,
- Simulationen,
- Rückrechnungen,
- Stichtagsberichte,
- fachliche Nachweise.
Eine Abfrage, die ausschließlich SYSDATE verwendet, kann nur den aktuellen Zustand betrachten. Für reproduzierbare Ergebnisse ist ein übergebener Stichtag meist besser geeignet.
Datensätze innerhalb eines Suchzeitraums finden
Eine andere Fragestellung lautet:
Welche Historieneinträge überschneiden sich mit einem vorgegebenen Suchzeitraum?
Der Suchzeitraum sei:
[:p_von, :p_bis)
Ein Datensatz überschneidet diesen Zeitraum, wenn:
gueltig_von < :p_bis
AND (
gueltig_bis IS NULL
OR gueltig_bis > :p_von
)
Die vollständige Abfrage lautet:
SELECT
s.status_id,
s.objekt_id,
s.status_code,
s.gueltig_von,
s.gueltig_bis
FROM objekt_status s
WHERE s.gueltig_von < :p_bis
AND (
s.gueltig_bis IS NULL
OR s.gueltig_bis > :p_von
);
Diese Bedingung berücksichtigt mehrere mögliche Überlappungsarten:
- Der Datensatz beginnt innerhalb des Suchzeitraums.
- Der Datensatz endet innerhalb des Suchzeitraums.
- Der Datensatz umschließt den Suchzeitraum vollständig.
- Der Suchzeitraum umschließt den Datensatz vollständig.
- Der Datensatz besitzt kein Enddatum.
Eine Abfrage mit BETWEEN deckt diese Fälle meist nicht vollständig ab.
BETWEEN bei Zeiträumen
BETWEEN ist in SQL an beiden Grenzen inklusive.
Die Bedingung
:p_stichtag BETWEEN gueltig_von AND gueltig_bis
entspricht:
:p_stichtag >= gueltig_von
AND :p_stichtag <= gueltig_bis
Damit wird das Enddatum eingeschlossen.
Bei einem exklusiven Zeitmodell ist diese Bedingung fachlich falsch.
Zusätzlich behandelt BETWEEN offene Enden nicht. Wenn GUELTIG_BIS den Wert NULL besitzt, ergibt der Vergleich kein TRUE.
Für Historientabellen ist eine ausgeschriebene Bedingung meist klarer:
:p_stichtag >= gueltig_von
AND (
gueltig_bis IS NULL
OR :p_stichtag < gueltig_bis
)
BETWEEN ist nicht grundsätzlich ungeeignet. Es muss jedoch zur Intervallsemantik passen.
Bei Zeitstempeln ist seine inklusive obere Grenze häufig unerwünscht.
Ein Suchzeitraum muss vollständig abgedeckt sein
Eine anspruchsvollere Anforderung lautet:
Der vorgegebene Zeitraum muss durch einen oder mehrere Historieneinträge vollständig abgedeckt sein.
Ein einfacher EXISTS-Nachweis reicht hierfür nicht aus.
Betrachten wir folgende Daten:
| STATUS_CODE | GUELTIG_VON | GUELTIG_BIS |
| AKTIV | 01.04.2026 | 10.04.2026 |
| AKTIV | 15.04.2026 | 01.05.2026 |
Beide Datensätze überschneiden den April.
Der Zeitraum vom 10. April bis zum 15. April ist jedoch nicht abgedeckt.
Eine reine Überlappungsabfrage würde beide Zeilen liefern, aber die Lücke nicht erkennen.
Für eine vollständige Abdeckungsprüfung werden typischerweise mehrere Aspekte geprüft:
- Der Suchzeitraum beginnt innerhalb eines passenden Datensatzes.
- Der Suchzeitraum endet innerhalb eines passenden Datensatzes.
- Zwischen den relevanten Datensätzen gibt es keine Lücke.
- Gegebenenfalls besitzen alle Abschnitte den zulässigen Typ.
- Überschneidungen werden fachlich korrekt behandelt.
Die vollständige Lösung wird in einem späteren Kapitel mit analytischen Funktionen aufgebaut.
Bereits an dieser Stelle ist jedoch wichtig:
Überlappung und Abdeckung sind unterschiedliche fachliche Konzepte.
Mehrere aktuell gültige Datensätze erkennen
Wenn pro Objekt nur ein Status gleichzeitig gültig sein darf, kann eine Kontrollabfrage aktuelle Mehrfachgültigkeiten erkennen.
SELECT
objekt_id,
COUNT(*) AS anzahl_aktuelle_status
FROM objekt_status
WHERE SYSTIMESTAMP >= gueltig_von
AND (
gueltig_bis IS NULL
OR SYSTIMESTAMP < gueltig_bis
)
GROUP BY objekt_id
HAVING COUNT(*) > 1;
Diese Abfrage prüft nur den aktuellen Zeitpunkt.
Sie erkennt nicht, ob in der Vergangenheit oder Zukunft Überschneidungen vorhanden sind.
Für eine vollständige historische Prüfung müssen die Datensätze derselben Objekt-ID miteinander verglichen werden.
Eine mögliche Grundstruktur lautet:
SELECT
a.objekt_id,
a.status_id AS status_id_a,
b.status_id AS status_id_b,
a.gueltig_von AS a_von,
a.gueltig_bis AS a_bis,
b.gueltig_von AS b_von,
b.gueltig_bis AS b_bis
FROM objekt_status a
JOIN objekt_status b
ON b.objekt_id = a.objekt_id
AND b.status_id > a.status_id
AND a.gueltig_von < COALESCE(
b.gueltig_bis,
TIMESTAMP '9999-12-31 00:00:00'
)
AND b.gueltig_von < COALESCE(
a.gueltig_bis,
TIMESTAMP '9999-12-31 00:00:00'
);
Die Bedingung
b.status_id > a.status_id
verhindert:
- den Vergleich einer Zeile mit sich selbst,
- die doppelte Ausgabe derselben Paarung.
Ohne diese Einschränkung würden sowohl die Paarung A/B als auch B/A ausgegeben.
Ein aktueller Datensatz ist nicht automatisch der neueste Datensatz
Ein häufiger Fehler besteht darin, den Datensatz mit dem höchsten Datum oder der höchsten ID als aktuell gültig anzusehen.
Beispiel:
| STATUS_ID | STATUS_CODE | GUELTIG_VON | GUELTIG_BIS |
| 100 | AKTIV | 01.01.2026 | NULL |
| 101 | GESPERRT | 01.01.2027 | NULL |
Am 1. Juni 2026 ist Datensatz 100 gültig.
Datensatz 101 besitzt zwar die höhere ID und den späteren Beginn, gilt aber erst in der Zukunft.
Folgende Abfrage wäre deshalb fachlich falsch:
SELECT *
FROM objekt_status
WHERE status_id = (
SELECT MAX(status_id)
FROM objekt_status
);
Auch diese Variante ist nicht ausreichend:
SELECT *
FROM objekt_status
WHERE gueltig_von = (
SELECT MAX(gueltig_von)
FROM objekt_status
);
Sie findet den zeitlich spätesten Beginn, nicht den am Stichtag gültigen Datensatz.
Die korrekte Reihenfolge lautet:
- Zuerst auf den gewünschten Stichtag einschränken.
- Nur innerhalb der gültigen Treffer eine Priorisierung oder Eindeutigkeitsprüfung durchführen.
Beispiel:
SELECT *
FROM objekt_status
WHERE :p_stichtag >= gueltig_von
AND (
gueltig_bis IS NULL
OR :p_stichtag < gueltig_bis
);
Wenn diese Abfrage mehrere Zeilen pro Objekt liefert, liegt entweder eine zulässige Mehrfachgültigkeit oder ein Datenqualitätsproblem vor.
Typische Modellierungsfehler
Fehler 1: Unklare Endgrenze
Es ist nicht dokumentiert, ob GUELTIG_BIS inklusive oder exklusiv gilt.
Folge:
Verschiedene Abfragen verwenden unterschiedliche Operatoren.
Fehler 2: Gemischte offene Enden
Einige Zeilen verwenden NULL, andere ein Maximaldatum.
Folge:
Jede Abfrage muss beide Varianten behandeln.
Fehler 3: Zeitanteile werden ignoriert
Die Anwendung zeigt nur das Datum an, speichert aber Uhrzeiten.
Folge:
Gleichheitsvergleiche und Tagesabfragen liefern unerwartete Ergebnisse.
Fehler 4: Fachliche und technische Zeit werden vermischt
ERSTELLT_AM wird als fachlicher Beginn interpretiert.
Folge:
Rückwirkende Änderungen können nicht korrekt ausgewertet werden.
Fehler 5: Historie ohne Eindeutigkeitsregel
Es ist nicht definiert, ob mehrere Werte gleichzeitig gültig sein dürfen.
Folge:
Mehrfachtreffer werden je nach Abfrage zufällig oder widersprüchlich behandelt.
Fehler 6: Direkte Aktualisierung ohne Nachbarprüfung
Ein Zeitraum wird geändert, ohne angrenzende Einträge zu prüfen.
Folge:
Lücken oder Überschneidungen entstehen.
Fehler 7: Maximaldatum wird wie ein echtes Datum behandelt
Ein technischer Platzhalter erscheint in Berichten als fachliches Enddatum.
Folge:
Benutzer interpretieren eine künstliche Grenze als reale Information.
Fehler 8: Aktuell wird mit maximal verwechselt
Der Datensatz mit dem höchsten Beginn oder der höchsten ID wird als aktuell verwendet.
Folge:
Zukünftige oder bereits abgelaufene Einträge werden falsch ausgewählt.
Zusammenfassung
Ein belastbares Zeitmodell ist die Grundlage jeder korrekten Historienabfrage.
Die wichtigsten Regeln dieses Kapitels lauten:
- Fachliche Gültigkeit und technische Speicherung müssen getrennt betrachtet werden.
- Für viele Anwendungen ist das Intervallmodell
[VON, BIS)besonders robust. - Bei diesem Modell ist der Beginn inklusive und das Ende exklusiv.
- Offene Enden können mit
NULLoder einem technischen Maximaldatum dargestellt werden. - Innerhalb eines Modells sollte nur eine Konvention verwendet werden.
- Oracle
DATEenthält immer auch eine Uhrzeit. TIMESTAMPunterstützt zusätzlich Sekundenbruchteile.- Datumswerte sollten nicht durch implizite Zeichenkettenkonvertierung erzeugt werden.
- Ein Stichtag liegt in einem offenen Zeitraum, wenn:
:p_stichtag >= gueltig_von
AND (
gueltig_bis IS NULL
OR :p_stichtag < gueltig_bis
)
- Ein Datensatz überschneidet einen Suchzeitraum, wenn:
gueltig_von < :p_bis
AND (
gueltig_bis IS NULL
OR gueltig_bis > :p_von
)
- Überlappung ist nicht dasselbe wie vollständige Abdeckung.
- Der neueste Datensatz ist nicht automatisch der aktuell gültige Datensatz.
- Check Constraints können einzelne Zeiträume prüfen, aber keine vollständige zeitliche Konsistenz zwischen mehreren Zeilen garantieren.
- Überschneidungen, Lücken und mehrere offene Datensätze benötigen eigene Prüfungen.
- Änderungen an Historien müssen benachbarte Zeiträume berücksichtigen.
- Zeitlogik muss mit gezielt konstruierten Grenzfällen getestet werden.
- Intervallsemantik, Zeitzone, offene Enden und Eindeutigkeitsregeln müssen ausdrücklich dokumentiert sein.