【问题标题】:How To Get Elapsed Time Between Two Dates, Ignoring Some Time Range?如何获取两个日期之间经过的时间,忽略一些时间范围?
【发布时间】:2025-12-21 23:20:15
【问题描述】:

我有一个包含 DATETIME 列“开始”和 DATETIME 列“结束”的表。我想返回开始和结束之间的分钟数(结束总是在开始之后)。通常我只会使用“DateDiff()”,但这次我需要排除另一个日期范围。例如 - 从每周的周二上午 9 点到周三下午 6 点,应忽略。

如果一行的开始时间是周二上午 8 点,周三结束时间是晚上 7 点 - 由于忽略了日期范围,因此经过的时间应该是两小时(120 分钟)。

我在想出一个合适的方法来做这件事时遇到了麻烦,而且我的在线搜索还没有找到我正在寻找的东西。有人可以帮我吗?

【问题讨论】:

  • 周二早上 8 点到周三下午 5 点的时间怎么样?那只是1小时吗?还是你会忽略它?
  • @ChrisStillwell - 一小时。如果我们在“忽略”范围内开始和结束,它应该是 0。

标签: sql sql-server tsql


【解决方案1】:

试试这个:

--total time span to calculate difference
DECLARE @StartDate DATETIME = '2015-11-10 8:00:00AM',
        @EndDate DATETIME = '2015-11-11 7:00:00PM'

--get the day of week (-1 because sunday is counted as first weekday)
DECLARE @StartDayOfWeek INT = (SELECT DATEPART(WEEKDAY, @StartDate)) -1
DECLARE @EndDayOfWeek INT = (SELECT DATEPART(WEEKDAY, @EndDate)) -1

--set the time span to exclude
DECLARE @InitialDOWToExclude TINYINT = 2
DECLARE @InitialTODToExclude VARCHAR(100) = '9:00:00 AM'

DECLARE @EndDOWToExclude TINYINT = 3
DECLARE @EndTODToExclude VARCHAR(100) = '6:00:00 PM'

--this will be the final output in hours
DECLARE @ElapsedHours INT = (SELECT DATEDIFF(HOUR, @StartDate, @EndDate))

DECLARE @WeeksBetween INT = (SELECT DATEDIFF(WEEK, @StartDate, @EndDate))
DECLARE @Iterator INT = 0
WHILE (@Iterator <= @WeeksBetween)
BEGIN
    DECLARE @InitialDaysBetween INT = @StartDayOfWeek - @InitialDOWToExclude
    DECLARE @StartDateToExclude DATETIME = (SELECT DATEADD(DAY, @InitialDaysBetween, DATEADD(WEEK, @Iterator, @StartDate)))
    SET @StartDateToExclude =CAST(DATEPART(YEAR, @StartDateToExclude) AS VARCHAR(100))
                              + CAST(DATEPART(MONTH, @StartDateToExclude) AS VARCHAR(100))
                              + CAST(DATEPART(DAY, @StartDateToExclude) AS VARCHAR(100))
                              + ' '
                              + CAST(@InitialTODToExclude AS VARCHAR(100))

    DECLARE @EndDaysBetween INT = @EndDayOfWeek - @EndDOWToExclude 
    DECLARE @EndDateToExclude DATETIME = (SELECT DATEADD(DAY, @EndDaysBetween, DATEADD(WEEK, @Iterator, @EndDate)))
    SET @EndDateToExclude =CAST(DATEPART(YEAR, @EndDateToExclude) AS VARCHAR(100))
                              + CAST(DATEPART(MONTH, @EndDateToExclude) AS VARCHAR(100))
                              + CAST(DATEPART(DAY, @EndDateToExclude) AS VARCHAR(100))
                              + ' '
                              + CAST(@EndTODToExclude AS VARCHAR(100))

    SET @ElapsedHours = @ElapsedHours - DATEDIFF(HOUR, @StartDateToExclude, @EndDateToExclude)

    SET @Iterator = @Iterator + 1
END

SELECT @ElapsedHours

