I have a leases table, with start and end date of each lease. Some leases don't have an end date, and the field is NULL.
I want to generate a list of the payment dates/amounts over the duration of the lease. For the leases with an end date, I'm using CONNECT BY LEVEL \<= TRUNC(MONTHS_BETWEEN(pl.END_DATE, pl.START_DATE)+1,0)
.
However if I try to handle the case when the end_date is NULL, then the query goes on forever.
Sample data, lease 2 works fine and generates 13 rows, while lease 4 just goes on forever.
Lease_Id | Start_Date | End_Date |
---|---|---|
4 | 16/Jun/2023 | NULL |
2 | 20/May/2024 | 14/Jun/2025 |
I've tried various iterations of replacing the NULL with the current date, as an output field and in the results.
SELECT CASE WHEN rownum = 1 THEN pl.START_DATE ELSE ADD_MONTHS(trunc(pl.START_DATE, 'mon') + pl.PAYMENT_DAY -1, rownum -1) END AS PAYMENT_DATE
, pl.*
, rownum AS pm_num
FROM PM_LEASES pl
WHERE pl.LEASE_ID = 4
CONNECT BY LEVEL <= TRUNC(MONTHS_BETWEEN(nvl(pl.END_DATE, trunc(sysdate)), pl.START_DATE)+1,0)
Also tried reading the Oracle documentation, however it focuses on actual hierarchies.I'm using this as a workaround for the moment, which works if the lease is a 100 payments or less
SELECT pl.lease_id
, CASE WHEN lvl = 1 THEN pl.first_payment_amt ELSE pl.lease_amt END AS lease_amt
, CASE WHEN lvl = 1 THEN pl.start_date ELSE ADD_MONTHS(trunc(pl.START_DATE, 'mon') + pl.PAYMENT_DAY -1, lvl-1) END AS payment_date
FROM pm_leases pl
LEFT JOIN (select level as lvl from dual connect by level <= 100) ON 1=1
WHERE add_months(pl.start_date, lvl) <= nvl(pl.end_date, add_months(trunc(sysdate), 1))
From Oracle 12, you can LATERAL
join to a row generator and use COALESCE(end_date, SYSDATE)
to get the final day:
SELECT CASE m.months
WHEN 0
THEN pl.start_date
ELSE ADD_MONTHS(
TRUNC(pl.start_date, 'MM') + pl.payment_date - 1,
m.months
)
END AS PAYMENT_DATE,
pl.*
FROM PM_LEASES pl
CROSS JOIN LATERAL(
SELECT LEVEL - 1 AS months
FROM DUAL
CONNECT BY LEVEL <= MONTHS_BETWEEN(
COALESCE(pl.end_date, SYSDATE),
TRUNC(pl.start_date, 'MM') + pl.payment_date - 1
) + 1
) m
-- WHERE pl.LEASE_ID = 4
Which, for the sample data:
CREATE TABLE pm_leases (Lease_Id, Payment_date, Start_Date, End_Date) AS
SELECT 4, 12, DATE '2023-07-16', NULL FROM DUAL UNION ALL
SELECT 2, 28, DATE '2024-05-20', DATE '2025-06-14' FROM DUAL;
Outputs:
PAYMENT_DATE | LEASE_ID | PAYMENT_DATE | START_DATE | END_DATE |
---|---|---|---|---|
2023-07-16 00:00:00 | 4 | 12 | 2023-07-16 00:00:00 | null |
2023-08-12 00:00:00 | 4 | 12 | 2023-07-16 00:00:00 | null |
2023-09-12 00:00:00 | 4 | 12 | 2023-07-16 00:00:00 | null |
... | ... | ... | ... | ... |
2024-04-12 00:00:00 | 4 | 12 | 2023-07-16 00:00:00 | null |
2024-05-12 00:00:00 | 4 | 12 | 2023-07-16 00:00:00 | null |
2024-06-12 00:00:00 | 4 | 12 | 2023-07-16 00:00:00 | null |
2024-05-20 00:00:00 | 2 | 28 | 2024-05-20 00:00:00 | 2025-06-14 00:00:00 |
2024-06-28 00:00:00 | 2 | 28 | 2024-05-20 00:00:00 | 2025-06-14 00:00:00 |
2024-07-28 00:00:00 | 2 | 28 | 2024-05-20 00:00:00 | 2025-06-14 00:00:00 |
... | ... | ... | ... | ... |
2025-05-28 00:00:00 | 2 | 28 | 2024-05-20 00:00:00 | 2025-06-14 00:00:00 |
2024-05-20 00:00:00 | 1 | 12 | 2024-05-20 00:00:00 | 2024-06-10 00:00:00 |
2024-05-20 00:00:00 | 3 | 22 | 2024-05-20 00:00:00 | 2024-06-21 00:00:00 |
or, depending on how you want to handle payment dates that may occur in the same month as the starting month:
SELECT GREATEST(
ADD_MONTHS(
TRUNC(pl.start_date - pl.payment_date + 1, 'MM')
+ pl.payment_date - 1,
m.months
),
pl.start_date
) AS PAYMENT_DATE,
pl.*
FROM PM_LEASES pl
CROSS JOIN LATERAL(
SELECT LEVEL - 1 AS months
FROM DUAL
CONNECT BY ADD_MONTHS(
TRUNC(pl.start_date - pl.payment_date + 1, 'MM')
+ pl.payment_date - 1,
LEVEL - 1
) <= COALESCE(pl.end_date, SYSDATE)
) m
--WHERE pl.LEASE_ID = 4
Which outputs:
PAYMENT_DATE | LEASE_ID | PAYMENT_DATE | START_DATE | END_DATE |
---|---|---|---|---|
2023-07-16 00:00:00 | 4 | 12 | 2023-07-16 00:00:00 | null |
2023-08-12 00:00:00 | 4 | 12 | 2023-07-16 00:00:00 | null |
2023-09-12 00:00:00 | 4 | 12 | 2023-07-16 00:00:00 | null |
... | ... | ... | ... | ... |
2024-04-12 00:00:00 | 4 | 12 | 2023-07-16 00:00:00 | null |
2024-05-12 00:00:00 | 4 | 12 | 2023-07-16 00:00:00 | null |
2024-06-12 00:00:00 | 4 | 12 | 2023-07-16 00:00:00 | null |
2024-05-20 00:00:00 | 2 | 28 | 2024-05-20 00:00:00 | 2025-06-14 00:00:00 |
2024-05-28 00:00:00 | 2 | 28 | 2024-05-20 00:00:00 | 2025-06-14 00:00:00 |
2024-06-28 00:00:00 | 2 | 28 | 2024-05-20 00:00:00 | 2025-06-14 00:00:00 |
... | ... | ... | ... | ... |
2025-03-28 00:00:00 | 2 | 28 | 2024-05-20 00:00:00 | 2025-06-14 00:00:00 |
2025-04-28 00:00:00 | 2 | 28 | 2024-05-20 00:00:00 | 2025-06-14 00:00:00 |
2025-05-28 00:00:00 | 2 | 28 | 2024-05-20 00:00:00 | 2025-06-14 00:00:00 |
Note: You may need something more robust if payment_date
is set to 29-31 as not all months have that many days.