Zeiträume und Gültigkeiten sauber modellieren

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_IDOBJEKT_IDSTATUS_CODEGUELTIG_VONGUELTIG_BIS
100110NEU01.01.2026 00:0015.01.2026 00:00
100210AKTIV15.01.2026 00:0001.06.2026 00:00
100310GESPERRT01.06.2026 00:00NULL

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_VON ist enthalten.
  • GUELTIG_BIS ist 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_CODEGUELTIG_VONGUELTIG_BIS
AKTIV01.01.2026 00:00NULL

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_CODEGUELTIG_VONGUELTIG_BIS
AKTIV01.01.202631.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_CODEGUELTIG_VONGUELTIG_BIS
AKTIV01.04.202610.04.2026
AKTIV15.04.202601.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:

  1. Der Suchzeitraum beginnt innerhalb eines passenden Datensatzes.
  2. Der Suchzeitraum endet innerhalb eines passenden Datensatzes.
  3. Zwischen den relevanten Datensätzen gibt es keine Lücke.
  4. Gegebenenfalls besitzen alle Abschnitte den zulässigen Typ.
  5. Ü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_IDSTATUS_CODEGUELTIG_VONGUELTIG_BIS
100AKTIV01.01.2026NULL
101GESPERRT01.01.2027NULL

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:

  1. Zuerst auf den gewünschten Stichtag einschränken.
  2. 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 NULL oder einem technischen Maximaldatum dargestellt werden.
  • Innerhalb eines Modells sollte nur eine Konvention verwendet werden.
  • Oracle DATE enthält immer auch eine Uhrzeit.
  • TIMESTAMP unterstü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.