【讨论】:

    【解决方案2】:

    这可能会让你非常接近..

    DECLARE @Table1 TABLE ([Id] INT, [Start] DATETIME, [End] DATETIME)
    INSERT INTO @Table1 VALUES
    (1, '2015-11-08 00:00:00', '2015-11-10 21:45:38'),
    (2, '2015-11-09 00:00:00', '2015-11-11 21:45:38')
    ;
    
    -- hours to exclude
    WITH excludeCTE AS 
    (
        SELECT * 
        FROM (VALUES('Tuesday', 9, 0), ('Wednesday', 0, 0)) AS T([Day], [Hour], [Amount])
        UNION ALL 
        SELECT [Day], [Hour] + 1, [Amount]  
        FROM excludeCTE
        WHERE  ([Day] = 'Tuesday' AND [Hour] < 23) OR ([Day] = 'Wednesday' AND [Hour] < 18)
    ),
    -- all hours between start and end
    dateCTE AS
    (
        SELECT  [Id], 
                [Start], 
                [End], 
                DATENAME(weekday, [Start])[Day],
                DATENAME(hour, [Start])[Hour]
        FROM    @Table1 t
        UNION ALL 
        SELECT  cte.[Id], 
                DATEADD(HOUR, 1, cte.[Start]), 
                cte.[End], 
                DATENAME(weekday, DATEADD(HOUR, 1, cte.[Start]))[Day],
                DATENAME(hour, DATEADD(HOUR, 1, cte.[Start]))[Hour]
        FROM    @Table1 t
                JOIN dateCTE cte ON t.Id = cte.Id
        WHERE   DATEADD(HOUR, 1, cte.[Start]) <= t.[End]
    
    )
    SELECT  t.[Id], 
            t.[Start], 
            t.[End], 
            SUM(COALESCE(e.[Amount], 1)) [Hours]
    FROM    @Table1 t
            INNER JOIN dateCTE d ON t.[Id] = d.[Id]
            LEFT JOIN excludeCTE e ON d.[Day] = e.[Day] AND d.[Hour] = e.[Hour]
    GROUP BY t.[Id], 
            t.[Start], 
            t.[End]
    OPTION (MAXRECURSION 0) -- allow more than 100 hours 
    

    【讨论】:

    • 我认为这是有道理的(如果我理解的话) - 您正在查看所有时间,然后忽略排除时间中的时间。我会试一试,看看我在几分钟内完成后效果如何。
    • 你是对的。它也适用于多周范围。如果你想处理分钟,我可能会采取不同的方法,特别是如果范围超过 1 周
    【解决方案3】:

    附加限制,即任意两个日期之间只能有一个排除范围

    CREATE TABLE worktable (
      _Id INT
    , _Start DATETIME
    , _End DATETIME
    );
    INSERT INTO worktable VALUES
      (1, '2015-11-09 00:00:00', '2015-11-09 00:45:00') -- Start and End before excluded range
    , (2, '2015-11-09 00:00:00', '2015-11-11 21:45:00') -- Start before, End after
    , (3, '2015-11-09 00:00:00', '2015-11-10 21:00:00') -- Start before, End between
    , (4, '2015-11-10 10:00:00', '2015-11-11 10:00:00') -- Start between, End between
    , (5, '2015-11-10 10:00:00', '2015-11-11 21:45:00') -- Start between, End after
    
    With getDates As (
      SELECT _Id
           , a = _Start
           , b = _End
           , c = DATEADD(hh, 9
               , DATEADD(DAY,DATEDIFF(DAY, 0, _Start) / 7 * 7
               + 7 * Cast(Sign(1 - DatePart(dw, _Start)) + 1 as bit), 1))
           , d = DATEADD(hh, 18
               , DATEADD(DAY,DATEDIFF(DAY, 0, _Start) / 7 * 7
               + 7 * Cast(Sign(1 - DatePart(dw, _Start)) + 1 as bit), 2))
      FROM   worktable
    ), getDiff As (
      SELECT c_a = DATEDIFF(mi, a, c)
           , c_b = DATEDIFF(mi, b, c)
           , b_d = DATEDIFF(mi, d, b)
           , a, b, c, d, _id
      FROM   getDates
    )
    Select _id
         , (c_a + ABS(c_a)) / 2
         - (c_b + ABS(c_b)) / 2
         + (b_d + ABS(b_d)) / 2
    FROM   getDiff;
    

    c 是开始日期 (Find the next occurance of a day of the week in SQL) 之后的第一个星期二的日期,您可能需要根据 DATEFIRST 调整最后一个值

    dc同一周开始日期后第一个星期三的日期

    如果a 大于或等于bCast(Sign(a - b) + 1 as bit) 为 1,否则为 0

    (x + ABS(x)) / 2如果不是负数,则为x,否则为0

    假设得到排除范围的经过时间的公式是:

     + (Exclusion Start - Start) If (Start < Exclusion Start)
     - (Exclusion Start - End) If (End < Exclusion Start) 
     + (End - Exclusion End) If (Exclusion End < End)
    

    【讨论】:

      【解决方案4】:
      -- excluded range (weekday numbers run from 1 to 7)
      declare @x datetime = /*ignore*/ '1900012' + /*start day # and time*/ '3 09:00am';
      declare @y datetime = /*ignore*/ '1900012' + /*  end day # and time*/ '4 06:00pm';
      
      -- normalize date to 1900-01-21, which was a Sunday
      declare @s datetime =
          dateadd(day, 19 + datepart(weekday, @start), cast(cast(@start as time) as datetime));
      declare @e datetime =
          dateadd(day, 19 + datepart(weekday, @end), cast(cast(@end as time) as datetime));
      
      -- split range into two parts, one before @x and the other after @y
      -- each part collapses to zero when @s and @e respectively fall between @x and @y
      select (
          datediff(second, -- diff in minutes would truncate so count seconds
              case when @s < @x then @s else @x end, -- minimum of @s, @x
              case when @e < @x then @e else @x end  -- minimum of @e, @x
          ) +
          datediff(second,
              case when @s > @y then @s else @y end, -- maximum of @s, @y
              case when @e > @y then @e else @y end  -- maximum of @e, @y
          )
      ) / 60; -- convert seconds to minutes, truncating with integer division
      

      我看了一眼之前的答案,我认为肯定有更直接和优雅的东西。也许这更容易理解,并且与某些解决方案相比,一个明显的优势是更改排除范围很简单,并且该范围不必限于一天。

      我假设您的日期从不超过一个常规日历周。不过,扩展它以处理更多问题并不难。一种方法是处理部分周的开始和结束以及中间的整周。

      假设您的开始时间是上午 8:59:30,结束时间是下午 6:00:30。在这种情况下,我想在减去 9-6 块之后,您需要在每边累积半分钟以获得一整分钟。如果您使用datediff(minute, ...),您将截断部分分钟并且永远没有机会将它们加在一起:这就是为什么我数秒然后在最后除以六十的原因。当然,如果你只在整分钟内处理,那么你不需要那样做。

      我有点武断地选择了我的参考日期。起初我认为在日历上查看一个真实且方便的日期可能会很方便,但最终它只在星期天才是真正重要的。所以我选择了第一个星期日,日期以数字 1 结尾。

      请注意,该解决方案还依赖于将datefirst 设置为星期日。如有必要,可以对其进行调整或使其更便携。

      【讨论】:

      • 是的,我在 SS2008 中测试了我的答案。
      • dayofweek 是自定义日期部分吗?另外,我还没有看到将 smalldatetime 作为第二个参数的 dateadd 函数
      • 我主要使用smalldatetime,因为我知道 1900 年 1 月 1 日是星期一,我正在手机上输入答案。在旧版本中 dateadd 总是工作得很好,但我发现它在 2014 年不再工作(在 SQL Fiddle 中)。我的意思是weekday/dw。有些人非常不喜欢较短的代码,所以我在发布之前对其进行了更改。我似乎很难记住长的。
      • @JamieD77 我认为缺少逗号导致第二个参数错误。无论哪种方式,我都废弃了 smalldatetime 的东西。