PL/SQL - Calculate distinct days between overlapping time periods - oracle

Imagine this scenario (YYYY/MM/DD):
Start date: 2015/01/01 End date: 2015/08/10
Start date: 2014/10/03 End date: 2015/07/06
Start date: 2015/09/30 End date: 2016/04/28
Using PL/SQL can I calculate the distinct days between these overlapping dates?
Edit: My table has 2 DATE columns, Start_Date and End_Date. The result I'm expecting is 515 days ((2015/08/10 - 2014/10/03) + (2016/04/28 -2015/09/30))

You can do also with pure SQL (no need for PL/SQL):
with
minmax as (select min(start_date) min_dt, max(end_date) max_dt from myTable ),
dates as (
SELECT min_dt + rownum-1 dt1
FROM minmax CONNECT BY ROWNUM <= (max_dt - min_dt +1)
)
select count(*) from dates
where exists(
select 1 from MyTable T2
where dates.dt1 between T2.start_date and T2.end_date )
NOTE: an idea, written from head, not tested. Adapt generated dates as needed, with start date and needed length.
Hope it helps.
EDIT: Using actual table dates

SQL Fiddle
Oracle 11g R2 Schema Setup:
CREATE TABLE DATES ( start_date, end_date ) AS
SELECT DATE '2015-01-01', DATE '2015-08-10' FROM DUAL
UNION ALL SELECT DATE '2014-10-03', DATE '2015-07-06' FROM DUAL
UNION ALL SELECT DATE '2015-09-30', DATE '2016-04-28' FROM DUAL
Query 1:
SELECT COUNT( DISTINCT COLUMN_VALUE ) AS number_of_days
FROM DATES d,
TABLE(
CAST(
MULTISET(
SELECT d.START_DATE + LEVEL - 1
FROM DUAL
CONNECT BY d.START_DATE + LEVEL - 1 < d.END_DATE
)
AS SYS.ODCIDATELIST
)
)
ORDER BY 1
Results:
| NUMBER_OF_DAYS |
|----------------|
| 522 |
Query 2 - Check:
SELECT DATE '2015-08-10' - DATE '2014-10-03'
+ DATE '2016-04-28' - DATE '2015-09-30'
FROM DUAL
Results:
| DATE'2015-08-10'-DATE'2014-10-03'+DATE'2016-04-28'-DATE'2015-09-30' |
|---------------------------------------------------------------------|
| 522 |

Related

Stop condition for recursive CTE on Oracle (ORA-32044)

