【问题标题】:SQL to find time elapsed from multiple overlapping intervalsSQL从多个重叠间隔中查找经过的时间
【发布时间】:2014-12-08 18:20:17
【问题描述】:

不使用 MSSQL、DB2 或 Oracle。 没有 CTE。 没有 OVERLAP 谓词。 没有 INTERVAL 数据类型。 情况:对要修理的车辆工作不能开始,直到 为这项工作订购的所有零件都已收到。 在开始维修之前,可以多次订购零件。 我们需要提取车辆处于“部件暂停”状态的时间

所以对于标识为 id = 1 的车辆 零件在 4 个不同的场合被订购 (d1) 和收到 (d2)

    ID     d1     d2
     1     8/1    8/8
     1     8/2    8/6
     1     8/12   8/14
     1     8/3    8/10

 8/1                             8/8
  d1                              d2   
  |-------------------------------|  
         8/2             8/6                    8/12      8/14                  
         d1               d2                     d1        d2     
          |---------------|                      |----------|    
                   8/3                 8/10
                   d1                    d2
                   |---------------------|   
 8/1                                                       8/14
  |---------------------------------------------------------|  = 13 days
                                        8/10    8/12
  |--------------------------------------|    +  |----------|  = parts hold  = 11 days

从上面可以看出,开始工作的等待时间(假设 8/1 为 车辆可用于工作的日期)为 13 天。 等待零件的实际时间是 11 天,这是数字 我们需要从数据中得出。 实际的日期时间数据将是我们从中提取小时数的时间戳, 为了简单起见,我们在此示例数据中使用了日期。 我们正在努力生成一个基于集合(不是 psm,不是 udf,不是光标)的解决方案。 TIA

【问题讨论】:

  • 虽然辅助日历表上的左连接可能会有所帮助。
  • @Brian,这个问题有很大不同。 OP,你能添加一个视图来帮助查询吗?
  • 如果您查看示例“绘图”,您可以尝试计算每个日期未交付订单数量的解决方案。 8/1-8/2 = 2、8/2-8/3 = 3、8/3-8/6 = 4 等等。当您到达 8/10-8/12 时,您有 0 个未完成的交付。用非零值计算日期并给出等待天数。

标签: sql intervals gaps-and-islands


【解决方案1】:

我无法让@Alex W 的查询工作。它不是标准 SQL,因此需要大量重写才能与 SQL Server(我可以测试)兼容。但它确实给了我一些灵感,我对此进行了扩展。


找出每个不间断等待周期的所有起点:

SELECT DISTINCT
    t1.ID,
    t1.d1 AS date,
    -DATEDIFF(DAY, (SELECT MIN(d1) FROM Orders), t1.d1) AS n
FROM Orders t1
LEFT JOIN Orders t2                   -- Join for any events occurring while this
    ON t2.ID = t1.ID                  -- is starting. If this is a start point,
    AND t2.d1 <> t1.d1                -- it won't match anything, which is what
    AND t1.d1 BETWEEN t2.d1 AND t2.d2 -- we want.
GROUP BY t1.ID, t1.d1, t1.d2
HAVING COUNT(t2.ID) = 0

以及端点的等价物:

SELECT DISTINCT
    t1.ID,
    t1.d2 AS date,
    DATEDIFF(DAY, (SELECT MIN(d1) FROM Orders), t1.d2) AS n
FROM Orders t1
LEFT JOIN Orders t2
    ON t2.ID = t1.ID
    AND t2.d2 <> t1.d2
    AND t1.d2 BETWEEN t2.d1 AND t2.d2
GROUP BY t1.ID, t1.d1, t1.d2
HAVING COUNT(t2.ID) = 0

n 是自某个共同时间点以来的天数。起点为负值,终点为正值。这样我们就可以将它们相加以获得两者之间的天数。

span = end - start
span = end + (-start)
span1 + span2 = end1 + (-start1) + end2 + (-start2)

最后,我们只需要添加一些东西:

SELECT ID, SUM(n) AS hold_days
FROM (
   SELECT DISTINCT
       t1.id,
       t1.d1 AS date,
       -DATEDIFF(DAY, (SELECT MIN(d1) FROM Orders), t1.d1)  AS n
   FROM Orders t1
   LEFT JOIN Orders t2
      ON t2.ID = t1.ID
      AND t2.d1 <> t1.d1
      AND t1.d1 BETWEEN t2.d1 AND t2.d2
   GROUP BY t1.ID, t1.d1, t1.d2
   HAVING COUNT(t2.ID) = 0
   UNION ALL
   SELECT DISTINCT
       t1.id,
       t1.d2 AS date,
       DATEDIFF(DAY, (SELECT MIN(d1) FROM Orders), t1.d2) AS n
   FROM Orders t1
   LEFT JOIN Orders t2
      ON t2.ID = t1.ID
      AND t2.d2 <> t1.d2
      AND t1.d2 BETWEEN t2.d1 AND t2.d2
   GROUP BY t1.ID, t1.d1, t1.d2
   HAVING COUNT(t2.ID) = 0
   ORDER BY ID, date
) s
GROUP BY ID;

