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

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

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...

Oracle: Return the specific records based on one column date

I have a database structure as below.
period
month
start_date
1
April
2022-04-01
2
May
2022-05-07
3
June
2022-06-04
4
July
2022-07-02
5
August
2022-08-06
6
September
2022-09-03
7
October
2022-10-01
8
November
2022-11-05
9
December
2022-12-03
10
January
2023-01-01
11
February
2023-02-04
12
March
2023-03-04
End date of the year is 2023-03-31.
Based on current_date, how do I select the query to return where the current date falls under Period 6.
My current query as below.
SELECT period FROM table1 as a
WHERE
a.start_date = (SELECT MAX(start_date) FROM table1 as b WHERE
b.start_date <=current_date) and ROWNUM <= 1
Is there anyway to improve the current query which to avoid using subquery?
Today is September 22nd, so - would this do?
Some sample data:
SQL> with test (period, month, start_date) as
2 (select 1, 'april' , date '2022-04-01' from dual union all
3 select 5, 'august' , date '2022-08-06' from dual union all
4 select 6, 'september', date '2022-09-03' from dual union all
5 select 7, 'october' , date '2022-10-01' from dual union all
6 select 10, 'january' , date '2023-01-01' from dual union all
7 select 12, 'march' , date '2023-03-04' from dual
8 ),
Query begins here:
9 temp as
10 (select period, month, start_date,
11 row_number() over (order by start_date desc) rn
12 from test
13 where start_date <= sysdate
14 )
15 select period
16 from temp
17 where rn = 1
18 /
PERIOD
----------
6
SQL>
It still uses a subquery (or a CTE, as in my example), but - as opposed to your approach, it selects from the source table only once, so performance should be improved.
A few more tests: instead of sysdate (line #13), presume that today is September 2nd (which means that it is in period #5):
9 temp as
10 (select period, month, start_date,
11 row_number() over (order by start_date desc) rn
12 from test
13 where start_date <= date '2022-09-02'
14 )
15 select period
16 from temp
17 where rn = 1;
PERIOD
----------
5
SQL>
Or, if today were August 7th:
9 temp as
10 (select period, month, start_date,
11 row_number() over (order by start_date desc) rn
12 from test
13 where start_date <= date '2022-08-07'
14 )
15 select period
16 from temp
17 where rn = 1;
PERIOD
----------
5
SQL>
Your rule for the start_date appears to be:
If the month is January (first month of the calendar year) or April (typically, first month of the financial year) then use the 1st of that month;
Otherwise use the 1st Saturday of the month.
If that is the case then you can calculate the start date of the next month and use the query:
SELECT *
FROM table1
WHERE start_date <= SYSDATE
AND SYSDATE < CASE
WHEN EXTRACT(MONTH FROM ADD_MONTHS(start_date, 1))
IN (1, 4) -- 1st month of calendar or financial year
THEN TRUNC(ADD_MONTHS(start_date, 1), 'MM')
ELSE NEXT_DAY(TRUNC(ADD_MONTHS(start_date, 1), 'MM') - 1, 'SATURDAY')
END
Then, for your sample data:
CREATE TABLE table1 (
period NUMBER(2,0),
month VARCHAR2(9)
GENERATED ALWAYS AS (
CAST(
TO_CHAR(start_date, 'FXMonth', 'NLS_DATE_LANGUAGE=English')
AS VARCHAR2(9)
)
),
start_date DATE
);
INSERT INTO table1 (period, start_date)
SELECT LEVEL,
CASE
WHEN EXTRACT(MONTH FROM ADD_MONTHS(DATE '2022-04-01', LEVEL - 1))
IN (1, 4) -- 1st month of calendar or financial year
THEN ADD_MONTHS(DATE '2022-04-01', LEVEL - 1)
ELSE NEXT_DAY(ADD_MONTHS(DATE '2022-04-01', LEVEL - 1) - 1, 'SATURDAY')
END
FROM DUAL
CONNECT BY LEVEL <= 12;
Outputs:
PERIOD
MONTH
START_DATE
6
September
2022-09-03 00:00:00
fiddle

Calculate the number of days per month between two dates

Using Oracle 12c, I need to run a script on an existing summary table of projects. The summary table has a project, a start date, and an end date. I need to break this data out into the number of days per month for each project.
An example is Project A has a start date of 2/10/2016 and an end date of 3/10/2016. My ending result for this example should be:
Project A, February, 19
Project A, March, 10
This was an easier one as some dates may span 2 or 3 months. This doesn't seem too difficult but for some reason I'm having trouble wrapping my head around it and overthinking it. Does someone have an quick and easy solution to this? I would like to run this as a SQL statement but a PL/SQL script would also work.
In this solution we don't assume any prior knowledge of the time period covered. Also, this solution does not use joins (which may be important for performance).
with
-- begin test data (this section can be deleted)
inputs ( project, start_date, end_date ) as (
select 'A', date '2014-10-03', date '2014-12-15' from dual union all
select 'B', date '2015-03-01', date '2015-03-31' from dual union all
select 'C', date '2015-11-30', date '2016-03-01' from dual
),
-- end test data; solution begins here (it includes the word "with" from the first line)
prep ( project, end_date, dt ) as (
select project, end_date, start_date from inputs union all
select project, end_date, end_date + 1 from inputs union all
select project, end_date, add_months( trunc(start_date, 'mm'), level )
from inputs
connect by add_months (trunc(start_date, 'mm'), level) <= end_date
and prior project = project
and prior sys_guid() is not null
),
computations ( project, dt, month, day_count ) as (
select project, dt, to_char(dt, 'Mon-yyyy'),
lead(dt) over (partition by project order by dt) - dt
from prep
where dt <= end_date + 1
)
select project, month, day_count
from computations
where day_count > 0
order by project, dt
;
OUTPUT:
PROJECT MONTH DAY_COUNT
------- -------- ---------
A Oct-2014 29
A Nov-2014 30
A Dec-2014 15
B Mar-2015 31
C Nov-2015 1
C Dec-2015 31
C Jan-2016 31
C Feb-2016 29
C Mar-2016 1
9 rows selected
If you can do an assumption on the maximum number of days for a project (1000 in my example), you can use the following:
with yourTable(project, startDate, endDate) as
(
select 'Project a' as project,
date '2016-02-10' as startDate,
date '2016-03-10' as endDate
from dual
UNION ALL
select 'Project XX',
date '2016-01-01',
date '2016-01-10'
from dual
)
select project, to_char(startDate + n, 'MONTH'), count(1)
from yourTable
inner join (
select level n
from dual
connect by level <= 1000
)
on (startDate + n <= endDate)
group by project, to_char(startDate + n, 'MONTH')
The part with the CONNECT BY is used as a date generator, assuming that every project is at maximum 1000 days long; the external query uses the date generator to split the row of a project in many rows, one for each day between start and end date, and then aggregates by month and project to build the output.
A slightly different way, based on months and not days, could be:
select project, to_char(add_months(startDate, n ), 'MONTH'),
case
when trunc(add_months(startDate, n ), 'MONTH') = trunc(add_months(endDate, n ), 'MONTH')
then endDate - startDate +1
when trunc(add_months(startDate, n ), 'MONTH') <= startDate
then last_day(add_months(startDate, n)) - startDate
when last_day(add_months(startDate, n )) >= endDate
then endDate - trunc(add_months(startDate, n ), 'MONTH') +1
else
last_day(add_months(startDate, n )) - trunc(last_day(add_months(startDate, n )), 'MONTH')
end as numOfDays
from yourTable
inner join (
select level -1 n
from dual
connect by level <= 1000
)
on trunc(add_months(startDate, n ), 'MONTH') <= trunc(endDate, 'MONTH')
This is a bit more complicated, to handle the different cases, but more efficient, given that it works at month level, not day level
I think you're after something like:
WITH sample_data AS (SELECT 'A' PROJECT, to_date('10/02/2016', 'dd/mm/yyyy') start_date, to_date('10/03/2016', 'dd/mm/yyyy') end_date FROM dual UNION ALL
SELECT 'B' PROJECT, to_date('10/02/2016', 'dd/mm/yyyy') start_date, to_date('10/06/2016', 'dd/mm/yyyy') end_date FROM dual UNION ALL
SELECT 'C' PROJECT, to_date('10/02/2016', 'dd/mm/yyyy') start_date, to_date('18/02/2016', 'dd/mm/yyyy') end_date FROM dual)
SELECT PROJECT,
to_char(add_months(trunc(start_date, 'mm'), LEVEL -1), 'fmMonth yyyy', 'nls_date_language=english') mnth,
CASE WHEN trunc(end_date, 'mm') = add_months(trunc(start_date, 'mm'), LEVEL -1)
THEN end_date
ELSE add_months(trunc(start_date, 'mm'), LEVEL) -1
END - CASE WHEN trunc(start_date, 'mm') = add_months(trunc(start_date, 'mm'), LEVEL -1)
THEN start_date + 1
ELSE add_months(trunc(start_date, 'mm'), LEVEL -1)
END + 1 num_days
FROM sample_data
CONNECT BY PRIOR PROJECT = PROJECT
AND PRIOR sys_guid() IS NOT NULL
AND add_months(trunc(start_date, 'mm'), LEVEL -1) <= TRUNC(end_date, 'mm');
PROJECT MNTH NUM_DAYS
------- -------------- ----------
A February 2016 19
A March 2016 10
B February 2016 19
B March 2016 31
B April 2016 30
B May 2016 31
B June 2016 10
C February 2016 8
This uses the multi-row connect-by-level technique (the presence of the and prior sys_guid() is not null enables the connect by to loop through each row separately) to loop through each project row in the sample_data table (you presumably have the project information in a table already, so you wouldn't need to have the sample_data subquery at all; you could just reference your table directly in the main SQL).
We then compare the month of the start date with the month of the row being generated by the connect by, and if it's the same month, then we know we need to use the start date, otherwise we use the first of the month of the generated row; we do similarly for the end date.
That way, we can now subtract one from the other and make adjustments to make the calculation correct. You may need to tweak this yourself if you need a start and end date of the same day to count as 1 day, rather than 0 - it'll probably need an extra case statement to take account of when the start and end date are in the same month.
Using this approach won't limit your project length; it could be as long as you liked.
ETA: Looks like Mathguy posted an answer whilst I was typing out my answer, and whilst our basic methods are the same, mine doesn't use an analytic function to determine the difference in the number of days. You may or may not find their answer more performant than mine - you should test both to see which one works best with your data.

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;

How to exclude holidays between two dates?

I have two dates and I have to find out the number of Sundays and holidays fall between those two dates. Can I do this using BETWEEN? If so, how?
SELECT date1, date2, trunc(deposit_date - transaction_date) TOTAL
FROM Table_Name FULL OUTER JOIN Holidays ON date2 = hdate
WHERE hdate IN (date1, date2)
Using this I can definitely check whether there is a holiday on either of the two days, i.e. date1 or date2 but what I am not able to find out that whether there lies a holiday or a Sunday between these two dates. Help!
The solution you've posted is horribly inefficient; you can do all of this in a single SQL statement:
Firstly generate all possible dates between the two you have:
select trunc(:min_date) + level - 1
from dual
connect by level <= trunc(:min_date) - trunc(:max_date)
Then use your HOLIDAY table to restrict to what you want:
with all_dates as (
select trunc(:min_date) + level - 1 as the_date
from dual
connect by level <= trunc(:min_date) - trunc(:max_date)
)
select count(*)
from all_dates a
left outer join holiday b
on a.the_date = b.hdate
where b.hdate is null
and to_char(a.the_date, 'DY') <> 'SUN'
If you want to check if hdate is between the two dates you can query using
where hdate between date1 and date2
If you want to check if hdate is on the same day as date1 or date two you can query like this
where trunc(hdate) in (trunc(date1) ,trunc(date2))
The trunc function removed the time.
You should create a table with the holidays and maintain it on your own.
CREATE TABLE holidays
(
holiday VARCHAR2(100)
, d_date DATE
);
INSERT INTO holidays VALUES ('National Developer Day', DATE'2013-06-01');
SELECT *
FROM holidays;
-- National Developer Day 2013-06-01 00:00:00
The rest is just a matter of a SQL statment
Scenario 1: EXISTS
SELECT COUNT
(
CASE
WHEN TRIM(TO_CHAR(d.start_date_level, 'DAY')) = 'SUNDAY'
OR CASE
WHEN EXISTS (SELECT 1 FROM holidays h WHERE d.start_date_level = h.d_date)
THEN 1
ELSE NULL
END = 1
THEN 1
ELSE NULL
END
) AS holiday_check
FROM
(
SELECT start_date + (LEVEL - 1) AS start_date_level
FROM
(
SELECT start_date, end_date, end_date - start_date AS diff_date
FROM
(
SELECT TRUNC(ADD_MONTHS(SYSDATE, -2)) AS start_date
, TRUNC(SYSDATE) AS end_date
FROM DUAL
)
)
CONNECT BY
LEVEL <= (diff_date + 1)
) d
Scenario 2: LEFT JOIN
SELECT COUNT
(
CASE
WHEN TRIM(TO_CHAR(d.start_date_level, 'DAY')) = 'SUNDAY'
OR h.d_date IS NOT NULL
THEN 1
ELSE NULL
END
) AS holiday_check
FROM
(
SELECT start_date + (LEVEL - 1) AS start_date_level
FROM
(
SELECT start_date, end_date, end_date - start_date AS diff_date
FROM
(
SELECT TRUNC(ADD_MONTHS(SYSDATE, -2)) AS start_date
, TRUNC(SYSDATE) AS end_date
FROM DUAL
)
)
CONNECT BY
LEVEL <= (diff_date + 1)
) d
LEFT JOIN holidays h
ON d.start_date_level = h.d_date
9 Sundays + 1 "National Developer Day" = 10
CREATE OR REPLACE FUNCTION workdays (dt1 DATE, dt2 DATE) RETURN NUMBER IS
weekday_count NUMBER := 0;
date1 DATE := dt1;
date2 DATE := dt2;
cur_dt date;
holiday_count number;
begin
if date1 = date2 then
return 0;
end if;
cur_dt := transaction_date;
while cur_dt <= date2 loop
if cur_dt = date2 then
null;
else
SELECT count(*) INTO holiday_count
FROM holiday
WHERE hdate = cur_dt;
IF holiday_count = 0 THEN
IF to_char(cur_dt,'DY') NOT IN ('SUN') THEN
weekday_count := weekday_count + 1;
END IF;
END IF;
END IF;
cur_dt := cur_dt +1;
END LOOP;
RETURN weekday_count;
END;
And then I queried my database and got the right results. Do post if you have an optimal solution for this.
Here is an even better and efficient solution to the problem,
SELECT A.ID,
COUNT(A.ID) AS COUNTED
FROM tableA A
LEFT JOIN TableB B
ON A.tableB_id=B.id
LEFT JOIN holiday C
ON TRUNC(C.hdate) BETWEEN (TRUNC(a.date1) +1) AND TRUNC(B.date2)
WHERE c.hdate IS NOT NULL
GROUP BY A.ID;
where TableA contains date1 and tableB contains date2. Holiday contains the list of holidays and Sundays. And this query excludes 'date1' from the count.
RESULT LOGIC
trunc(date2) - trunc(date1) = x
x - result of the query
Make a table T$HOLIDAYS with your holidays (HDATE column). These dates will be excluded from calculation of working days within given period (sdate is start date and edate end date of period). Here is the function that calculates working days within given period excluding holidays, saturdays and sundays:
CREATE OR REPLACE FUNCTION WorkingDays(sdate IN DATE,edate IN DATE) RETURN NUMBER IS
days NUMBER;
BEGIN
WITH dates AS (SELECT sdate+LEVEL-1 AS d FROM DUAL CONNECT BY LEVEL<=edate-sdate+1)
SELECT COUNT(*) INTO days
FROM dates
WHERE d NOT IN (SELECT hdate FROM t$holidays) --exclude holidays
AND TO_CHAR(d,'D') NOT IN (6,7); --exclude saturdays + sundays
RETURN days;
END WorkingDays;
/
select sum(qq) from (
select case when to_number(to_char((trunc(sysdate-10) + level - 1),'D'))<=5 then 1 else 0 end as qq
from dual
connect by level <= trunc(sysdate) - trunc(sysdate-10))

Resources