Monatsreihen zwischen zwei Datumswerten erzeugen

Für Auswertungen werden häufig alle Monate innerhalb eines Zeitraums benötigt, auch wenn für einzelne Monate keine Geschäftsdaten vorhanden sind.

Typische Anwendungsfälle sind:

  • Monatsberichte
  • Soll-Ist-Vergleiche
  • Umsatzübersichten
  • lückenlose Zeitachsen
  • Prüfung fehlender Monatsdaten
  • Aufteilung eines Zeitraums in einzelne Monatsabschnitte

In Oracle SQL kann eine Monatsreihe mit ADD_MONTHS, MONTHS_BETWEEN, TRUNC und CONNECT BY LEVEL erzeugt werden.

Monate eines Kalenderjahres erzeugen

Die folgende Abfrage erzeugt alle zwölf Monate des Jahres 2026:

SELECT
    ADD_MONTHS(
        DATE '2026-01-01',
        LEVEL - 1
    ) AS monatsbeginn
FROM dual
CONNECT BY LEVEL <= 12;

Ergebnis:

MONATSBEGINN
01.01.2026
01.02.2026
01.03.2026
01.04.2026
01.05.2026
01.06.2026
01.07.2026
01.08.2026
01.09.2026
01.10.2026
01.11.2026
01.12.2026

LEVEL beginnt bei 1.

Deshalb wird für den ersten Monat gerechnet:

LEVEL - 1

Beim ersten Datensatz ergibt das:

1 - 1 = 0 Monate

Beim zweiten Datensatz:

2 - 1 = 1 Monat

ADD_MONTHS addiert die jeweilige Anzahl von Monaten zum Ausgangsdatum.

Monatsreihe zwischen zwei festen Datumswerten

Die folgende Abfrage erzeugt alle Monate von März bis August 2026:

WITH parameter AS (
    SELECT
        DATE '2026-03-15' AS von_datum,
        DATE '2026-08-20' AS bis_datum
    FROM dual
)
SELECT
    ADD_MONTHS(
        TRUNC(von_datum, 'MM'),
        LEVEL - 1
    ) AS monatsbeginn
FROM parameter
WHERE bis_datum >= von_datum
CONNECT BY LEVEL <=
    MONTHS_BETWEEN(
        TRUNC(bis_datum, 'MM'),
        TRUNC(von_datum, 'MM')
    ) + 1;

Ergebnis:

MONATSBEGINN
01.03.2026
01.04.2026
01.05.2026
01.06.2026
01.07.2026
01.08.2026

Obwohl das Anfangsdatum auf den 15. März und das Enddatum auf den 20. August fällt, wird jeweils der vollständige Kalendermonat berücksichtigt.

Dafür sorgt:

TRUNC(datum, 'MM')

Diese Funktion setzt ein Datum auf den ersten Tag des jeweiligen Monats.

Anzahl der benötigten Monate berechnen

Der Ausdruck:

MONTHS_BETWEEN(
    TRUNC(bis_datum, 'MM'),
    TRUNC(von_datum, 'MM')
) + 1

berechnet die Anzahl der auszugebenden Monate.

Für März bis August ergibt sich:

August minus März = 5 Monate
zuzüglich Ausgangsmonat = 6 Monate

Das + 1 ist erforderlich, weil sowohl der Anfangsmonat als auch der Endmonat ausgegeben werden sollen.

Ohne + 1 würde der Endmonat fehlen.

Bind-Variablen verwenden

In einer Anwendung oder einem SQL-Werkzeug werden meistens keine festen Datumsliterale, sondern Bind-Variablen verwendet:

WITH parameter AS (
    SELECT
        TRUNC(:von_datum, 'MM') AS von_monat,
        TRUNC(:bis_datum, 'MM') AS bis_monat
    FROM dual
)
SELECT
    ADD_MONTHS(
        von_monat,
        LEVEL - 1
    ) AS monatsbeginn
