在 Oracle SQL 中计算工作日(无函数或过程)
Posted
技术标签:
【中文标题】在 Oracle SQL 中计算工作日(无函数或过程)【英文标题】:Calculate business days in Oracle SQL(no functions or procedure) 【发布时间】:2013-01-31 15:27:09 【问题描述】:我正在尝试计算 Oracle 选择中两个日期之间的工作日。当我的计算给出给定日期的大多数结果正确时,我明白了(我将它与 excel 中的 NETWORKDAYS 进行比较)但有时它从 2 天到 -2 天不等 - 我不知道为什么......
这是我的代码:
SELECT
((to_char(CompleteDate,'J') - to_char(InstallDate,'J'))+1) - (((to_char(CompleteDate,'WW')+ (52 * ((to_char(CompleteDate,'YYYY') - to_char(InstallDate,'YYYY'))))) - to_char(InstallDate,'WW'))*2) as BusinessDays
FROM TABLE
谢谢!
【问题讨论】:
将您的解决方案添加为答案,这样您就可以“接受”它(这个网站最好有接受的答案的问题) 【参考方案1】:我将示例更改为更具可读性并返回总线计数。天之间。我不知道你为什么需要'J'- Julian 格式。只需要开始/安装和结束/完成日期。使用此方法,您将获得 2 个日期之间的正确天数。用你的替换我的日期,如果需要添加 NLS...:
SELECT Count(*) BusDaysBtwn
FROM
(
SELECT TO_DATE('2013-02-18', 'YYYY-MM-DD') + LEVEL-1 InstallDate -- MON or any other day
, TO_DATE('2013-02-25', 'YYYY-MM-DD') CompleteDate -- MON or any other day
, TO_CHAR(TO_DATE('2013-02-18', 'YYYY-MM-DD') + LEVEL-1, 'DY') InstallDay -- day of week
FROM dual
CONNECT BY LEVEL <= (TO_DATE('2013-02-25', 'YYYY-MM-DD') - TO_DATE('2013-02-18', 'YYYY-MM-DD')) -- end_date - start_date
)
WHERE InstallDay NOT IN ('SAT', 'SUN')
/
SQL> 5
【讨论】:
谢谢 - 我能用我的方法计算出来 @yochim - 与我们分享。 它是作为我原帖的答案分享的 @yochim - 我尝试使用你的方法并得到 2 作为回报......也许我不理解它或smth......这是查询 - 如果有兴趣请复制粘贴:SELECT TRUNC( (CompleteDate),'J') CompleteDate , TRUNC((InstallDate),'J') InstallDate , ((((TRUNC((CompleteDate),'D'))-(TRUNC((InstallDate),'D')) )/7)*2) “周?” FROM ( SELECT TO_DATE('2013-02-16', 'YYYY-MM-DD') InstallDate , TO_DATE('2013-02-23', 'YYYY-MM-DD') CompleteDate FROM dual ) / 我不知道你想用你的查询计算什么 :) 如果我将你的日期粘贴到我的查询中,我会得到 5 天 SELECT (TO_CHAR(TO_DATE('2013-02-23' ,'YYYY-MM-DD'),'J') - TO_CHAR(TO_DATE('2013-02-16','YYYY-MM-DD'),'J'))+1 - ((((TRUNC( TO_DATE('2013-02-23','YYYY-MM-DD'),'D'))-(TRUNC(TO_DATE('2013-02-16','YYYY-MM-DD'),'D' )))/7)*2) - (CASE WHEN TO_CHAR(TO_DATE('2013-02-16','YYYY-MM-DD'),'DY','nls_date_language=english')='SUN' THEN 1 ELSE 0 END) -(CASE WHEN TO_CHAR(TO_DATE('2013-02-23','YYYY-MM-DD'),'DY','nls_date_language=english')='SAT' THEN 1 ELSE 0 END) 为来自 DUAL 的工作日【参考方案2】:试试这个:
with holidays as
(
select d from (
select minDate + level -1 d
from (select min(submitDate) minDate, max (completeDate) maxDate
from t)
connect by level <= maxDate - mindate + 1)
where to_char(d, 'dy', 'nls_date_language=AMERICAN') not in ('sun' , 'sat')
)
select t.OrderNo, t.submitDate, t.completeDate, count(*) businessDays
from t join holidays h on h.d between t.submitDate and t.completeDate
group by t.OrderNo, t.submitDate, t.completeDate
order by orderno
Here is a sqlfiddle demo
【讨论】:
【参考方案3】:终于解决了:
SELECT OrderNumber, InstallDate, CompleteDate,
(TRUNC(CompleteDate) - TRUNC(InstallDate) ) +1 -
((((TRUNC(CompleteDate,'D'))-(TRUNC(InstallDate,'D')))/7)*2) -
(CASE WHEN TO_CHAR(InstallDate,'DY','nls_date_language=english')='SUN' THEN 1 ELSE 0 END) -
(CASE WHEN TO_CHAR(CompleteDate,'DY','nls_date_language=english')='SAT' THEN 1 ELSE 0 END) as BusinessDays
FROM Orders
ORDER BY OrderNumber;
感谢您的所有回复!
【讨论】:
我认为您采取了一种很好的方法,尝试使用简单的 SQL 函数而不是函数或日期表。但结果并不总是准确的。例如,如果您使用日期 2012-02-15 和 2012-02-18,即星期五和星期一,则结果为 3。 实际上它在所有日期都正常工作:) 你只是使用了错误的日期:) 2012-02-15 是星期三,2012-02-18 是星期六。我猜你想计算相同的天数,但在 2013 年 - 它给出了 2 天(再次正确!)。 我必须添加两个额外的案例才能为我工作。注意:根据我的 oracle 环境,一周从星期一开始。不确定这是否适用于 oracle。【参考方案4】:这是一个快速灵活的功能。您可以计算日期范围内的任何工作日。
CREATE OR REPLACE FUNCTION wfportal.cx_count_specific_weekdays( p_week_days VARCHAR2 DEFAULT 'MON,TUE,WED,THU,FRI'
, p_start_date DATE
, p_end_date DATE)
RETURN NUMBER
IS
/***************************************************************************************************************
*
* FUNCTION DESCRIPTION:
*
* This function calculates the total required week days in a date range.
*
* PARAMETERS:
*
* p_week_days VARCHAR2 The week days that need to be counted, comma seperated e.g. MON,TUE,WED,THU,FRU,SAT,SUN
* p_start_date DATE The start date
* p_end_date DATE The end date
*
* CHANGE history
*
* No. Date Changed by Change Description
* ---- ----------- ------------- -------------------------------------------------------------------------
* 0 07-May-2013 yourname Created
*
***************************************************************************************************************/
v_date_end_first_date_range DATE;
v_date_start_last_date_range DATE;
v_total_days_in_the_weeks NUMBER;
v_total_days_first_date_range NUMBER;
v_total_days_last_date_range NUMBER;
v_output NUMBER;
v_error_text CX_ERROR_CODES.ERROR_MESSAGE%TYPE;
--Count the required days in a specific date ranges by using a list of all the weekdays in that range.
CURSOR c_total_days ( v_start_date DATE
, v_end_date DATE ) IS
SELECT COUNT(*) total_days
FROM ( SELECT ( v_start_date + level - 1) days
FROM dual
CONNECT BY LEVEL <= ( v_end_date - v_start_date ) + 1
)
WHERE INSTR( ',' || p_week_days || ',', ',' || TO_CHAR( days, 'DY', 'NLS_DATE_LANGUAGE=english') || ',', 1 ) > 0
;
--Calculate the first and last date range by retrieving the first Sunday after the start date and the last Monday before the end date.
--Calculate the total amount of weeks in between and multiply that with the total required days.
CURSOR c_calculate_new_dates ( v_start_date DATE
, v_end_date DATE ) IS
SELECT date_end_first_date_range
, date_start_last_date_range
, (
(
( date_start_last_date_range - ( date_end_first_date_range + 1 ) )
) / 7
) * total_required_days total_days_in_the_weeks --The total amount of required days
FROM ( SELECT v_start_date + DECODE( TO_CHAR( v_start_date, 'DY', 'NLS_DATE_LANGUAGE=english')
, 'MON', 6
, 'TUE', 5
, 'WED', 4
, 'THU', 3
, 'FRI', 2
, 'SAT', 1
, 'SUN', 0
, 0 ) date_end_first_date_range
, v_end_date - DECODE( TO_CHAR( v_end_date, 'DY', 'NLS_DATE_LANGUAGE=english')
, 'MON', 0
, 'TUE', 1
, 'WED', 2
, 'THU', 3
, 'FRI', 4
, 'SAT', 5
, 'SUN', 6
, 0 ) date_start_last_date_range
, REGEXP_COUNT( p_week_days, ',' ) + 1 total_required_days --Count the commas + 1 to get the total required weekdays
FROM dual
)
;
BEGIN
--Verify that the start date is before the end date
IF p_start_date < p_end_date THEN
--Get the new calculated days.
OPEN c_calculate_new_dates( p_start_date, p_end_date );
FETCH c_calculate_new_dates INTO v_date_end_first_date_range
, v_date_start_last_date_range
, v_total_days_in_the_weeks;
CLOSE c_calculate_new_dates;
--Calculate the days in the first date range
OPEN c_total_days( p_start_date, v_date_end_first_date_range );
FETCH c_total_days INTO v_total_days_first_date_range;
CLOSE c_total_days;
--Calculate the days in the last date range
OPEN c_total_days( v_date_start_last_date_range, p_end_date );
FETCH c_total_days INTO v_total_days_last_date_range;
CLOSE c_total_days;
--Sum the total required days
v_output := v_total_days_first_date_range + v_total_days_last_date_range + v_total_days_in_the_weeks;
ELSE
v_output := 0;
END IF;
RETURN v_output;
EXCEPTION
WHEN OTHERS
THEN
RETURN NULL;
END cx_count_specific_weekdays;
/
【讨论】:
提问者没有请求任何函数或过程。这可能就是您的答案被否决的原因。【参考方案5】:给你...
-
首先查看假期表中有多少天,不包括周末。
获取两个日期之间的工作日(周一至周五),然后减去假期。
create or replace
FUNCTION calculate_business_days (p_start_date IN DATE, p_end_date IN DATE)
RETURN NUMBER IS
v_holidays NUMBER;
v_start_date DATE := TRUNC (p_start_date);
v_end_date DATE := TRUNC (p_end_date);
BEGIN
IF v_end_date >= v_start_date
THEN
SELECT COUNT (*)
INTO v_holidays
FROM holidays
WHERE day BETWEEN v_start_date AND v_end_date
AND day NOT IN (
SELECT hol.day
FROM holidays hol
WHERE MOD(TO_CHAR(hol.day, 'J'), 7) + 1 IN (6, 7)
);
RETURN GREATEST (NEXT_DAY (v_start_date, 'MON') - v_start_date - 2, 0)
+ ( ( NEXT_DAY (v_end_date, 'MON')
- NEXT_DAY (v_start_date, 'MON')
)
/ 7
)
* 5
- GREATEST (NEXT_DAY (v_end_date, 'MON') - v_end_date - 3, 0)
- v_holidays;
ELSE
RETURN NULL;
END IF;
END calculate_business_days;
之后你可以测试一下,比如:
select
calculate_business_days('21-AUG-2013','28-AUG-2013') as business_days
from dual;
【讨论】:
提问者没有请求任何函数或过程。这可能就是您的答案被否决的原因。【参考方案6】:还有另一种更简单的方法,使用 connect by 和 dual...
with t as (select to_date('30-sep-2013') end_date, trunc(sysdate) start_date from dual)select count(1) from dual, t where to_char(t.start_date + level, 'D') not in (1,7) connect by t.start_date + level <= t.end_date;
通过连接,您可以获得从 start_date 到 end_date 的所有日期。然后你可以排除你不需要的日期,只计算需要的日期。
【讨论】:
【参考方案7】:我考虑了上面讨论的所有不同方法,并提出了一个简单的查询,它为我们提供了两个日期之间一年中每个月的工作日数:
WITH test_data AS
(
SELECT TO_DATE('01-JAN-14') AS start_date,
TO_DATE('31-DEC-14') AS end_date
FROM dual
),
all_dates AS
(
SELECT td.start_date, td.end_date, td.start_date + LEVEL-1 as week_day
FROM test_data td
CONNECT BY td.start_date + LEVEL-1 <= td.end_date)
SELECT TO_CHAR(week_day, 'MON'), COUNT(*)
FROM all_dates
WHERE to_char(week_day, 'dy', 'nls_date_language=AMERICAN') NOT IN ('sun' , 'sat')
GROUP BY TO_CHAR(week_day, 'MON');
请随时根据需要修改查询。
【讨论】:
非常感谢您的询问。我对其进行了修改,以减少公司假期的工作日数,现在可以单独使用此计算。【参考方案8】:公认的解决方案非常接近,但在某些情况下似乎是错误的(例如,2015 年 2 月 1 日至 2015 年 2 月 28 日或 2015 年 5 月 1 日至 2015 年 5 月 31 日)。这是一个精致的版本...
end_date-begin_date+1 /* total days */
- TRUNC(2*(end_date-begin_date+1)/7) /* weekend days in whole weeks */
- (CASE
WHEN TO_CHAR(begin_date,'D') = 1 AND REMAINDER(end_date-begin_date+1,7) > 0 THEN 1
WHEN TO_CHAR(begin_date,'D') = 8 - REMAINDER(end_date-begin_date+1,7) THEN 1
WHEN TO_CHAR(begin_date,'D') > 8 - REMAINDER(end_date-begin_date+1,7) THEN 2
ELSE 0
END) /* weekend days in partial week */
AS business_days
处理 7 的倍数(整周)的部分很好。但是,在考虑部分周部分时,它取决于周偏移量和部分部分的天数,根据以下矩阵...
654321
1N 111111
2M 100000
3T 210000
4W 221000
5R 222100
6F 222210
7S 222221
【讨论】:
【参考方案9】:我看到标记的最终解决方案并不总是正确的。假设 InstallDate 是本月的 1 号(如果是星期六),而 CompleteDate 是本月的 16 号(如果是星期日)
在这种情况下,实际工作日是 10,但标记的查询结果将给出答案为 12。所以,我们也必须处理这种类型的情况,我使用了这种情况
(CASE WHEN TO_CHAR(InstallDate,'DY','nls_date_language=english')='SAT' AND TO_CHAR(CompleteDate,'DY','nls_date_language=english')='SUN' THEN 2 ELSE 0 END
行来处理它。
SELECT OrderNumber, InstallDate, CompleteDate,
(TRUNC(CompleteDate) - TRUNC(InstallDate) ) +1 -
((((TRUNC(CompleteDate,'D'))-(TRUNC(InstallDate,'D')))/7)*2) -
(CASE WHEN TO_CHAR(InstallDate,'DY','nls_date_language=english')='SUN' THEN 1 ELSE 0 END) -
(CASE WHEN TO_CHAR(CompleteDate,'DY','nls_date_language=english')='SAT' THEN 1 ELSE 0 END) -
(CASE WHEN TO_CHAR(InstallDate,'DY','nls_date_language=english')='SAT' AND TO_CHAR(CompleteDate,'DY','nls_date_language=english')='SUN' THEN 2 ELSE 0 END)as BusinessDays
FROM Orders
ORDER BY OrderNumber;
【讨论】:
作为后期更新:这是不正确的,您的额外行破坏了代码。最终解决方案确实正确处理了给定的情况,因为TRUNC(<date>,'D')
的行为是四舍五入到工作周的第一天(尽管这可能取决于您的数据库认为“一周的第一天”)。跨度>
【参考方案10】:
要删除星期日和星期六,您可以使用它
SELECT Base_DateDiff
- (floor((Base_DateDiff + 0 + Start_WeekDay) / 7))
- (floor((Base_DateDiff + 1 + Start_WeekDay) / 7))
FROM (SELECT 1 + TRUNC(InstallDate) - TRUNC(InstallDate, 'IW') Start_WeekDay
, CompleteDate - InstallDate + 1 Base_DateDiff
FROM TABLE) a
Base_DateDiff
计算两个日期之间的天数(floor((Base_DateDiff + 0 + Start_WeekDay) / 7))
计算星期日的数量(floor((Base_DateDiff + 1 + Start_WeekDay) / 7))
计算星期六的数量
1 + TRUNC(InstallDate) - TRUNC(InstallDate, 'IW')
星期一得到 1 到星期日得到 7
【讨论】:
【参考方案11】:此查询可用于从给定日期向后退 N 天(仅限工作日)
例如,从 2017-05-17 后退 15 天:
select date_point, closest_saturday - (15 - offset + floor((15 - offset) / 6) * 2) from(
select date_point,
closest_saturday,
(case
when weekday_num > 1 then
weekday_num - 2
else
0
end) offset
from (
select to_date('2017-05-17', 'yyyy-mm-dd') date_point,
to_date('2017-05-17', 'yyyy-mm-dd') - to_char(to_date('2017-05-17', 'yyyy-mm-dd'), 'D') closest_saturday,
to_char(to_date('2017-05-17', 'yyyy-mm-dd'), 'D') weekday_num
from dual
))
一些简短的解释:假设我们想从给定日期向后退 N 天 - 查找小于或等于给定日期的最近的星期六。 - 从最近的星期六开始,向后退(N - 偏移)天。 offset 是最近的星期六与给定日期(不包括给定日期)之间的工作日数。
*要从星期六返回 M 天(仅限工作日),请使用此公式 DateOfMonthOfTheSaturday - [M + Floor(M / 6) * 2]
【讨论】:
【参考方案12】:这将返回工作日:
(CompleteDate-InstallDate)-2*FLOOR((CompleteDate-InstallDate)/7)-
DECODE(SIGN(TO_CHAR(CompleteDate,'D')-
TO_CHAR(InstallDate,'D')),-1,2,0)+DECODE(TO_CHAR(CompleteDate,'D'),7,1,0)-
DECODE(TO_CHAR(InstallDate,'D'),7,1,0) as BusinessDays,
【讨论】:
以上是关于在 Oracle SQL 中计算工作日(无函数或过程)的主要内容,如果未能解决你的问题,请参考以下文章