输入表(订单):

ID   d1           d2
 1   2011-08-01   2011-08-08
 1   2011-08-02   2011-08-06
 1   2011-08-03   2011-08-10
 1   2011-08-12   2011-08-14
 2   2011-08-01   2011-08-03
 2   2011-08-02   2011-08-06
 2   2011-08-05   2011-08-09

输出:

ID   hold_days
 1          11
 2           8

或者,您可以使用存储过程来执行此操作。

CREATE PROCEDURE CalculateHoldTimes
    @ID int = 0
AS
BEGIN
    DECLARE Events CURSOR FOR
    SELECT *
    FROM (
        SELECT d1 AS date, 1 AS diff
        FROM Orders
        WHERE ID = @ID
        UNION ALL
        SELECT d2 AS date, -1 AS diff
        FROM Orders
        WHERE ID = @ID
    ) s
    ORDER BY date;

    DECLARE @Events_date date,
            @Events_diff int,
            @Period_start date,
            @Period_accum int,
            @Total_start date,
            @Total_count int;

    OPEN Events;

    FETCH NEXT FROM Events
    INTO @Events_date, @Events_diff;

    SET @Period_start = @Events_date;
    SET @Period_accum = 0;
    SET @Total_start = @Events_date;
    SET @Total_count = 0;

    WHILE @@FETCH_STATUS = 0
    BEGIN
        SET @Period_accum = @Period_accum + @Events_diff;

        IF @Period_accum = 1 AND @Events_diff = 1
            -- Start of period
            SET @Period_start = @Events_date;
        ELSE IF @Period_accum = 0 AND @Events_diff = -1
            -- End of period
            SET @Total_count = @Total_count +
                DATEDIFF(day, @Period_start, @Events_date);

        FETCH NEXT FROM Events
        INTO @Events_date, @Events_diff;
    END;

    SELECT
        @Total_start AS d1,
        @Events_date AS d2,
        @Total_count AS hold_time;
END;

调用它:

EXEC CalculateHoldTimes 1;

【讨论】:

  • 谢谢 MizardX,这正是我们想要的
  • 现在可以了。 :) 反正;如果您认为它回答了您的问题,您仍然可以接受它。点击投票箭头下方的复选标记。
  • MizardX:Alex 回复中的 SQL 方言是 Sybase Advantage 使用的方言,这是我们使用的产品,因此我对他的回复感到高兴
  • 很好的答案,对我帮助很大!
  • @MarkusJarderot 如果你有时间我有一些问题:stackoverflow.com/questions/48531322/…
【解决方案2】:

这条SQL语句好像得到了你想要的(t是sampe表的表名):

SELECT
   d.id, 
   d.duration, 
   d.duration - 
   IFNULL(
      ( SELECT Sum( timestampdiff( SQL_TSI_DAY, 
                                   no_hold.d2, 
                                   ( SELECT min(d1) FROM t t4 
                                     WHERE t4.id = no_hold.id and t4.d1 > no_hold.d2 )))
        FROM ( SELECT DISTINCT id, d2 FROM t t1 
               WHERE ( SELECT sum( IIF( t1.d2 between t2.d1 and t2.d2, 1, 0 ) ) 
                       FROM t t2 WHERE t2.id = t1.id and t2.d2 <> t1.d2 ) = 0 
             And d2 <> ( select max( d2 ) from t t3 where t3.id = t1.id )) no_hold
        WHERE no_hold.id = d.id ),
      0 ) "parts hold"
FROM 
   ( SELECT id, timestampdiff( SQL_TSI_DAY, min( d1 ), max( d2 ) ) duration
     FROM t GROUP BY id ) d

外部查询获取修复工作的持续时间。复杂子查询计算不等待零件的总天数。这是通过定位车辆不等待零件的开始日期来完成的,然后计算它再次开始等待零件的天数:

// 1) The query for finding the starting dates when the vehicle is not waiting for parts, 
// i.e. finding all d2 that is not within any date range where the vehicle is waiting for part.
// The DISTINCT is needed to removed duplicate starting "no hold" period.

