T-SQL获得2个日期之间的工作日数

时间:2013-02-07 11:20:04

标签: sql sql-server tsql

我想计算两个给定日期之间的工作日数。例如,如果我想计算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)

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

5 个答案:

答案 0 :(得分:16)

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

以下是创建具有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.

现在你要查询的查询非常简单:

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

有关日历表的更多信息:

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

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

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

还要注意依赖DATENAME的英文输出之类的小事。我看到有几个应用程序中断了,因为有些用户使用了不同的语言设置,如果您依赖WEEKDAY,请确保正确设置DATEFIRST设置...

答案 1 :(得分:1)

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

我使用的脚本如下(请注意,我没有写它@我忘了我发现它的位置)

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

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

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)

请注意,此脚本包含英国银行假日,我不确定您所在的地区。

答案 2 :(得分:1)

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

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

答案 3 :(得分:0)

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

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

如果像我一样,你有一个假期表,你也可以添加:

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

以上内容仅适用于您的日期在彼此的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

答案 4 :(得分:0)

我在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
相关问题