FROM parameter
WHERE bis_monat >= von_monat
CONNECT BY LEVEL <=
    MONTHS_BETWEEN(
        bis_monat,
        von_monat
    ) + 1;

Beispielwerte:

:von_datum = 15.03.2026
:bis_datum = 20.08.2026

Das Ergebnis bleibt eine Monatsreihe von März bis August 2026.

Monatsbeginn und Monatsende ausgeben

Neben dem Monatsbeginn kann auch der letzte Kalendertag ausgegeben werden:

WITH parameter AS (
    SELECT
        DATE '2026-03-15' AS von_datum,
        DATE '2026-08-20' AS bis_datum
    FROM dual
)
SELECT
    ADD_MONTHS(
        TRUNC(von_datum, 'MM'),
        LEVEL - 1
    ) AS monatsbeginn,

    LAST_DAY(
        ADD_MONTHS(
            TRUNC(von_datum, 'MM'),
            LEVEL - 1
        )
    ) AS monatsende
FROM parameter
WHERE bis_datum >= von_datum
CONNECT BY LEVEL <=
    MONTHS_BETWEEN(
        TRUNC(bis_datum, 'MM'),
        TRUNC(von_datum, 'MM')
    ) + 1;

Ergebnis:

MONATSBEGINNMONATSENDE
01.03.202631.03.2026
01.04.202630.04.2026
01.05.202631.05.2026
01.06.202630.06.2026
01.07.202631.07.2026
01.08.202631.08.2026

LAST_DAY ermittelt den letzten Kalendertag des jeweiligen Monats.

Exklusive Monatsgrenze verwenden

Für Datenbankabfragen ist eine exklusive Obergrenze meist sicherer als ein künstlich erzeugtes Monatsende mit Uhrzeit.

Statt:

datum BETWEEN monatsbeginn AND monatsende

sollte häufig dieses Muster verwendet werden:

datum >= monatsbeginn
AND datum < naechster_monatsbeginn

Die Monatsreihe kann dafür direkt die exklusive Obergrenze ausgeben:

WITH parameter AS (
    SELECT
        DATE '2026-03-15' AS von_datum,
        DATE '2026-08-20' AS bis_datum
    FROM dual
)
SELECT
    ADD_MONTHS(
        TRUNC(von_datum, 'MM'),
        LEVEL - 1
    ) AS monatsbeginn,

    ADD_MONTHS(
        TRUNC(von_datum, 'MM'),
        LEVEL
    ) AS naechster_monatsbeginn
FROM parameter
WHERE bis_datum >= von_datum
CONNECT BY LEVEL <=
    MONTHS_BETWEEN(
        TRUNC(bis_datum, 'MM'),
        TRUNC(von_datum, 'MM')
    ) + 1;

Ergebnis:

MONATSBEGINNNAECHSTER_MONATSBEGINN
01.03.202601.04.2026
01.04.202601.05.2026
01.05.202601.06.2026
01.06.202601.07.2026
01.07.202601.08.2026
01.08.202601.09.2026

Eine Umsatzabfrage kann anschließend so verknüpft werden:

SELECT
    m.monatsbeginn,
    SUM(u.betrag) AS umsatz
FROM monate m
LEFT JOIN umsatz_daten u
    ON u.buchungsdatum >= m.monatsbeginn
   AND u.buchungsdatum <  m.naechster_monatsbeginn
GROUP BY
    m.monatsbeginn
ORDER BY
    m.monatsbeginn;

Dieses Intervallmodell berücksichtigt auch Uhrzeiten und Nachkommasekunden korrekt.

Teilzeiträume je Monat erzeugen

Ein fachlicher Zeitraum beginnt oder endet nicht immer am Monatsanfang beziehungsweise Monatsende.

Beispiel:

Beginn: 15.03.2026
Ende:   20.05.2026

Gewünscht sind diese Teilzeiträume:

15.03.2026 bis 31.03.2026
01.04.2026 bis 30.04.2026
01.05.2026 bis 20.05.2026

