T-SQL 获取两个日期之间的工作日数

声明:本页面是StackOverFlow热门问题的中英对照翻译,遵循CC BY-SA 4.0协议,如果您需要使用它,必须同样遵循CC BY-SA许可,注明原文地址和作者信息,同时你必须将它归于原作者(不是我):StackOverFlow 原文地址: http://stackoverflow.com/questions/14749877/
Warning: these are provided under cc-by-sa 4.0 license. You are free to use/share it, But you must attribute it to the original authors (not me): StackOverFlow

提示:将鼠标放在中文语句上可以显示对应的英文。显示中英文
时间:2020-09-01 13:32:43  来源:igfitidea点击:

T-SQL get number of working days between 2 dates

sqlsql-servertsql

提问by Rocshy

I want to calculate the number of working days between 2 given dates. For example if I want to calculate the working days between 2013-01-10 and 2013-01-15, the result must be 3 working days (I don't take into consideration the last day in that interval and I subtract the Saturdays and Sundays). I have the following code that works for most of the cases, except the one in my example.

我想计算两个给定日期之间的工作日数。例如,如果我想计算 2013-01-10 和 2013-01-15 之间的工作日,结果必须是 3 个工作日(我不考虑该间隔的最后一天,我减去星期六和星期日)。我有以下代码适用于大多数情况,除了我的示例中的代码。

  SELECT (DATEDIFF(day, '2013-01-10', '2013-01-15')) 
    - (CASE WHEN DATENAME(weekday, '2013-01-10') = 'Sunday' THEN 1 ELSE 0 END)
    - (CASE WHEN DATENAME(weekday, DATEADD(day, -1, '2013-01-15')) = 'Saturday' THEN 1 ELSE 0 END)

How can I accomplish this? Do I have to go through all the days and check them? Or is there an easy way to do this.

我怎样才能做到这一点?我是否必须经历所有的日子并检查它们?或者有没有一种简单的方法可以做到这一点。

回答by Aaron Bertrand

Please, please, please use a calendar table. SQL Server doesn't know anything about national holidays, company events, natural disasters, etc. A calendar table is fairly easy to build, takes an extremely small amount of space, and will be in memory if it is referenced enough.

请,请,请使用日历表。SQL Server 对国定假日、公司事件、自然灾害等一无所知。日历表相当容易构建,占用的空间极小,如果引用足够多,就会在内存中。

Here is an example that creates a calendar table with 30 years of dates (2000 -> 2029) but requires only 200 KB on disk (136 KB if you use page compression). That is almost guaranteed to be less than the memory grant required to process some CTE or other set at runtime.

这是一个示例,该示例创建了一个具有 30 年日期 (2000 -> 2029) 的日历表,但只需要 200 KB 的磁盘空间(如果使用页面压缩,则为 136 KB)。这几乎可以保证小于在运行时处理某些 CTE 或其他设置所需的内存授予。

CREATE TABLE dbo.Calendar
(
  dt DATE PRIMARY KEY, -- use SMALLDATETIME if < SQL Server 2008
  IsWorkDay BIT
);

DECLARE @s DATE, @e DATE;
SELECT @s = '2000-01-01' , @e = '2029-12-31';

INSERT dbo.Calendar(dt, IsWorkDay)
  SELECT DATEADD(DAY, n-1, '2000-01-01'), 1 
  FROM
  (
    SELECT TOP (DATEDIFF(DAY, @s, @e)+1) ROW_NUMBER() 
      OVER (ORDER BY s1.[object_id])
      FROM sys.all_objects AS s1
      CROSS JOIN sys.all_objects AS s2
  ) AS x(n);

SET DATEFIRST 1;

-- weekends
UPDATE dbo.Calendar SET IsWorkDay = 0 
  WHERE DATEPART(WEEKDAY, dt) IN (6,7);

-- Christmas
UPDATE dbo.Calendar SET IsWorkDay = 0 
  WHERE MONTH(dt) = 12
  AND DAY(dt) = 25
  AND IsWorkDay = 1;

-- continue with other holidays, known company events, etc.

Now the query you're after is quite simple to write:

现在您要查询的查询编写起来非常简单:

SELECT COUNT(*) FROM dbo.Calendar
  WHERE dt >= '20130110'
    AND dt <  '20130115'
    AND IsWorkDay = 1;

More info on calendar tables:

有关日历表的更多信息:

http://web.archive.org/web/20070611150639/http://sqlserver2000.databases.aspfaq.com/why-should-i-consider-using-an-auxiliary-calendar-table.html

http://web.archive.org/web/20070611150639/http://sqlserver2000.databases.aspfaq.com/why-should-i-think-using-an-auxiliary-calendar-table.html

More info on generating sets without loops:

有关无循环生成集的更多信息:

http://www.sqlperformance.com/tag/date-ranges

http://www.sqlperformance.com/tag/date-ranges

Also beware of little things like relying on the English output of DATENAME. I've seen several applications break because some users had a different language setting, and if you're relying on WEEKDAYbe sure you set your DATEFIRSTsetting appropriately...

还要注意一些小事情,比如依赖DATENAME. 我见过几个应用程序因为某些用户的语言设置不同而中断,如果您依赖,请WEEKDAY确保DATEFIRST正确设置您的设置...

回答by HeavenCore

For stuff like this i tend to maintain a calendar table that also includes bank holidays etc.

对于这样的事情,我倾向于维护一个日历表,其中还包括银行假期等。

The script i use for this is as follows (Note that i didnt write it @ i forget where i found it)

我为此使用的脚本如下(请注意,我没有写它@我忘记了在哪里找到它)

SET DATEFIRST 1
SET NOCOUNT ON
GO

--Create ISO week Function (thanks BOL)
CREATE FUNCTION ISOweek ( @DATE DATETIME )
RETURNS INT
AS 
    BEGIN
        DECLARE @ISOweek INT
        SET @ISOweek = DATEPART(wk, @DATE) + 1 - DATEPART(wk, CAST(DATEPART(yy, @DATE) AS CHAR(4)) + '0104')
        --Special cases: Jan 1-3 may belong to the previous year
        IF ( @ISOweek = 0 ) 
            SET @ISOweek = dbo.ISOweek(CAST(DATEPART(yy, @DATE) - 1 AS CHAR(4)) + '12' + CAST(24 + DATEPART(DAY, @DATE) AS CHAR(2))) + 1
        --Special case: Dec 29-31 may belong to the next year
        IF ( ( DATEPART(mm, @DATE) = 12 )
             AND ( ( DATEPART(dd, @DATE) - DATEPART(dw, @DATE) ) >= 28 )
           ) 
            SET @ISOweek = 1
        RETURN(@ISOweek)
    END
GO
--END ISOweek

--CREATE Easter algorithm function 
--Thanks to Rockmoose (http://www.sqlteam.com/forums/topic.asp?TOPIC_ID=45689)
CREATE FUNCTION fnDLA_GetEasterdate ( @year INT )
RETURNS CHAR(8)
AS 
    BEGIN
    -- Easter date algorithm of Delambre
        DECLARE @A INT ,
            @B INT ,
            @C INT ,
            @D INT ,
            @E INT ,
            @F INT ,
            @G INT ,
            @H INT ,
            @I INT ,
            @K INT ,
            @L INT ,
            @M INT ,
            @O INT ,
            @R INT              

        SET @A = @YEAR % 19
        SET @B = @YEAR / 100
        SET @C = @YEAR % 100
        SET @D = @B / 4
        SET @E = @B % 4
        SET @F = ( @B + 8 ) / 25
        SET @G = ( @B - @F + 1 ) / 3
        SET @H = ( 19 * @A + @B - @D - @G + 15 ) % 30
        SET @I = @C / 4
        SET @K = @C % 4
        SET @L = ( 32 + 2 * @E + 2 * @I - @H - @K ) % 7
        SET @M = ( @A + 11 * @H + 22 * @L ) / 451
        SET @O = 22 + @H + @L - 7 * @M

        IF @O > 31 
            BEGIN
                SET @R = @O - 31 + 400 + @YEAR * 10000
            END
        ELSE 
            BEGIN
                SET @R = @O + 300 + @YEAR * 10000
            END 

        RETURN @R
    END
GO
--END fnDLA_GetEasterdate

--Create the table
CREATE TABLE MyDateTable
    (
      FullDate DATETIME NOT NULL
                        CONSTRAINT PK_FullDate PRIMARY KEY CLUSTERED ,
      Period INT ,
      ISOWeek INT ,
      WorkingDay VARCHAR(1) CONSTRAINT DF_MyDateTable_WorkDay DEFAULT 'Y'
    )
GO
--End table create

--Populate table with required dates
DECLARE @DateFrom DATETIME ,
    @DateTo DATETIME ,
    @Period INT
SET @DateFrom = CONVERT(DATETIME, '20000101')
 --yyyymmdd (1st Jan 2000) amend as required
SET @DateTo = CONVERT(DATETIME, '20991231')
 --yyyymmdd (31st Dec 2099) amend as required
WHILE @DateFrom <= @DateTo 
    BEGIN
        SET @Period = CONVERT(INT, LEFT(CONVERT(VARCHAR(10), @DateFrom, 112), 6))
        INSERT  MyDateTable
                ( FullDate ,
                  Period ,
                  ISOWeek
                )
                SELECT  @DateFrom ,
                        @Period ,
                        dbo.ISOweek(@DateFrom)
        SET @DateFrom = DATEADD(dd, +1, @DateFrom)
    END
GO
--End population


/* Start of WorkingDays UPDATE */
UPDATE  MyDateTable
SET     WorkingDay = 'B' --B = Bank Holiday
--------------------------------EASTER---------------------------------------------
WHERE   FullDate = DATEADD(dd, -2, CONVERT(DATETIME, dbo.fnDLA_GetEasterdate(DATEPART(yy, FullDate)))) --Good Friday
        OR FullDate = DATEADD(dd, +1, CONVERT(DATETIME, dbo.fnDLA_GetEasterdate(DATEPART(yy, FullDate))))
 --Easter Monday
GO

UPDATE  MyDateTable
SET     WorkingDay = 'B'
--------------------------------NEW YEAR-------------------------------------------
WHERE   FullDate IN ( SELECT    MIN(FullDate)
                      FROM      MyDateTable
                      WHERE     DATEPART(mm, FullDate) = 1
                                AND DATEPART(dw, FullDate) NOT IN ( 6, 7 )
                      GROUP BY  DATEPART(yy, FullDate) )
---------------------MAY BANK HOLIDAYS(Always Monday)------------------------------
        OR FullDate IN ( SELECT MIN(FullDate)
                         FROM   MyDateTable
                         WHERE  DATEPART(mm, FullDate) = 5
                                AND DATEPART(dw, FullDate) = 1
                         GROUP BY DATEPART(yy, FullDate) )
        OR FullDate IN ( SELECT MAX(FullDate)
                         FROM   MyDateTable
                         WHERE  DATEPART(mm, FullDate) = 5
                                AND DATEPART(dw, FullDate) = 1
                         GROUP BY DATEPART(yy, FullDate) )
--------------------AUGUST BANK HOLIDAY(Always Monday)------------------------------
        OR FullDate IN ( SELECT MAX(FullDate)
                         FROM   MyDateTable
                         WHERE  DATEPART(mm, FullDate) = 8
                                AND DATEPART(dw, FullDate) = 1
                         GROUP BY DATEPART(yy, FullDate) )
--------------------XMAS(Move to next working day if on Sat/Sun)--------------------
        OR FullDate IN ( SELECT CASE WHEN DATEPART(dw, FullDate) IN ( 6, 7 ) THEN DATEADD(dd, +2, FullDate)
                                     ELSE FullDate
                                END
                         FROM   MyDateTable
                         WHERE  DATEPART(mm, FullDate) = 12
                                AND DATEPART(dd, FullDate) IN ( 25, 26 ) )
GO

---------------------------------------WEEKENDS--------------------------------------
UPDATE  MyDateTable
SET     WorkingDay = 'N'
WHERE   DATEPART(dw, FullDate) IN ( 6, 7 )
GO
/* End of WorkingDays UPDATE */

--SELECT * FROM MyDateTable ORDER BY 1
DROP FUNCTION fnDLA_GetEasterdate
DROP FUNCTION ISOweek
--DROP TABLE MyDateTable

SET NOCOUNT OFF

Once you have created the table, finding the number of working days is easy peasy:

创建表后,查找工作日数很容易:

SELECT  COUNT(FullDate) AS WorkingDays
FROM    dbo.tbl_WorkingDays
WHERE   WorkingDay = 'Y'
        AND FullDate >= CONVERT(DATETIME, '10/01/2013', 103)
        AND FullDate <  CONVERT(DATETIME, '15/01/2013', 103)

Note that this script includes UK bank holidays, i'm not sure what region you're in.

请注意,此脚本包括英国银行假期,我不确定您在哪个地区。

回答by SHennessy

Here's a simple function that counts working days not including Saturday and Sunday (when counting holidays isn't necessary):

这是一个计算工作日的简单函数,不包括周六和周日(当不需要计算假期时):

CREATE FUNCTION dbo.udf_GetBusinessDays (

@START_DATE DATE,
@END_DATE DATE

)
RETURNS INT
WITH EXECUTE AS CALLER
AS

BEGIN

 DECLARE @NUMBER_OF_DAYS INT = 0;
 DECLARE @DAY_COUNTER INT = 0;
 DECLARE @BUSINESS_DAYS INT = 0;
 DECLARE @CURRENT_DATE DATE;
 DECLARE @DAYNAME NVARCHAR(9)

 SET @NUMBER_OF_DAYS = DATEDIFF(DAY, @START_DATE, @END_DATE);

 WHILE @DAY_COUNTER <= @NUMBER_OF_DAYS
 BEGIN

    SET @CURRENT_DATE = DATEADD(DAY, @DAY_COUNTER, @START_DATE)
    SET @DAYNAME = DATENAME(WEEKDAY, @CURRENT_DATE)
    SET @DAY_COUNTER += 1

    IF @DAYNAME = N'Saturday' OR @DAYNAME = N'Sunday'
    BEGIN
        CONTINUE
    END
    ELSE
    BEGIN
        SET @BUSINESS_DAYS += 1
    END
 END

 RETURN @BUSINESS_DAYS
END
GO

回答by GarethD

This is the method I normally use (When not using a calendar table):

这是我通常使用的方法(不使用日历表时):

DECLARE @T TABLE (Date1 DATE, Date2 DATE);
INSERT @T VALUES ('20130110', '20130115'), ('20120101', '20130101'), ('20120611', '20120701');

SELECT  Date1, Date2, WorkingDays
FROM    @T t
        CROSS APPLY
        (   SELECT  [WorkingDays] = COUNT(*)
            FROM    Master..spt_values s
            WHERE   s.Number BETWEEN 1 AND DATEDIFF(DAY, t.date1, t.Date2)
            AND     s.[Type] = 'P'
            AND     DATENAME(WEEKDAY, DATEADD(DAY, s.number, t.Date1)) NOT IN ('Saturday', 'Sunday')
        ) wd

If like I do you have a table with holidays in you can add this in too:

如果像我一样你有一张带假期的桌子,你也可以添加这个:

SELECT  Date1, Date2, WorkingDays
FROM    @T t
        CROSS APPLY
        (   SELECT  [WorkingDays] = COUNT(*)
            FROM    Master..spt_values s
            WHERE   s.Number BETWEEN 1 AND DATEDIFF(DAY, t.date1, t.Date2)
            AND     s.[Type] = 'P'
            AND     DATENAME(WEEKDAY, DATEADD(DAY, s.number, t.Date1)) NOT IN ('Saturday', 'Sunday')
            AND     NOT EXISTS
                    (   SELECT  1
                        FROM    HolidayTable ht
                        WHERE   ht.Date = DATEADD(DAY, s.number, t.Date1)
                    )
        ) wd

The above will only work if your dates are within 2047 days of each other, if you are likely to be calculating larger date ranges you can use this:

以上仅当您的日期彼此相差 2047 天以内时才有效,如果您可能要计算更大的日期范围,则可以使用以下方法:

SELECT  Date1, Date2, WorkingDays
FROM    @T t
        CROSS APPLY
        (   SELECT  [WorkingDays] = COUNT(*)
            FROM    (   SELECT  [Number] = ROW_NUMBER() OVER(ORDER BY s.number)
                        FROM    Master..spt_values s
                                CROSS JOIN Master..spt_values s2
                    ) s
            WHERE   s.Number BETWEEN 1 AND DATEDIFF(DAY, t.date1, t.Date2)
            AND     DATENAME(WEEKDAY, DATEADD(DAY, s.number, t.Date1)) NOT IN ('Saturday', 'Sunday')
        ) wd

回答by KESAVAN PURUSOTHAMAN

I did my code in SQL SERVER 2008 (MS SQL) . It works fine for me. I hope it will help you.

我在 SQL SERVER 2008 (MS SQL) 中完成了我的代码。这对我来说可以。我希望它会帮助你。

     DECLARE  @COUNTS int,                       
     @STARTDATE  date,
     @ENDDATE date
      SET @STARTDATE ='01/21/2013' /*Start date in mm/dd/yyy */
      SET @ENDDATE ='01/26/2013' /*End date in mm/dd/yyy */
     SET @COUNTS=0
      WHILE (@STARTDATE<=@ENDDATE)

       BEGIN
    /*Check for holidays*/
   IF  ( DATENAME(weekday,@STARTDATE)<>'Saturday' and  DATENAME(weekday,@STARTDATE)<>'Sunday')                                

    BEGIN 

    SET @COUNTS=@COUNTS+1
    END
    SET @STARTDATE=DATEADD(day,1,@STARTDATE)
    END
    /* Display the no of working days */
    SELECT @COUNTS