I have the following recursive CTE which splits each element coming from base per month:
with
base (id, start_date, end_date) as (
select 1, date '2022-01-15', date '2022-03-15' from dual
union
select 2, date '2022-09-15', date '2022-12-31' from dual
union
select 3, date '2023-09-15', date '2023-09-25' from dual
),
split (id, start_date, end_date) as (
select base.id, base.start_date, least(last_day(base.start_date), base.end_date) from base
union all
select base.id, split.end_date + 1, least(last_day(split.end_date + 1), base.end_date) from base join split on base.id = split.id and split.end_date < base.end_date
)
select * from split order by id, start_date, end_date;
It works on Oracle and gives the following result:
id
start_date
end_date
1
2022-01-15
2022-01-31
1
2022-02-01
2022-02-28
1
2022-03-01
2022-03-15
2
2022-09-15
2022-09-30
2
2022-10-01
2022-10-31
2
2022-11-01
2022-11-30
2
2022-12-01
2022-12-31
3
2023-09-15
2023-09-25
The two following stop conditions work correctly:
... from base join split on base.id = split.id and split.end_date < base.end_date
... from base, split where base.id = split.id and split.end_date < base.end_date
The following one fails with the message ORA-32044: cycle detected while executing recursive WITH query:
... from base join split on base.id = split.id where split.end_date < base.end_date
I fail to understand how the last one is different from the two others.
It looks like a bug as all your queries should result in identical explain plans.
However, you can rewrite the recursive sub-query without the join (and using a SEARCH clause so you may not have to re-order the query later):
WITH split (id, start_date, month_end, end_date) AS (
SELECT id,
start_date,
LEAST(
ADD_MONTHS(TRUNC(start_date, 'MM'), 1) - INTERVAL '1' SECOND,
end_date
),
end_date
FROM base
UNION ALL
SELECT id,
month_end + INTERVAL '1' SECOND,
LEAST(
ADD_MONTHS(month_end, 1),
end_date
),
end_date
FROM split
WHERE month_end < end_date
) SEARCH DEPTH FIRST BY id, start_date SET order_id
SELECT id,
start_date,
month_end AS end_date
FROM split;
Note: if you want to just use values at midnight rather than the entire month then use INTERVAL '1' DAY rather than 1 second.
Which, for the sample data:
CREATE TABLE base (id, start_date, end_date) as
select 1, date '2022-01-15', date '2022-04-15' from dual union all
select 2, date '2022-09-15', date '2022-12-31' from dual union all
select 3, date '2023-09-15', date '2023-09-25' from dual;
Outputs:
ID
START_DATE
END_DATE
1
2022-01-15T00:00:00Z
2022-01-31T23:59:59Z
1
2022-02-01T00:00:00Z
2022-02-28T23:59:59Z
1
2022-03-01T00:00:00Z
2022-03-31T23:59:59Z
1
2022-04-01T00:00:00Z
2022-04-15T00:00:00Z
2
2022-09-15T00:00:00Z
2022-09-30T23:59:59Z
2
2022-10-01T00:00:00Z
2022-10-31T23:59:59Z
2
2022-11-01T00:00:00Z
2022-11-30T23:59:59Z
2
2022-12-01T00:00:00Z
2022-12-31T00:00:00Z
3
2023-09-15T00:00:00Z
2023-09-25T00:00:00Z
fiddle
It's because WHERE and ON conditions are not evaluated at the same level:
when the condition is in the ON clause it's limiting the rows concerned by the JOIN, where it's in the WHERE it's filtering the results after the JOIN has been applied, and since a recursive CTE see all rows selected up to now...

How to get last workday before holiday in Oracle [duplicate]