Abfrage:

WITH parameter AS (
    SELECT
        DATE '2026-03-15' AS von_datum,
        DATE '2026-05-20' AS bis_datum
    FROM dual
),
monate AS (
    SELECT
        p.von_datum,
        p.bis_datum,

        ADD_MONTHS(
            TRUNC(p.von_datum, 'MM'),
            LEVEL - 1
        ) AS monatsbeginn,

        ADD_MONTHS(
            TRUNC(p.von_datum, 'MM'),
            LEVEL
        ) AS naechster_monatsbeginn
    FROM parameter p
    WHERE p.bis_datum >= p.von_datum
    CONNECT BY LEVEL <=
        MONTHS_BETWEEN(
            TRUNC(p.bis_datum, 'MM'),
            TRUNC(p.von_datum, 'MM')
        ) + 1
)
SELECT
    GREATEST(
        von_datum,
        monatsbeginn
    ) AS abschnitt_von,

    LEAST(
        bis_datum,
        naechster_monatsbeginn - 1
    ) AS abschnitt_bis
FROM monate
ORDER BY
    abschnitt_von;

Ergebnis:

ABSCHNITT_VONABSCHNITT_BIS
15.03.202631.03.2026
01.04.202630.04.2026
01.05.202620.05.2026

GREATEST verhindert, dass der erste Abschnitt vor dem tatsächlichen Anfangsdatum beginnt.

LEAST verhindert, dass der letzte Abschnitt über das tatsächliche Enddatum hinausgeht.

Exklusive Teilzeiträume bevorzugen

Wenn Uhrzeiten oder TIMESTAMP-Werte verarbeitet werden, ist eine exklusive Obergrenze robuster:

WITH parameter AS (
    SELECT
        TIMESTAMP '2026-03-15 10:30:00' AS von_zeitpunkt,
        TIMESTAMP '2026-05-20 18:00:00' AS bis_zeitpunkt
    FROM dual
),
monate AS (
    SELECT
        p.von_zeitpunkt,
        p.bis_zeitpunkt,

        ADD_MONTHS(
            TRUNC(p.von_zeitpunkt, 'MM'),
            LEVEL - 1
        ) AS monatsbeginn,

        ADD_MONTHS(
            TRUNC(p.von_zeitpunkt, 'MM'),
            LEVEL
        ) AS naechster_monatsbeginn
    FROM parameter p
    WHERE p.bis_zeitpunkt > p.von_zeitpunkt
    CONNECT BY LEVEL <=
        MONTHS_BETWEEN(
            TRUNC(p.bis_zeitpunkt, 'MM'),
            TRUNC(p.von_zeitpunkt, 'MM')
        ) + 1
)
SELECT
    GREATEST(
        von_zeitpunkt,
        monatsbeginn
    ) AS abschnitt_von,

    LEAST(
        bis_zeitpunkt,
        naechster_monatsbeginn
    ) AS abschnitt_bis_exklusiv
FROM monate
ORDER BY
    abschnitt_von;

Die Zeiträume werden dabei als halb offene Intervalle interpretiert:

abschnitt_von <= Zeitpunkt < abschnitt_bis_exklusiv

Damit entstehen keine Überschneidungen zwischen zwei aufeinanderfolgenden Monatsabschnitten.

Fehlende Monate sichtbar machen

Eine erzeugte Monatsreihe kann mit Geschäftsdaten über einen LEFT JOIN verbunden werden.

Dadurch erscheinen auch Monate, für die keine Datensätze vorhanden sind.

