How to repeat select query when date within range? - sql

I have a table with a column "Date". The date will be displayed in a calendar in a cyclic form. For example the records date will be shown in the calendar in a certain day each week till a specific date (let's say TerminationDate). To summarize in my table I have the Date and the TerminationDate columns like this:
Table:
Title | Date | TerminationDate
------------------------------
t1 | d1 | td1
and I want to achieve something like this:
From query:
Title | Date | TerminationDate
------------------------------
t1 | d1+7 | td1
t1 | d1+14| td1
t1 | d1+21| td1
.................... till Date < TerminationDate
Does anyone have any idea how to achieve this in Oracle?

This should do the trick
select distinct title, date + ( level * 7 ), termination_date
from table
connect by date + ( level * 7 ) < termination_date
EDIT:
Forget about above query, since the rows must be connected only with itself there has to be
connect_by prior title = title
but that means a loop must be created. Unfortunately Oracle connect by clause throws an error if there is a loop whatsoever. Even if you use
date + ( level * 7 ) < termination_date
Oracle still stops execution immediately where it detects a loop at runtime. Using nocycle returns the result, but that returns only the first record which is date + 7
ANSWER:
So i had to approach to the problem in a different way
select t.*, date + (r * 7) as the_date
from table t,
(select rownum as r
from dual
connect by level < (select max(weeks) --max date interval as weeks to be used to repeat each row as much, if you know the max weeks difference you can use the constant number instead of this sub-query
from (select ceil((termination_date - date) / 7) as weeks
from table ))
)
where r < ceil((termination_date - date) / 7)
Let me know is there is any conufsion or performance problem

I have not tested the query ,but it should work like as you need
SELECT t1, d1 + (7 * LEVEL), termination_date
FROM tab
WHERE d1 + (7 * LEVEL) < termination_date
CONNECT BY LEVEL <= CEIL( (termination_date - d1) / 7);
EDIT
SELECT DISTINCT t1,dt,termination_date
FROM(
SELECT t1, d1 + (7 * LEVEL) dt, termination_date
FROM tab
WHERE d1 + (7 * LEVEL) < termination_date
CONNECT BY LEVEL <= CEIL( (termination_date - d1) / 7)
);

Here's one more way to do it
SELECT
*,
date + ( ROWNUM * 7 ) as modified_date
FROM (
SELECT
title,
date,
termination_date
FROM
table
) WHERE date + ( ROWNUM * 7 ) < termination_date

Related

Count Data by Loop Calendar SQL/Oracle

I need to get the data that generates count of total ID by date between date_active and date_end using date ranges for each. If the dates are crossing each other the ID will adding up. here is the data I have right now,
TABLE CONTRACT:
ID DATE_ACTIVE DATE_END
1 05-FEB-13 08-NOV-13
1 21-DEC-18 06-OCT-19
2 05-FEB-13 27-JAN-14
3 05-FEB-13 07-NOV-13
4 06-FEB-13 02-NOV-13
4 25-OCT-14 13-APR-16
TABLE CALENDAR:
DT
05-FEB-13
06-FEB-13
07-FEB-13
08-FEB-13
09-FEB-13
..-DEC-19
what I want out is basically like this:
DT COUNT(ID)
05-FEB-13 3
06-FEB-13 4
07-FEB-13 4
08-FEB-13 4
09-FEB-13 4
10-FEB-13 4
....
03-NOV-13 3
....
08-NOV-13 2
09-NOV-13 1
....
28-JAN-14 0
....
25-OCT-14 1
....
13-APR-16 1
14-APR-16 0
....
21-DEC-18 1
....
06-OCT-19 1
07-OCT-19 0
....
....
And here is my query to get that result
with contract as (
select * from contract
where id in ('1','2','3','4')
)
,
cal as
(
select TRUNC (SYSDATE - ROWNUM) dt
from dual
connect by rownum < sysdate - to_date('05-FEB-13')
)
select aa.dt,count(distinct bb.id)id from cal aa
left join contract bb on aa.dt >= bb.date_active and aa.dt<= bb.date_end
group by aa.dt
order by 1
but the problem is I have 6 mio of ID and if I use this kind of query, the result maybe will take forever, and I'm having a hard times to figured out how to get the result with different query. It will be my pleasure if somebody can help me out of this. Thank you so much.
If you group your events by date_active and date_end, you will get the numbers of events which have started and ended on each separate day.
Not a lot of days have passed between 2013 and 2019 (about 2 000), so the grouped resultsets will be relatively short.
Now that you have the two groups, you can notice that the number of events on each given date is the number of events which have started on or before this date, minus the number of events which have finished on or before this date (I'm assuming the end dates are non-inclusive).
In other words, the number of events on every given day is:
The number of events on the previous date,
plus the number of events started on this date,
minus the number of events ended on this date.
This can be easily done using a window function.
This will require a join between the calendar table and the two groups, but fortunately all of them are relatively short (thousands of records) and the join would be fast.
Here's the query: http://sqlfiddle.com/#!4/b21ce/5
WITH cal AS
(
SELECT TRUNC (to_date('01-NOV-13') - ROWNUM) dt
FROM dual
CONNECT BY
rownum < to_date('01-NOV-13')- to_date('01-FEB-13')
),
started_on AS
(
SELECT date_active AS dt, COUNT(*) AS cnt_start
FROM contract
GROUP BY
date_active
),
ended_on AS
(
SELECT date_end AS dt, COUNT(*) AS cnt_end
FROM contract
GROUP BY
date_end
)
SELECT dt,
SUM(COALESCE(cnt_start, 0) - COALESCE(cnt_end, 0)) OVER (ORDER BY dt) cnt
FROM cal c
LEFT JOIN
started_on s
USING (dt)
LEFT JOIN
ended_on e
USING (dt)
(I used a fixed date instead of SYSDATE to keep the resultset short, but the idea is the same)
This query requires that the calendar starts before the earliest event, otherwise every result will be off by a fixed amount, the number of events before the beginning of the calendar.
You can replace the fixed date in the calendar condition with (SELECT MIN(date_active) FROM contract) which is instant if date_active is indexed.
Update:
If your contract dates can overlap and you want to collapse multiple overlapping contracts into a one continuous contract, you can use window functions to do so.
WITH cal AS
(
SELECT TRUNC (to_date('01-NOV-13') - ROWNUM) dt
FROM dual
CONNECT BY
rownum <= to_date('01-NOV-13')- to_date('01-FEB-13')
),
collapsed_contract AS
(
SELECT *
FROM (
SELECT c.*,
COALESCE(LAG(date_end_effective) OVER (PARTITION BY id ORDER BY date_active), date_active) AS date_start_effective
FROM (
SELECT c.*,
MAX(date_end) OVER (PARTITION BY id ORDER BY date_active) AS date_end_effective
FROM contract c
) c
) c
WHERE date_start_effective < date_end_effective
),
started_on AS
(
SELECT date_start_effective AS dt, COUNT(*) AS cnt_start
FROM collapsed_contract
GROUP BY
date_start_effective
),
ended_on AS
(
SELECT date_end_effective AS dt, COUNT(*) AS cnt_end
FROM collapsed_contract
GROUP BY
date_end_effective
)
SELECT dt,
SUM(COALESCE(cnt_start, 0) - COALESCE(cnt_end, 0)) OVER (ORDER BY dt) cnt
FROM cal c
LEFT JOIN
started_on s
USING (dt)
LEFT JOIN
ended_on e
USING (dt)
http://sqlfiddle.com/#!4/adeba/1
The query might seem bulky, but that's to make it more efficient, as all these window functions can be calculated in a single pass over the table.
Note however that this single pass relies on the table being sorted on (id, date_active) so an index on these two fields is crucial.
Firstly, row_number() over (order by id,date_active) analytic function is used in order to generate unique ID values those will be substituted in
connect by level <= ... and prior id = id syntax to get unpivoted hierarchical data :
with t0 as
(
select row_number() over (order by id,date_active) as id, date_active, date_end
from contract
), t1 as
(
select date_active + level - 1 as dt
from t0
connect by level <= date_end - date_active + 1
and prior id = id
and prior sys_guid() is not null
)
select dt, count(*)
from t1
group by dt
order by dt
Demo

Irregular grouping of timestamp variable

I have a table organized as follows:
id lateAt
1231235 2019/09/14
1242123 2019/09/13
3465345 NULL
5676548 2019/09/28
8986475 2019/09/23
Where lateAt is a timestamp of when a certain loan's payment became late. So, for each current date - I need to look at these numbers daily - there's a certain amount of entries which are late for 0-15, 15-30, 30-45, 45-60, 60-90 and 90+ days.
This is my desired output:
lateGroup Count
0-15 20
15-30 22
30-45 25
45-60 32
60-90 47
90+ 57
This is something I can easily calculate in R, but to get the results back to my BI dashboard I'd have to create a new table in my database, which I don't think is a good practice. What is the SQL-native approach to this problem?
I would define the "late groups" using a range, the join against the number of days:
with groups (grp) as (
values
(int4range(0,15, '[)')),
(int4range(15,30, '[)')),
(int4range(30,45, '[)')),
(int4range(45,60, '[)')),
(int4range(60,90, '[)')),
(int4range(90,null, '[)'))
)
select grp, count(t.user_id)
from groups g
left join the_table t on g.grp #> current_date - t.late_at
group by grp
order by grp;
int4range(0,15, '[)') creates a range from 0 (inclusive) and 15 (exclusive)
Online example: https://rextester.com/QJSN89445
The quick and dirty way to do this in SQL is:
SELECT '0-15' AS lateGroup,
COUNT(*) AS lateGroupCount
FROM my_table t
WHERE (CURRENT_DATE - t.lateAt) >= 0
AND (CURRENT_DATE - t.lateAt) < 15
UNION
SELECT '15-30' AS lateGroup,
COUNT(*) AS lateGroupCount
FROM my_table t
WHERE (CURRENT_DATE - t.lateAt) >= 15
AND (CURRENT_DATE - t.lateAt) < 30
UNION
SELECT '30-45' AS lateGroup,
COUNT(*) AS lateGroupCount
FROM my_table t
WHERE (CURRENT_DATE - t.lateAt) >= 30
AND (CURRENT_DATE - t.lateAt) < 45
-- Etc...
For production code, you would want to do something more like Ross' answer.
You didn't mention which DBMS you're using, but nearly all of them will have a construct known as a "value constructor" like this:
select bins.lateGroup, bins.minVal, bins.maxVal FROM
(VALUES
('0-15',0,15),
('15-30',15.0001,30), -- increase by a small fraction so bins don't overlap
('30-45',30.0001,45),
('45-60',45.0001,60),
('60-90',60.0001,90),
('90-99999',90.0001,99999)
) AS bins(lateGroup,minVal,maxVal)
If your DBMS doesn't have it, then you can probably use UNION ALL:
SELECT '0-15' as lateGroup, 0 as minVal, 15 as maxVal
union all SELECT '15-30',15,30
union all SELECT '30-45',30,45
Then your complete query, with the sample data you provided, would look like this:
--- example from SQL Server 2012 SP1
--- first let's set up some sample data
create table #temp (id int, lateAt datetime);
INSERT #temp (id, lateAt) values
(1231235,'2019-09-14'),
(1242123,'2019-09-13'),
(3465345,NULL),
(5676548,'2019-09-28'),
(8986475,'2019-09-23');
--- here's the actual query
select lateGroup, count(*) as Count
from #temp as T,
(VALUES
('0-15',0,15),
('15-30',15.0001,30), -- increase by a small fraction so bins don't overlap
('30-45',30.0001,45),
('45-60',45.0001,60),
('60-90',60.0001,90),
('90-99999',90.0001,99999)
) AS bins(lateGroup,minVal,maxVal)
) AS bins(lateGroup,minVal,maxVal)
where datediff(day,lateAt,getdate()) between minVal and maxVal
group by lateGroup
order by lateGroup
--- remove our sample data
drop table #temp;
Here's the output:
lateGroup Count
15-30 2
30-45 2
Note: rows with null lateAt are not counted.
I think you can do it all in one clear query :
with cte_lategroup as
(
select *
from (values(0,15,'0-15'),(15,30,'15-30'),(30,45,'30-45')) as t (mini, maxi, designation)
)
select
t2.designation
, count(*)
from test t
left outer join cte_lategroup t2
on current_date - t.lateat >= t2.mini
and current_date - lateat < t2.maxi
group by t2.designation;
With a preset like yours :
create table test
(
id int
, lateAt date
);
insert into test
values (1231235, to_date('2019/09/14', 'yyyy/mm/dd'))
,(1242123, to_date('2019/09/13', 'yyyy/mm/dd'))
,(3465345, null)
,(5676548, to_date('2019/09/28', 'yyyy/mm/dd'))
,(8986475, to_date('2019/09/23', 'yyyy/mm/dd'));

Oracle SQL: Compare Date Ranges and get the dates of the same day

I have different events:
If more than 2 of them take place in one day, I would like to know which ones and how many that are.
How do I build the SQL command?
Expected output:
More than 2 events on the same day.
Try this:
WITH calendar
AS ( SELECT TRUNC( SYSDATE - LEVEL ) AS cal_day
FROM DUAL
CONNECT BY LEVEL < 30)
SELECT calendar.cal_day,
COUNT( e.event_id ) AS number_of_events,
LISTAGG( e.event_id, ', ' ) WITHIN GROUP (ORDER BY e.date_from)
AS events
FROM calendar, event e
WHERE calendar.cal_day BETWEEN e.date_from AND e.date_until
GROUP BY calendar.cal_day
HAVING COUNT( e.event_id ) > 1;
You can always change number in CONNECT BY LEVEL < :n or materialize calendar as a table.
sample input:
create table ns_123(a int,b date,c date);
insert into ns_123 values(179,'27-sep-2018','27-sep-2018');
insert into ns_123 values(181,'26-sep-2018','28-sep-2018');
insert into ns_123 values(180,'27-sep-2018','27-sep-2018');
select * from ns_123;
select distinct n1.a from ns_123 n1,ns_123 n2 where (n1.b-n2.b)>=(n1.c-n2.c);
sample output:
179
180
181

Oracle show all dates within a range and join to another table

I am trying to run a report to show a list of all dates within the past 90 days and then join that back to another table that has a date in one column and supplemental data in another. Here is how I am getting all the dates within a range:
select trunc(sysdate-90) + rownum -1 from all_objects where rownum <=90
The problem is joining this to another table on date. If I run:
select trunc(sysdate-90) + rownum -1, t.col2 from all_objects
left join (select date, col2 from table) t on trunc(sysdate-90) + rownum -1 = t.date
where rownum <=90
Then it only displays the first record from t.col2 for all values within the date range. How do I properly join these two tables?
Thanks
A better way to get the previous 90 days is to use the dual / connect by trick:
select trunc(sysdate-level) as the_date from dual connect by level <= 90
Now, you should be able to do something like:
select the_date, t.col2
from (select trunc(sysdate-level) as the_date from dual connect by level <= 90) date_tbl
left join t on date_tbl.the_date = t.date
Don't use all_objects, this is a really bad idea in terms of performance and your DBA will want to string you up. I have no idea where this idea of using all_objects came from originally, but I wish it would die!
In order to get a list of dates, you can implement a much simpler and fast bit of SQL:
SELECT TRUNC( SYSDATE ) - ROWNUM a_date
FROM DUAL
CONNECT BY ROWNUM <= (the number of days you want)
If you want an offset, you can add that
SELECT TRUNC( SYSDATE ) - ROWNUM + (the offset) a_date
FROM DUAL
CONNECT BY ROWNUM <= (the number of days you want)
If you want a different order you can wrap if in another select and then order:
SELECT a_date
FROM (
SELECT TRUNC( SYSDATE ) - ROWNUM + (the offset)
FROM DUAL
CONNECT BY ROWNUM <= (the number of days you want)
)
ORDER BY a_date ASC
You can then embed this into your existing statement, bearing in mind that you need it in a sub-select. The reason for that is so that your ROWNUM values don't get mixed up:
SELECT t.date, t.col_2
FROM ( SELECT TRUNC( SYSDATE ) - ROWNUM a_date
FROM DUAL
CONNECT BY ROWNUM <= 90 ) date_list
LEFT JOIN table t ON date_list.a_date = t.date
ORDER BY a_date DESC

Display Distinct Dates Not Exist In Table

I have a table called maximo_products and have a field called product_date.
I need to display all distinct dates which are not in in maximo_products table in MON-YYYY format
between Jan-2014 and Dec-2014.
How can do this?
SQLFiddle with table structure and records.
Sample output (dates which are not available in table)
MAR-2014
APR-2014
JUN-2014
JUL-2014
SEP-2014
To get the missing months, you need to generate all the months. The following uses a simple formulation for getting 12 months. It then uses not in to figure out which have no values:
with mons as (
select rownum r, add_months('01-JAN-2014', rownum - 1) as mon
from dual
connect by rownum <= 12
)
select *
from mons
where not exists (select 1
from maximo_products mp
where to_char(mp.product_date, 'YYYY-MM') =
to_char(mons.mon, 'YYYY-MM')
)
order by r;
EDIT:
You can move the definition of mons into a subquery:
select *
from (select rownum r, add_months('01-JAN-2014', rownum - 1) as mon
from dual
connect by rownum <= 12
) mons
where not exists (select 1
from maximo_products mp
where to_char(mp.product_date, 'YYYY-MM') =
to_char(mons.mon, 'YYYY-MM')
)
order by r;