2013-02-07 365 views
3

我想计算两个给定日期之间的工作天数。例如,如果我想计算2013年1月1日至2013年1月15日之间的工作日,结果必须为3个工作日(我没有考虑该间隔中的最后一天,我将星期六和周日)。我有以下代码适用于大多数情况,除了我的示例中的代码。T-SQL获取两个日期之间的工作天数

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) 

我该如何做到这一点?我必须经历所有的日子并检查它们吗?或者有一个简单的方法来做到这一点。

+1

你怎么会占到假期是不是周末? –

+0

[Count between two days between two dates]可能的重复(http://stackoverflow.com/questions/252519/count-work-days-between-two-dates) – Mark

回答

14

请使用日历表。 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://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

对于这样的东西,我倾向于认为,还包括节假日等日历表

我使用这个脚本如下所示(请注意,我没有把它写@我忘了,我发现它)

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 

一旦创建表,发现的工作天数是易p​​easy:

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) 

注意,该脚本包括英国银行假期,我不是确保你在什么区

+0

只需要注意你的第一个函数,SQL-Server版本2008年以后,在'DATEPART'中将'ISO_WEEK'作为参数,这将比UDF更有效。 – GarethD

+0

我同意,但你知道,这只是运行一次,一旦日历表已经建立了功能被丢弃,并再也没有使用过,引用克拉克盖布“坦率地说,亲爱的,我不给一个该死的”大声笑 – HeavenCore

+2

您在最后的查询使用'BETWEEN',这会产生4行。 OP表示他们需要放弃范围内的最后一天,因此查询应该使用> =和<。另外为什么不使用标准的日期格式(如'YYYYMMDD'),这样你就不必做所有的转换东西? http://sqlblog.com/blogs/aaron_bertrand/archive/2009/10/16/bad-habits-to-kick-mishandling-date-range-queries.aspx http://sqlblog.com/blogs/aaron_bertrand/archive /2011/10/19/what-do-between-and-the-devil-have-in-common.aspx –

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 
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<[email protected]) 

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

    BEGIN 

    SET @[email protected]+1 
    END 
    SET @STARTDATE=DATEADD(day,1,@STARTDATE) 
    END 
    /* Display the no of working days */ 
    SELECT @COUNTS 
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 
相关问题