Using the model clause to expand dates - sql

I have several different types of data involving date range that I want to merge together, but at the same time broken down by day. So a 3 day piece of data would result in three rows:
start primary_key
start+1 primary_key
start+2 primary_key
I've been playing around using the model clause of the select statement in 10g and was looking for the best way to achieve this. Currently I'm joining a range of dates that covers the full range of possible dates (select min(start date), max(end date)). I'd prefer to be selecting the data and adding in more rows to transform it to a per day dataset.
edit:
I've managed to come up with (now includes sample data):
SELECT * FROM (
SELECT 123 req_code,
345 req_par_code,
TO_DATE('01-03-2010', 'dd-mm-yyyy') req_start_date,
TO_DATE('05-03-2010', 'dd-mm-yyyy') req_end_date
FROM dual
)
MODEL
PARTITION BY (req_code)
DIMENSION BY (0 d)
MEASURES (SYSDATE dt, req_par_code, req_start_date, req_end_date)
RULES ITERATE(365) UNTIL (dt[iteration_number] >= TRUNC(req_end_date[0])) (
dt[iteration_number] = NVL(dt[iteration_number-1] + 1, TRUNC(req_start_date[0])),
--Copy data across
req_par_code[ iteration_number ] = req_par_code[0],
req_start_date[ iteration_number ] = req_start_date[0],
req_end_date[ iteration_number ] = req_end_date[0]
)
ORDER BY dt, req_code;

you can use the MODEL clause to generate rows, here's a small example:
SQL> SELECT * FROM t_data;
PK START_DATE END_DATE
---------- ----------- -----------
1 20/01/2010 20/01/2010
2 21/01/2010 23/01/2010
3 24/01/2010 27/01/2010
SQL> SELECT pk, start_date, end_date FROM t_data
2 MODEL
3 PARTITION BY (pk)
4 DIMENSION BY (0 AS i)
5 MEASURES(start_date, end_date)
6 RULES
7 ( start_date[FOR i
8 FROM 1 TO end_date[0]-start_date[0]
9 INCREMENT 1] = start_date[0] + cv(i),
10 end_date[ANY] = start_date[CV()] + 1
11 )
12 ORDER BY 1,2;
PK START_DATE END_DATE
---------- ----------- -----------
1 20/01/2010 21/01/2010
2 21/01/2010 22/01/2010
2 22/01/2010 23/01/2010
2 23/01/2010 24/01/2010
3 24/01/2010 25/01/2010
3 25/01/2010 26/01/2010
3 26/01/2010 27/01/2010
3 27/01/2010 28/01/2010

SELECT TO_DATE('01.01.2009', 'dd.mm.yyyy') + level - 1
FROM dual
CONNECT BY
TO_DATE('01.01.2009', 'dd.mm.yyyy') + level <= TRUNC(SYSDATE, 'DDD') + 1
will give you the list of all dates from Jan 1st, 2009 till today.

Related

repeat a sequence from a lookup table across a set number of records oracle sql