SELECT DISTINCT id, d2 
FROM t t1
WHERE ( SELECT sum( IIF( t1.d2 between t2.d1 and t2.d2, 1, 0 ) ) from t t2 
        WHERE t2.id = t1.id and t2.d2 <> t1.d2 ) = 0 AND 
      d2 <> ( SELECT max( d2 ) FROM t t3 WHERE t3.id = t1.id ) )

// 2) 车辆不等待零件的天数是从上述查询到车辆 // 再次等待零件的日期

timestampdiff( SQL_TSI_DAY, no_hold.d2, ( SELECT min(d1) FROM t t4 WHERE t4.id = no_hold.id and t4.d1 > no_hold.d2 ) )

结合上述两个并汇总所有这些时间段可以得出车辆不等待零件的天数。最后的查询添加了一个额外的条件来计算来自外部查询的每个 id 的结果。

这在具有许多 id 的非常大的表上可能不是非常有效。如果 id 被限制为一个或几个应该没问题。

【讨论】:

  • 已编辑以解决“无保留”期间重复开始日期的问题。
  • Alex:在上面的主代码块中,后面缺少一个右括号
  • 之后:and d2 (select max( d2 ) from t t3 where t3.id = t1.id )
【解决方案3】:
USE [DnnMasterShoraSystem]
GO
/****** Object:  StoredProcedure [dbo].[CalculateHoldTimes]    Script Date: 12/8/2014 1:36:12 PM ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

ALTER PROCEDURE [dbo].[CalculateHoldTimes]
    @PID int    
AS
BEGIN      
CREATE TABLE #tblTemp(
    [ID] [int] NOT NULL,
    [PID] [int] NOT NULL,
    [BID] [int] NOT NULL,
    [Active] [bit] NULL,
    [WorkStartDate] [nvarchar](10) NULL,
    [WorkEndDate] [nvarchar](10) NULL,
    [jobStateID] [int] NULL,
    [RegisterType] [int] NULL,
    [RegisterState] [int] NULL,
    [En_time] [datetime] NULL,
    [Fa_time] [nvarchar](40) NULL,
    [Status] [nvarchar](100) NULL,
    [PortalId] [int] NULL,
    [ModuleId] [int] NULL,
    [UserId] [int] NULL,
    [BrName] [nvarchar](150) NULL,
    [BrCode] [nvarchar](20) NULL,
    [WorkEndDate_New] [nvarchar](10) NULL
) ON [PRIMARY]

insert into #tblTemp
select * from [dbo].[Shora.Personel_Branch_Copy] 
        where WorkStartDate is not null 
        --and [dbo].[ShamsiToMiladi](WorkStartDate) <GETDATE() 
        --and [dbo].[ShamsiToMiladi](WorkEndDate) <GETDATE() 
        and PID=@PID
        --and [dbo].[ShamsiToMiladi](WorkEndDate)<[dbo].[ShamsiToMiladi](@NewDate)
        order by WorkStartDate

DECLARE Events CURSOR FOR
    SELECT [dbo].[ShamsiToMiladi](WorkStartDate) AS StartDate,[dbo].[ShamsiToMiladi](WorkEndDate) AS EndDate
        FROM #tblTemp        
    ORDER BY StartDate;

--drop table #tblTemp

    DECLARE @SDate date,
            @EDate date,
            @Period_Start date,
            @Period_End date,
            @Total int,
            @OldSDate date,
            @OldEDate date


    OPEN Events;

    FETCH NEXT FROM Events
    INTO @SDate, @EDate;

    set @Total=0
    SET @Period_Start =@SDate
    set @Period_End=@EDate

    WHILE @@FETCH_STATUS = 0
    BEGIN      
    if @OldSDate>@Period_End
        begin
            set @Period_Start=@SDate            

            if @Period_End>=@Period_Start
            set @Total+=DATEDIFF(DAY,@Period_Start,@Period_End)
        end
    else if @SDate<@Period_End
        begin       
        set @Period_Start=@Period_Start     
            set @Total=DATEDIFF(DAY,@Period_Start,@Period_End)
        end

        set @OldSDate=@SDate 
        set @OldEDate=@EDate

        FETCH NEXT FROM Events
        INTO @SDate, @EDate;

        if  @Period_End<@EDate
        set @Period_End=@EDate

    END;

INSERT INTO [dbo].[PersonelDays]
           (PID
           ,[Total_Start]
           ,[Total_End]
           ,[Total_count])
     VALUES
           (@PID,           
            @Period_Start,
            @Period_End,
            @Total
           )

drop table #tblTemp
CLOSE Events
DEALLOCATE Events
END;

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2023-01-10
    • 2020-02-19
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多