在 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(&lt;date&gt;,'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 中计算工作日(无函数或过程)的主要内容,如果未能解决你的问题,请参考以下文章

PL/SQL Oracle 无提示错误

Oracle意外赢官司,程序员或过苦日子

插入或更新的值过高或过低时的 SQL 触发器

SQL计算无工作天数

oracle sql单行函数 常用函数实例

SQL函数和存储过程的区别