case:
the user is required to select a start and end date for a specific period and they are also required to select a sequence and where in the sequence they would like to start the sequence cycle (the sequence is stored in a lookup table...an example of a store sequence is shown in the example below under sequence lookup ).
user input parameters:
start date : 01-jan-2021
end date : 14-jan-2021
sequence_name : 1-5
start sequence at : 4
after the user inputs the paramaters , the system will
list all dates between between 01-jan-2021 and 14-jan-2021 - shown below in the example then it will start to map the sequence to the dates starting with the sequence number inputted in this case its 4 (shown in example below)
when the system reaches the end of the sequence (in this case the end is 5) it will restart the sequence from 1 because that was the start of the sequence in the look up.
in the example image below it shows what the results should look like.
thank you for your help!
i prefer to write it in sql but if its not possible in sql then plsql is also fine.
You haven't really explained how you get from your 'sequence name' to a range of values, so I'll assume you already that that part, and will work from a date range and a sequence range, which can be provided for simplicity as a CTE:
with input (start_date, end_date, start_seq, end_seq, start_at) as (
select date '2021-01-01', date '2021-01-14', 1, 5, 4 from dual
)
select * from input
You tagged the question with Oracle 11g. If that is 11gR2 then you can use a recursive CTE to generate the result from that simulated input data:
with input (start_date, end_date, start_seq, end_seq, start_at) as (
select date '2021-01-01', date '2021-01-14', 1, 5, 4 from dual
),
rcte (dt, seq, end_date, start_seq, end_seq) as (
select start_date, start_at, end_date, start_seq, end_seq
from input
union all
select dt + 1, case when seq = end_seq then start_seq else seq + 1 end,
end_date, start_seq, end_seq
from rcte
where dt < end_date
)
select dt, seq
from rcte
order by dt;
The anchor member use the start date and start-at value, and keeps the other information needed later. The recursive member increments both, wrapping the seq value at the top of that range. Giving the result:
DT | SEQ
:-------- | --:
01-JAN-21 | 4
02-JAN-21 | 5
03-JAN-21 | 1
04-JAN-21 | 2
05-JAN-21 | 3
06-JAN-21 | 4
07-JAN-21 | 5
08-JAN-21 | 1
09-JAN-21 | 2
10-JAN-21 | 3
11-JAN-21 | 4
12-JAN-21 | 5
13-JAN-21 | 1
14-JAN-21 | 2
On earlier versions, or just if you prefer it, you can use a hierarchical query, which looks shorter but I think it's a bit less intuitive:
with input (start_date, end_date, start_seq, end_seq, start_at) as (
select date '2021-01-01', date '2021-01-14', 1, 5, 4 from dual
)
select start_date + level - 1 as dt,
mod(level - 1 + start_at - start_seq, end_seq - start_seq + 1) + start_seq as seq
from input
connect by level <= end_date - start_date + 1
order by dt;
db<>fiddle showing both approaches.
And a second db<>fiddle showing a different sequence range and starting point.

How to include values that count nothing on certain day (APEX)