WITH parameter AS (
    SELECT
        DATE '2026-01-01' AS von_datum,
        DATE '2026-06-30' AS bis_datum
    FROM dual
),
monate AS (
    SELECT
        ADD_MONTHS(
            TRUNC(von_datum, 'MM'),
            LEVEL - 1
        ) AS monatsbeginn,

        ADD_MONTHS(
            TRUNC(von_datum, 'MM'),
            LEVEL
        ) AS naechster_monatsbeginn
    FROM parameter
    CONNECT BY LEVEL <=
        MONTHS_BETWEEN(
            TRUNC(bis_datum, 'MM'),
            TRUNC(von_datum, 'MM')
        ) + 1
)
SELECT
    m.monatsbeginn,
    NVL(SUM(u.betrag), 0) AS umsatz
FROM monate m
LEFT JOIN umsatz_daten u
    ON u.buchungsdatum >= m.monatsbeginn
   AND u.buchungsdatum <  m.naechster_monatsbeginn
GROUP BY
    m.monatsbeginn
ORDER BY
    m.monatsbeginn;

Ein Monat ohne Umsatzdaten erscheint beispielsweise so:

MONATSBEGINNUMSATZ
01.01.20263200
01.02.20260
01.03.20261800

Ohne Monatsreihe und LEFT JOIN würde der Februar vollständig im Ergebnis fehlen.

Warum DBA_OBJECTS nicht als Zeilengenerator verwendet werden sollte

Ältere SQL-Beispiele erzeugen Reihen häufig über Systemansichten:

SELECT ...
FROM dba_objects
WHERE ROWNUM <= 12;

Diese Lösung ist ungünstig, weil:

  • nicht jeder Benutzer Zugriff auf DBA_OBJECTS besitzt,
  • die Abfrage fachlich nichts mit Datenbankobjekten zu tun hat,
  • das Ergebnis von einer Systemansicht abhängig gemacht wird,
  • die Absicht der Abfrage schlechter erkennbar ist.

Für einfache Zahlen- und Monatsreihen ist dieses Muster klarer:

SELECT LEVEL
FROM dual
CONNECT BY LEVEL <= 12;

Typische Fehler

Der Endmonat fehlt

Falsch:

CONNECT BY LEVEL <=
    MONTHS_BETWEEN(bis_monat, von_monat)

Richtig:

CONNECT BY LEVEL <=
    MONTHS_BETWEEN(bis_monat, von_monat) + 1

Das + 1 berücksichtigt den Ausgangsmonat.

Datumswerte werden nicht auf den Monatsanfang gekürzt

Problematisch:

MONTHS_BETWEEN(
    DATE '2026-08-20',
    DATE '2026-03-15'
)

Das Ergebnis kann einen Dezimalanteil enthalten.

Für eine Monatsreihe sollten beide Werte zuerst auf den Monatsanfang gesetzt werden:

MONTHS_BETWEEN(
    TRUNC(DATE '2026-08-20', 'MM'),
    TRUNC(DATE '2026-03-15', 'MM')
)

BETWEEN wird bei Zeitstempeln verwendet

Problematisch:

buchungszeitpunkt BETWEEN monatsbeginn AND monatsende

Ein Monatsende ohne vollständige Uhrzeit kann Datensätze des letzten Tages ausschließen.

Robuster:

buchungszeitpunkt >= monatsbeginn
AND buchungszeitpunkt < naechster_monatsbeginn

Anfangsdatum liegt nach dem Enddatum

Diese Bedingung verhindert eine fachlich ungültige Monatsreihe:

WHERE bis_datum >= von_datum

In einer Anwendung sollte zusätzlich eine fachliche Validierung erfolgen.

Kurze Zusammenfassung

Monatsanfang:
TRUNC(datum, 'MM')

Monate addieren:
ADD_MONTHS(datum, anzahl)

Anzahl der Monate:
MONTHS_BETWEEN(bis_monat, von_monat) + 1

Monatsende:
LAST_DAY(monatsbeginn)

Exklusive Obergrenze:
ADD_MONTHS(monatsbeginn, 1)

Zeilen erzeugen:
CONNECT BY LEVEL <= anzahl

Empfohlener Zeitvergleich:
zeitpunkt >= monatsbeginn
AND zeitpunkt < naechster_monatsbeginn