This question already has answers here:
How to get the previous working day from Oracle?
(4 answers)
Closed 1 year ago.
need help for some oracle stuff ..
I need to get Day-1 from sysdate, holiday and weekend will be excluded .
And for holiday, we need to get the range to get the last workday before holiday.
The start date and end date will coming from my holiday table.
ex :
Holiday Table
HolidayName
Start_date
End_Date
holiday1
5th Aug'21
6th Aug'21
condition :
this query run on 9th Aug 2021
expected result :
4th Aug'21
I've tried some query and function but I just can't get what I need.
Thanks a lot for help!
Here's one way to do it.
select max(d) as last_workday
from (select trunc(sysdate)-level as d from dual connect by level < 30) prior_month
where to_char(d, 'DY') not in ('SAT','SUN')
and not exists (select holidayname from holiday_table
where prior_month.d between start_date and end_date)
;
Without seeing your Holiday table, it's hard to say how many days back you would need to look to find the last workday. If you have a holiday that lasts for more than 30 days, you'll need to change the 30 to a larger number.
You can use a simple case expression to determine what day of the week the start of your holiday is, then subtract a number of days based on that.
WITH
holiday (holidayname, start_date, end_date)
AS
(SELECT 'holiday1', DATE '2021-8-5', DATE '2021-8-6' FROM DUAL
UNION ALL
SELECT 'Christmas', DATE '2021-12-25', DATE '2021-12-26' FROM DUAL
UNION ALL
SELECT 'July 4th', DATE '2021-7-4', DATE '2021-7-5' FROM DUAL)
SELECT holidayname,
start_date,
end_date,
start_date - CASE TO_CHAR (start_date, 'Dy') WHEN 'Mon' THEN 3 WHEN 'Sun' THEN 2 ELSE 1 END AS prior_business_day
FROM holiday;
HOLIDAYNAME START_DATE END_DATE PRIOR_BUSINESS_DAY
______________ _____________ ____________ _____________________
holiday1 05-AUG-21 06-AUG-21 04-AUG-21
Christmas 25-DEC-21 26-DEC-21 24-DEC-21
July 4th 04-JUL-21 05-JUL-21 02-JUL-21
You can use a recursive sub-query factoring clause from this answer:
WITH start_date (dt) AS (
SELECT DATE '2021-05-02' FROM DUAL
),
days ( dt, day, found ) AS (
SELECT dt,
TRUNC(dt) - TRUNC(dt, 'IW'),
0
FROM start_date
UNION ALL
SELECT dt - CASE day WHEN 0 THEN 3 WHEN 6 THEN 2 ELSE 1 END,
CASE WHEN day IN (0, 6, 5) THEN 4 ELSE day - 1 END,
CASE WHEN h.start_date IS NULL THEN 1 ELSE 0 END
FROM days d
LEFT OUTER JOIN holidays h
ON ( dt - CASE day WHEN 0 THEN 3 WHEN 6 THEN 2 ELSE 1 END
BETWEEN h.start_date AND h.end_date )
WHERE found = 0
)
SELECT dt
FROM days
WHERE found = 1;
Which, for the sample data:
CREATE TABLE holidays (HolidayName, Start_date, End_Date) AS
SELECT 'holiday1', DATE '2021-08-05', DATE '2021-08-06' FROM DUAL;
Outputs:
DT
2021-08-04 00:00:00
db<>fiddle here
Don't know if it's very efficient. Did it just for fun
create table holidays (
holiday_name varchar2(100) primary key,
start_date date not null,
end_date date not null
)
/
Table created
insert into holidays (holiday_name, start_date, end_date)
values ('holiday1', date '2021-08-05', date '2021-08-06');
1 row inserted
with days_before(day, wrk_day) as
(select trunc(sysdate - 1) d,
case
when h.holiday_name is not null then 0
when to_char(trunc(sysdate - 1), 'D') in ('6', '7') then 0
else 1
end work_day
from dual
left join holidays h
on trunc(sysdate - 1) between h.start_date and h.end_date
union all
select db.day - 1,
case
when h.holiday_name is not null then 0
when to_char(db.day - 1, 'D') in ('6', '7') then 0
else 1
end work_day
from days_before db
left join holidays h
on db.day - 1 between h.start_date and h.end_date
where db.wrk_day = 0) search depth first by day set order_no
select day from days_before where wrk_day = 1;
DAY
-----------
04.08.2021

Convert time from a format to an int in Oracle

I can't seem to figure this out. I have some rows with time in the format 00:00:00 (hh:mm:ss) and i need to calculate the total time it takes for a task.
I am unable to sum this data. Can someone advise on a way to convert this to a format i can sum or a method to calculate the total time for the task.
Thanks for any assistance. This is in an Oracle DB.
Convert your time string to a date and subtract the equivalent date at midnight to give you an number as a fraction of a day. You can then sum this number and convert it to an interval:
Oracle Setup:
CREATE TABLE test_data( value ) AS
SELECT '01:23:45' FROM DUAL UNION ALL
SELECT '12:34:56' FROM DUAL UNION ALL
SELECT '23:45:00' FROM DUAL;
Query:
SELECT NUMTODSINTERVAL(
SUM( TO_DATE( value, 'HH24:MI:SS' ) - TO_DATE( '00:00:00', 'HH24:MI:SS' ) ),
'DAY'
) AS total_time_taken
FROM test_data;
Output:
| TOTAL_TIME_TAKEN |
| :---------------------------- |
| +000000001 13:43:41.000000000 |
db<>fiddle here
Update including durations longer than 23:59:59.
Oracle Setup:
CREATE TABLE test_data( value ) AS
SELECT '1:23:45' FROM DUAL UNION ALL
SELECT '12:34:56' FROM DUAL UNION ALL
SELECT '23:45:00' FROM DUAL UNION ALL
SELECT '48:00:00' FROM DUAL;
Query:
SELECT NUMTODSINTERVAL(
SUM(
DATE '1970-01-01'
+ NUMTODSINTERVAL( SUBSTR( value, 1, HM - 1 ), 'HOUR' )
+ NUMTODSINTERVAL( SUBSTR( value, HM + 1, MS - HM - 1 ), 'MINUTE' )
+ NUMTODSINTERVAL( SUBSTR( value, MS + 1 ), 'SECOND' )
- DATE '1970-01-01'
),
'DAY'
) AS total_time
FROM (
SELECT value,
INSTR( value, ':', 1, 1 ) AS HM,
INSTR( value, ':', 1, 2 ) AS MS
FROM test_data
);
Output:
| TOTAL_TIME |
| :---------------------------- |
| +000000003 13:43:41.000000000 |
db<>fiddle here
Even better would be if you changed your table to hold the durations as intervals rather than as strings then everything becomes much simpler:
Oracle Setup:
CREATE TABLE test_data( value ) AS
SELECT INTERVAL '1:23:45' HOUR TO SECOND FROM DUAL UNION ALL
SELECT INTERVAL '12:34:56' HOUR TO SECOND FROM DUAL UNION ALL
SELECT INTERVAL '23:45:00' HOUR TO SECOND FROM DUAL UNION ALL
SELECT INTERVAL '48:00:00' HOUR TO SECOND FROM DUAL;
Query:
SELECT NUMTODSINTERVAL(
SUM( DATE '1970-01-01' + value - DATE '1970-01-01' ),
'DAY'
) AS total_time
FROM test_data;
Output:
| TOTAL_TIME |
| :---------------------------- |
| +000000003 13:43:41.000000000 |
db<>fiddle here