I have this query:
SELECT
COUNT(ID) AS FREQ,
TO_CHAR(TRUNC(CREATED_AT),'DD-MON') DATES
FROM TICKETS
WHERE TRUNC(CREATED_AT) > TRUNC(SYSDATE) - 32
GROUP BY TRUNC(CREATED_AT)
ORDER BY TRUNC(CREATED_AT) ASC
This counts how many tickets where created every day for the past month.
The result looks something like this: (first 10 rows)
FREQ DATES
3 28-DEC
4 04-JAN
8 05-JAN
1 06-JAN
4 07-JAN
5 08-JAN
2 11-JAN
6 12-JAN
3 13-JAN
8 14-JAN
The linechart that I created looks like this:
The problem is that the days where tickets are not created (in particular the weekends) the line just goes straight to the day where there is created a ticket.
Is there a way in APEX or in my query to include the days that aren't counted?
As commented, using one of row generator techniques you'd create a "calendar" table and outer join it with a table that contains data you're displaying.
Something like this (see comments within code):
SQL> with yours (amount, datum) as
2 -- your sample table
3 (select 100, date '2021-01-01' from dual union all
4 select 200, date '2021-01-03' from dual union all
5 select 300, date '2021-01-07' from dual
6 ),
7 minimax as
8 -- MIN and MAX date (so that they could be used in row generator --> CALENDAR CTE (below)
9 (select min(datum) min_datum,
10 max(datum) max_datum
11 from yours
12 ),
13 calendar as
14 -- calendar, from MIN to MAX date in YOUR table
15 (select min_datum + level - 1 datum
16 from minimax
17 connect by level <= max_datum - min_datum + 1
18 )
19 -- final query uses outer join
20 select c.datum,
21 nvl(y.amount, 0) amount
22 from calendar c left join yours y on y.datum = c.datum
23 order by c.datum;
DATUM AMOUNT
---------- ----------
01.01.2021 100
02.01.2021 0
03.01.2021 200
04.01.2021 0
05.01.2021 0
06.01.2021 0
07.01.2021 300
7 rows selected.
SQL>
Applied to your current query:
WITH
minimax
AS
-- MIN and MAX date (so that they could be used in row generator --> CALENDAR CTE (below)
(SELECT MIN (created_at) min_datum, MAX (created_at) max_datum
FROM tickets),
calendar
AS
-- calendar, from MIN to MAX date in YOUR table
( SELECT min_datum + LEVEL - 1 datum
FROM minimax
CONNECT BY LEVEL <= max_datum - min_datum + 1)
-- final query uses outer join
SELECT COUNT (t.id) AS freq, TO_CHAR (TRUNC (c.datum), 'DD-MON') dates
FROM calendar c LEFT JOIN tickets t ON t.created_at = c.datum
WHERE TRUNC (t.created_at) > TRUNC (SYSDATE) - 32
GROUP BY TRUNC (c.datum)
ORDER BY dates ASC
I added a with clause to generate last 31 days, then I left joined with your base table like below.
with last_31_days as (
select trunc(sysdate) - 32 + level dt from dual connect by trunc(sysdate) - 32 + level < trunc(sysdate)
)
SELECT
nvl(COUNT(t.ID), 0) AS FREQ,
TO_CHAR(
nvl(TRUNC(t.CREATED_AT), a.dt)
,'DD-MON') DATES
FROM last_31_days a
LEFT JOIN TICKETS t
ON TRUNC(t.CREATED_AT) = a.dt
GROUP BY nvl(TRUNC(t.CREATED_AT), a.dt)
ORDER BY 2 ASC
;
#Littlefoot answer is perfect. but here is a cheeky way to get the similar table with format match OP output. using a simple cte for this.
WITH cte AS (
SELECT To_Char(Trunc(SYSDATE - ROWNUM),'DD-MON') dtcol
FROM DUAL
CONNECT BY ROWNUM < 366
)
SELECT * FROM cte
here is db<>fiddle
and then you can simply join this cte to fill up empty date. as the origin output column date looks like a string column.
connect by is for oracle only. but I think you can still use recursive cte to get similar result in other DBMS support recursive cte.

Finding gaps between date ranges spanning records

I'm trying to write a query where I can find any gap in the date ranges for a given ID when passing in two dates.
EDIT: I need to know if a whole gap or part of a gap exists in my date range.
I have data in this format:
Example 1:
| ID | START_DATE | END_DATE |
|----|------------|------------|
| 1 | 01/01/2019 | 30/09/2019 |
| 1 | 01/03/2020 | (null) |
Example 2:
| ID | START_DATE | END_DATE |
|----|------------|------------|
| 2 | 01/01/2019 | 30/09/2019 |
| 2 | 01/10/2019 | 01/12/2019 |
| 2 | 02/12/2019 | (null) |
NB. A null end date essentially means "still active up to current day".
E.g. Example 1 has a gap of 152 days between 30/09/2019 and 01/03/2020. If I queried in the range of 05/05/2019 - 01/09/2019 there's no gap in that range. Whereas if I'm looking at the date range 05/05/2019 - 02/10/2019 there's a single day gap in that range.
For what it's worth, I don't actually care how many days gap, just whether there is one or not.
I've tried doing something like this but it doesn't work when my date falls into a gap:
SELECT SUM(START_DATE - PREV_END - 1)
FROM
(
SELECT ID, START_DATE, END_DATE, LAG(END_DATE) OVER (ORDER BY START_DATE) AS PREV_END_DATE
FROM TBL
WHERE ID = X_ID
)
WHERE START_DATE >= Y_FIRST_DATE
AND START_DATE <= Z_SECOND_DATE;
X_ID, Y_FIRST_DATE, and Z_SECOND_DATE are just any different ID or date range I might want to pass in.
How could I go about this?
Another option to determine the days might be by use SELECT .. FROM dual CONNECT BY LEVEL <= syntax through EXISTence of gaps by INTERSECTing two sets, one finds all dates between extremum parameters while the other finds all the dates fitting within the dates inserted into table as bounds :
SELECT CASE WHEN
SUM( 1 + LEAST(Z_SECOND_DATE,NVL(END_DATE,TRUNC(SYSDATE)))
- GREATEST(Y_FIRST_DATE,START_DATE) ) = Z_SECOND_DATE - Y_FIRST_DATE + 1 THEN
'NO Gap'
ELSE
'Gap Exists'
END "gap?"
FROM TBL t
WHERE ID = X_ID
AND EXISTS ( SELECT Y_FIRST_DATE + LEVEL - 1
FROM dual
CONNECT BY LEVEL <= Z_SECOND_DATE - Y_FIRST_DATE + 1
INTERSECT
SELECT t.START_DATE + LEVEL - 1
FROM dual
CONNECT BY LEVEL <= NVL(t.END_DATE,TRUNC(SYSDATE))- t.START_DATE + 1
)
START_DATE values are assumed to be non-null based on the sample data.
Demo
This is another variation the islands-and-gaps problem that pops up a lot here. I think this fits with Oracle's pattern matching functionality. Take this example:
WITH tbl AS
(
SELECT 1 AS ID, to_date('01/01/2019', 'DD/MM/YYYY') AS START_DATE, to_date('30/09/2019', 'DD/MM/YYYY') AS END_DATE FROM DUAL
UNION ALL
SELECT 1 AS ID, to_date('01/03/2020', 'DD/MM/YYYY') AS START_DATE, NULL AS END_DATE FROM DUAL
UNION ALL
SELECT 2 AS ID, to_date('01/01/2019', 'DD/MM/YYYY') AS START_DATE, to_date('30/09/2019', 'DD/MM/YYYY') AS END_DATE FROM DUAL
UNION ALL
SELECT 2 AS ID, to_date('01/10/2019', 'DD/MM/YYYY') AS START_DATE, to_date('01/12/2019', 'DD/MM/YYYY') AS END_DATE FROM DUAL
UNION ALL
SELECT 2 AS ID, to_date('02/12/2019', 'DD/MM/YYYY') AS START_DATE, NULL AS END_DATE FROM DUAL
)
SELECT *
FROM tbl
MATCH_RECOGNIZE(ORDER BY ID, start_date
MEASURES b.id AS ID,
a.end_date+1 AS GAP_START,
b.start_date-1 AS GAP_END
PATTERN (A B+)
DEFINE B AS start_date > PREV(end_date)+1 AND ID = PREV(ID))L;
I know it looks long, but most of it is creating the WITH clause. The pattern matching allows you to define what a gap is and pull the information accordingly. Notice that in order to have a gap, your start date must be greater than the previous end date + 1 grouped by the ID column.
To enhance this to answer your updated/edited question, just add this line of code to the end:
WHERE GREATEST(gap_start, TO_DATE('15/09/2019', 'DD/MM/YYYY' /*Y_FIRST_DATE*/)) <= LEAST(gap_end, to_date('15/10/2019', 'DD/MM/YYYY')/*Z_SECOND_DATE*/)
You can split the date range you are passing, into dates and then compare it with a date range in your table as follows:
SELECT
CASE WHEN SUM(CASE WHEN T.ID IS NULL THEN 1 END) > 0
THEN 'THERE IS GAP'
ELSE 'THERE IS NO GAP'
END AS RESULT_
FROM ( SELECT P_IN_FROM_DATE + LEVEL - 1 AS CUST_DATES
FROM DUAL
CONNECT BY LEVEL <= P_IN_TO_DATE - P_IN_FROM_DATE + 1
) CUST_TBL
LEFT JOIN TBL T
ON CUST_TBL.CUST_DATES BETWEEN T.START_DATE AND T.END_DATE
OR ( CUST_TBL.CUST_DATES >= T.START_DATE AND T.END_DATE IS NULL )
I would suggest finding the maximum end date before the current record -- based on the start date.
That would be:
select t.*
from (select t.*,
max(end_date) over (order by start_date
rows between unbounded preceding and 1 preceding
) as max_prev_end_date
from tbl t
where start_date <= :input_end_date and
end_date >= :input_start_date
) t
where max_prev_end_date < start_date;

Expand date range to get all dates between series of date ranges from a table with excluded dates

I need to get all dates between DATE_FROM and DATE_TO of every ID of table LEAVE excluding weekends, work suspensions and holidays. Considering this record (ID, DATE_FROM, DATE_TO):
001 04-OCT-2018 09-OCT-2018
002 05-OCT-2018 05-OCT-2018
...
n 01-OCT-2018 05-OCT-2018
I need to get all the dates between those ranges in this format (ID, DAY_TOKEN):
001 04-OCT-2018
001 05-OCT-2018
001 08-OCT-2018
001 09-OCT-2018
002 05-OCT-2018
...
n 01-OCT-2018
n 02-OCT-2018
n 03-OCT-2018
n 04-OCT-2018
n 05-OCT-2018
I am using this query modified from the queries I found:
SELECT ID, a.date_from + rnum - 1 AS day_token
FROM (SELECT a.ID, a.date_from, a.date_to, ROWNUM AS rnum
FROM all_objects, leave a
-- Aside from ALL_OBJECT, I cross join it with my LEAVE table
WHERE ROWNUM <= a.date_to - a.date_from + 1) a
WHERE TO_CHAR (a.date_from + rnum - 1, 'DY') NOT IN ('SAT', 'SUN');
AND NOT EXISTS (SELECT 1
FROM holiday b
WHERE b.schedule = d.date_from + rnum - 1)
AND NOT EXISTS (SELECT 1
FROM suspension c
WHERE c.schedule = d.date_from + rnum - 1)
The problem is that only the first record will expand properly and the other records will not be included in the record set unless the DATE_FROM and DATE_TO is of the same date.
I want to avoid using a PL-SQL function as much as possible, but if it's impossible to achieve the resultset I needed without using a function, please tell me at least the reason why.
Here's how to create as many rows for each ID as there are days between FROM and TO dates, without weekends (Saturdays and Sundays):
SQL> with leave (id, date_from, date_to) as
2 (select '001', date '2018-10-04', date '2018-10-09' from dual union all
3 select '002', date '2018-10-05', date '2018-10-05' from dual union all
4 select '003', date '2018-10-02', date '2018-10-08' from dual
5 ),
6 inter as
7 (select l.id,
8 l.date_from + column_value datum,
9 to_char(l.date_from + column_value, 'day') day
10 from leave l,
11 table(cast(multiset(select level from dual
12 connect by level <= l.date_to - l.date_from + 1
13 ) as sys.odcinumberlist))
14 )
15 select id, datum
16 from inter
17 where to_char(datum, 'dy') not in ('sat', 'sun');
ID DATUM
--- -----------
001 05-oct-2018
001 08-oct-2018
001 09-oct-2018
001 10-oct-2018
003 03-oct-2018
003 04-oct-2018
003 05-oct-2018
003 08-oct-2018
003 09-oct-2018
9 rows selected.
SQL>
As line 18 (and so forth), add additional conditions (remove holidays, suspensions, whatever).
(BTW, I wonder who & why downvoted your question; it is well-formed, shows what you have, your attempt to solve it ... really, a mystery to me).

Oracle SQL overlap between begin date and end date in 2 or more records

Database my_table:
id seq start_date end_date
1 1 01-01-2017 02-01-2017
1 2 07-01-2017 09-01-2017
1 3 11-01-2017 11-01-2017
2 1 20-01-2017 20-01-2017
3 1 01-02-2017 02-02-2017
3 2 03-02-2017 04-02-2017
3 3 08-01-2017 09-02-2017
3 4 09-01-2017 10-02-2017
3 5 10-01-2017 12-02-2017
My requirement is to get the first date (normally seq 1 start date) and end date (normally last seq end date) and the number of dates occurred during all seq for each unique ID.
Date occurred:
id 1 2 3
01-01-2017 20-01-2017 01-02-2017
02-01-2017 02-02-2017
07-01-2017 03-02-2017
08-01-2017 04-02-2017
09-01-2017 08-02-2017
11-01-2017 09-02-2017
10-02-2017
11-02-2017
12-02-2017
total 6 1 9
Here is the result I want:
id start_date end_date num_date
1 01-01-2017 11-01-2017 6
2 20-01-2017 20-01-2017 1
3 01-02-2017 12-02-2017 9
I have tried
SELECT id
, MIN(start_date)
, MAX(end_date)
, SUM(end_date - start_date + 1)
FROM my_table
GROUP BY id
and this SQL statement work fine in id 1 and 2 since there is no overlap date between begin date and end date. But for id 3, the result num_date is 11. Could you please suggest the SQL statement to solve this problem? Thank you.
One more question: The date in database is in datetime format. How do I convert it to date. I tried to use TRUNC function but it sometimes convert date to yesterday instead.
You need to count how many times an end_date equals the following start_date. For this you need to use the lag() or the lead() analytic function. You can use a case expression for the comparison, but alas you can't wrap the case expression within a COUNT or SUM in the same query; you need a subquery and an outer query.
Something like this; not tested, since you didn't provide CREATE TABLE and INSERT statements to recreate your sample data.
select id, min(start_date) as start_date, max(end_date) as end_date,
sum(end_date - start_date + 1 - flag) as num_days
from ( select id, start_date, end_date,
case when start_date = lag(end_date)
over (partition by id order by end_date) then 1
else 0 end as flag
from my_table
)
group by id;
SELECT id,
MIN( start_date ) AS start_date,
MAX( end_date ) AS end_date,
SUM( end_date - start_date + 1 ) AS num_days
FROM (
SELECT id,
GREATEST(
start_date,
COALESCE(
LAG( end_date ) OVER ( PARTITION BY id ORDER BY seq ) + 1,
start_date
)
) AS start_date,
end_date
FROM your_table
)
WHERE start_date <= end_date
GROUP BY id;