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:
| MONATSBEGINN | MONATSENDE |
|---|---|
| 01.03.2026 | 31.03.2026 |
| 01.04.2026 | 30.04.2026 |
| 01.05.2026 | 31.05.2026 |
| 01.06.2026 | 30.06.2026 |
| 01.07.2026 | 31.07.2026 |
| 01.08.2026 | 31.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:
| MONATSBEGINN | NAECHSTER_MONATSBEGINN |
|---|---|
| 01.03.2026 | 01.04.2026 |
| 01.04.2026 | 01.05.2026 |
| 01.05.2026 | 01.06.2026 |
| 01.06.2026 | 01.07.2026 |
| 01.07.2026 | 01.08.2026 |
| 01.08.2026 | 01.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_VON | ABSCHNITT_BIS |
|---|---|
| 15.03.2026 | 31.03.2026 |
| 01.04.2026 | 30.04.2026 |
| 01.05.2026 | 20.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:
| MONATSBEGINN | UMSATZ |
|---|---|
| 01.01.2026 | 3200 |
| 01.02.2026 | 0 |
| 01.03.2026 | 1800 |
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_OBJECTSbesitzt, - 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