Finding no. of working days in a month till sysdate/ today's date is it is current month

I am using the following query to find out the number of working days in a month excluding the weekends
select payroll_id as payrollId,
(select count(*)
from ( select rownum rnum
from all_objects
where rownum <= to_date(to_char(last_day(to_date('01-'||'MAY'||'2017','DD-MM-YYYY')),'DD')||'MAY'||'2017','DD-MM-YYYY') - to_date('01-'||'MAY'||'2017','DD-MM-YYYY')+1 )
where to_char( to_date('01-'||'MAY'||'2017','DD-MM-YYYY')+rnum-1, 'DY' )
not in ( 'SAT', 'SUN' )) - (select count(*) from admin_holiday where to_char(holiday_date,'DD-MON-YYYY') like '%-MAY-2017%' and holiday_type_id=1) days
from employee where DEL_FLAG=1 order by payrollId
Here, payrollId is the employee_id in employee table and admin_holiday is a table that contains information about the national holidays in a month
My requirement is that if the month is current month then the working days should be upto today's date. For example the current month is MAY/2017 and till today(i.e 22/05/2017) the working days count excluding weekends is 15 (10/05/2017 is a national holiday according to Hindu calendar.
How do I obtain the required result.
NOTE this action has to be performed in a single select statement and no other pl/sql blocks, etc.
Here is how I would approach the equivalent query. By placing the date range calculation into a small cross joined query you can access the result easily throughout the remainder of the query.
SELECT
e.payroll_id AS payrollId
, cj. working_days
FROM employee e
cross join (
select
GREATEST(NEXT_DAY(start_date, 'MON') - start_date - 2, 0)
+ ((NEXT_DAY(end_date, 'MON') - NEXT_DAY(start_date, 'MON'))/7)*5
- GREATEST(NEXT_DAY(end_date, 'MON') - end_date - 3, 0)
- (
SELECT COUNT(holiday)
FROM admin_holiday
WHERE holiday_date BETWEEN start_date AND end_date
)
as working_days
FROM (
select
to_date('20170501','yyyymmdd') as start_date
, to_date('20170522','yyyymmdd') as end_date
from dual
)
) cj
WHERE e.DEL_FLAG = 1
ORDER BY e.payrollId
The working days logic is:
Calculate leading days in first week + (Count the number on mon/fri periods) * 5 - number of trailing days in final week - the additional holidays in a table:
select
GREATEST(NEXT_DAY(start_date, 'MON') - start_date - 2, 0)
+ ((NEXT_DAY(end_date, 'MON') - NEXT_DAY(start_date, 'MON'))/7)*5
- GREATEST(NEXT_DAY(end_date, 'MON') - end_date - 3, 0)
- (
SELECT COUNT(holiday)
FROM admin_holiday
WHERE holiday_date BETWEEN start_date AND end_date
)
as working_days
, start_date
, end_date
, holidays
, end_date - start_date
FROM (
select
trunc(sysdate,'MM') as start_date
, trunc(sysdate) as end_date
from dual
)
example (not there is zero holidays here):
+--------------+--------------+-------------+------------+-------------------------+
| WORKING_DAYS | START_DATE | END_DATE | HOLIDAYS | END_DATE-START_DATE |
+--------------+--------------+-------------+------------+-------------------------+
| 16 | 01.05.2017 | 22.05.2017 | 0 | 21 |
+--------------+--------------+-------------+------------+-------------------------+
To manually set the dates you can do this:
select
GREATEST(NEXT_DAY(start_date, 'MON') - start_date - 2, 0)
+ ((NEXT_DAY(end_date, 'MON') - NEXT_DAY(start_date, 'MON'))/7)*5
- GREATEST(NEXT_DAY(end_date, 'MON') - end_date - 3, 0)
- (
SELECT COUNT(holiday)
FROM admin_holiday
WHERE holiday_date BETWEEN start_date AND end_date
)
as working_days
FROM (
select
to_date('20170501','yyyymmdd') as start_date
, to_date('20170522','yyyymmdd') as end_date
from dual
)

