I have a recursive CTE, which generates start and END dates for multiple dates, which is working as expected.
It will stop creating rows before the count(*) of the locations table is exceeded.
I'm looking for a way to assign a location_id to each row in the recursive CTE without having a duplicate location_id.
There is no commonality between the start/end dates and the locations so a JOIN didn't seem possible to me.
In the sample code below I tried a cross apply, which gave me the same location_id for each group generated by the recursive code so that isn't working either and that's how I left the test case.
I'm using the PIPELINED function to simulate multiple dates but I'm not necessarily tied to that idea. If the solution requires processing one DATE at a time.
My goal is to populate the schedule table below by passing in a schedule_id, range of dates and a number, which represents the time the schedule should begin.
In my test CASE that is all hard coded as I wanted to keep the test CASE as simple as possible. You can assume the schedule_id =1 or any number you wish.
Once I get this logic working I want to use a MERGE statement to INSERT rows so there will not be duplicate rows for a schedule if the process is run more than once with the same values.
The PRIMARY KEY being schedule_id, location_id, base_date
My test CASE is below, which I can't seem to get working. Thanks in advance for your expertise and patience.
Current Result
LOCATION_ID BASE_DATE START_DATE END_DATE
1 08212021 00:00:00 08212021 23:16:00 08212021 23:21:00
1 08212021 00:00:00 08212021 23:26:00 08212021 23:31:00
1 08212021 00:00:00 08212021 23:36:00 08212021 23:41:00
…
…
Expected Result
LOCATION_ID BASE_DATE START_DATE END_DATE
1 08212021 00:00:00 08212021 23:16:00 08212021 23:21:00
2 08212021 00:00:00 08212021 23:26:00 08212021 23:31:00
3 08212021 00:00:00 08212021 23:36:00 08212021 23:41:00
…
...
ALTER SESSION SET NLS_DATE_FORMAT = 'MMDDYYYY HH24:MI:SS';
CREATE OR REPLACE TYPE nt_date IS TABLE OF DATE;
/
CREATE OR REPLACE FUNCTION generate_dates_pipelined(
p_from IN DATE,
p_to IN DATE
)
RETURN nt_date PIPELINED DETERMINISTIC
IS
v_start DATE := TRUNC(LEAST(p_from, p_to));
v_end DATE := TRUNC(GREATEST(p_from, p_to));
BEGIN
LOOP
PIPE ROW (v_start);
EXIT WHEN v_start >= v_end;
v_start := v_start + INTERVAL '1' DAY;
END LOOP;
RETURN;
END generate_dates_pipelined;
/
CREATE TABLE locations AS
SELECT level AS location_id,
'Door ' || level AS location_name,
CASE round(dbms_random.value(1,3))
WHEN 1 THEN 'A'
WHEN 2 THEN 'T'
WHEN 3 THEN 'T'
END AS location_type
FROM dual
CONNECT BY level <= 20;
create table schedule(
schedule_id NUMBER(4),
location_id number(4),
base_date DATE,
start_date DATE,
end_date DATE,
CONSTRAINT start_min check (start_date=trunc(start_date,'MI')),
CONSTRAINT end_min check (end_date=trunc(end_date,'MI')),
CONSTRAINT end_gt_start CHECK (end_date >= start_date),
CONSTRAINT same_day CHECK (TRUNC(end_date) = TRUNC(start_date))
);
WITH input (base_date,start_time) AS (
SELECT
COLUMN_VALUE,
COLUMN_VALUE+
NUMTODSINTERVAL(83760, 'SECOND')
FROM TABLE(generate_dates_pipelined(DATE '2021-08-21',DATE '2021-08-30'))
)
SELECT
location_id,
base_date, start_time + (LEVEL-1) * INTERVAL '10' MINUTE
AS start_date,
start_time + (LEVEL-1) * INTERVAL '10' MINUTE + INTERVAL '5' MINUTE
AS end_date
FROM input i
CROSS APPLY (SELECT location_id from locations)
CONNECT BY (LEVEL-1) * INTERVAL '10' MINUTE < INTERVAL '1' DAY
AND LEVEL <= (SELECT COUNT(*) FROM locations)
AND start_time + (LEVEL-1) * INTERVAL '10' MINUTE < TRUNC(start_time) + INTERVAL '1' DAY;
I'm looking for a way to assign a location_id to each row in the recursive CTE without having a duplicate location_id
What exactly do you mean here?
Each location can appear
only once in the results? Or every other location must appear before you reuse the same location?
Either way, you've created the locations table with values from 1..N with no gaps. So it looks to me like you can assign row numbers to your date generator and join these to the locations.
If you want to cycle through the locations, take the modulus of the date row numbers first. Use however many rows there are in the locations table for the modulus.
As you're on 19c, here's an example using a SQL macro to generate the dates and cycle through the locations:
create or replace function generate_days (
start_date date, end_date date,
day_increment integer default 1
)
return clob sql_macro as
stmt clob;
begin
stmt := 'select start_date
+ ( level - 1 ) * day_increment as dt
from dual
connect by level <= (
( ( end_date - start_date ) + day_increment
) / day_increment
)';
return stmt;
end generate_days;
/
select * from (
select dt,
mod ( row_number () over ( order by dt ), 20 ) + 1 rn
from generate_days ( date '2021-08-21',date '2021-08-30', 10/1440 )
) join locations
on rn = location_id
order by dt
fetch first 23 rows only;
DT RN LOCATION_ID LOCATION_NAME LOCATION_TYPE
21-AUG-2021 00:00:00 2 2 Door 2 T
21-AUG-2021 00:10:00 3 3 Door 3 T
... <snip> ...
21-AUG-2021 02:50:00 19 19 Door 19 T
21-AUG-2021 03:00:00 20 20 Door 20 T
21-AUG-2021 03:10:00 1 1 Door 1 T
21-AUG-2021 03:20:00 2 2 Door 2 T
21-AUG-2021 03:30:00 3 3 Door 3 T
21-AUG-2021 03:40:00 4 4 Door 4 T
Read more about date generation & macros at
https://blogs.oracle.com/sql/how-to-generate-days-weeks-or-months-between-two-dates-in-oracle-database PS - the example query is just a plain CTE, it's not recursive. To be recursive there must be a union all branch, joining the CTE to the source tables.