SQL to find time elapsed from multiple overlapping intervals

后端 未结 3 1902
-上瘾入骨i
-上瘾入骨i 2020-12-05 20:50

Not using MSSQL or DB2 or Oracle. No CTE. No OVERLAP predicate. No INTERVAL data type. The situation: on a vehicle to be repaired work can not start until all parts orde

3条回答
  •  情话喂你
    2020-12-05 21:37

    I couldn't get @Alex W's queries to work. It is not standard SQL, so it required a lot of rewrite to be compatible with SQL Server (which I can test). But it did give me some inspiration, which I have expanded upon.


    Find all start-points of every period of uninterrupted waiting:

    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
    

    And the equivalent for end-points:

    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 is the number of days since some common point in time. Start-points have a negative value, and end-points have a positive value. This is so that we can just add them up to get the number of days in between.

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

    Finally, we just need to add things up:

    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;
    

    Input table (Orders):

    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
    

    Output:

    ID   hold_days
     1          11
     2           8
    

    Alternatively, you can do this with a stored procedure.

    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;
    

    Call it with:

    EXEC CalculateHoldTimes 1;
    

提交回复
热议问题