Retrieve rows with 0 count from Oracle

I am woking on a query which can give back the count divided by month about the offices that will be closed this summer.
SELECT
qa.tmonth,
COUNT(qa.tmonth) AS qtn
FROM
(
SELECT TO_CHAR(CLOSURE_DATE, 'yyyymm') AS tmonth
FROM Holidays
WHERE CLOSURE_DATE >= TO_DATE('20160501', 'YYYY-MM-DD') AND
CLOSURE_DATE <= TO_DATE('20160901', 'YYYY-MM-DD')
) qa
GROUP BY qa.tmonth;
Since the months: May, June, August and September no office will be closed the output is the following:
TMONTH|QTN
201607|80
But I need a thing like this
TMONTH|QTN
201605|0
201606|0
201607|80
201608|0
201609|0
How could I achieve that?
Thanks to all!
You can try with something like this:
SQL> with holidays(closure_date) as
2 (
3 select date '2016-07-01' from dual union all
4 select date '2016-07-02' from dual union all
5 select date '2016-07-03' from dual union all
6 select date '2016-07-04' from dual union all
7 select date '2016-07-05' from dual
8 )
9 select count(closure_date) as closure_days, to_char(day, 'yyyymm') as month
10 from (
11 select date '2016-05-01' + level -1 as day
12 from dual
13 connect by date '2016-05-01' + level -1 <= date '2016-09-30'
14 ) days
15 left outer join holidays
16 on (day = closure_date)
17 group by to_char(day, 'yyyymm') ;
CLOSURE_DAYS MONTH
------------ ------
0 201608
5 201607
0 201606
0 201605
0 201609
SQL>
This uses a query to build the list of all the days between a starting and an ending date; I used 01/05 and 30/09 and called it days.
Then it queries days with the holidays table in outer join; this way you can count only the days for which there is a corrensponding value in the closure days list, thus counting the closure days for each day, month year; the aggregation for year and month completes the job
A similar approach like above. Tip: You can execute the two sub-queries separately, to analyse the logic.
select to_char (m.month, 'yyyymm') as TMONTH, m.month,
nvl (h.qtn, 0) as QTN
from
(
SELECT add_months(trunc (SYSDATE, 'MONTH'), -(LEVEL-1)) as MONTH
FROM dual
CONNECT BY LEVEL <= 12 -- generate a list of the last 12 month
) m
left join
(
SELECT trunc (closure_date, 'MONTH') as MONTH,
count (*) as QTN
FROM Holidays
group by trunc (closure_date, 'MONTH')
) h
on m.MONTH = h.MONTH
where m.month between DATE '2016-01-01' and sysdate
order by TMONTH desc;

Resources