Combining overlapping time intervals, then summing the total time

I have a hardware group and many devices in this group. Example:

+ Room 1
|-- Computer
|-- Camera
+ Room 2
|-- Computer
|-- Switch

      

All devices are monitored using ping. When a device is not working, the program adds a line to the table that says the beginning of the break. When the device is turned on again, the program will update this line, indicating the end of the break.

It is good to know the total break intervals for each device. My need is to know the real cumulative time for the whole group. Example:

Group    Device     Start                 End
Room 1   Computer   2015-05-12 01:40:00   2015-05-12 01:40:20
Room 1   Camera     2015-05-12 01:40:01   2015-05-12 01:40:27
Room 2   Computer   2015-05-12 03:43:03   2015-05-12 03:46:14
Room 2   Switch     2015-05-12 03:43:00   2015-05-12 03:46:12
Room 1   Camera     2015-05-12 07:12:10   2015-05-12 07:12:22

      

The downtime of the real group "Room 1" is 39 seconds (NOT 58).

01:40:00 - 01:40:20 = 20 seconds
01:40:01 - 01:40:27 = 7 seconds
07:12:10 - 07:12:22 = 12 seconds

      

On the first two lines, see why 27 seconds and not 46 seconds:

| 00, 01, 02, 03, 04, 05, 06, 07, 08, 09, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20                              |
|     01, 02, 03, 04, 05, 06, 07, 08, 09, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27  |

      

Well ... I have many groups and many devices for each group. How can I do this using SQL?

To help with tests ...

DECLARE @tblGroup TABLE (
  id int,
  name varchar(20)
)
INSERT INTO @tblGroup (id, name)
  VALUES (1, 'Room 1'), (2, 'Room 2'), (3, 'Room 3'), (4, 'Room 4')

DECLARE @tblDevice TABLE (
  id int,
  name varchar(20),
  group_id int
)
INSERT INTO @tblDevice (id, name, group_id)
  VALUES (1, 'Computer', 1), (2, 'Camera', 1), (3, 'Computer', 2), (4, 'Switch', 2)

DECLARE @tblStatus TABLE (
  id int,
  device_id int,
  dtStart datetime,
  dtEnd datetime
)
INSERT INTO @tblStatus (id, device_id, dtStart, dtEnd)
  VALUES (1, 1, '2015-05-12 01:40:00.0', '2015-05-12 01:40:20.0'),
  (2, 2, '2015-05-12 01:40:01.0', '2015-05-12 01:40:27.0'),
  (3, 3, '2015-05-12 03:43:03.0', '2015-05-12 03:46:14.0'),
  (4, 4, '2015-05-12 03:43:00.0', '2015-05-12 03:46:12.0'),
  (5, 2, '2015-05-12 07:12:10.0', '2015-05-12 07:12:22.0')

SELECT
  s.id,
  s.device_id,
  g.name AS groupName,
  d.name AS deviceName,
  s.dtStart,
  s.dtEnd
FROM @tblStatus s
INNER JOIN @tblDevice d
  ON d.id = s.device_id
INNER JOIN @tblGroup g
  ON g.id = d.group_id

      

+3


source to share


4 answers


You want to combine different groups into "islands" and calculate the extent of the islands. This is why this type of problem is sometimes referred to as gaps and islands.

Let me assume you are using SQL Server 2012+. This simplifies the calculations a bit. The idea is to define start and end for overlapping groups. Next, it is determined if the group overlaps:

select t.*,
       (case when exists (select 1
                          from @tblstatus t2
                          where t2.group_id = t.group_id and
                                t2.dtend > t.dtstart and t2.dtstart <= t.dtstart and
                                t2.id < t.id
                         )
              then 0 else 1 end) as NoOverlapBefore
from @tblstatus t

      

That being said, you can simply assign each row in the table the number of "NoOverlapBefore" entries that occur before it, and use the result for aggregation:

with t as (
      select t.*,
             (case when exists (select 1
                                from @tblstatus t2
                                where t2.group_id = t.group_id and
                                      t2.dtend > t.dtstart and t2.dtstart <= t.dtstart and
                                      t2.id < t.id
                               )
                    then 0 else 1 end) as NoOverlapBefore
      from @tblstatus t
     )
select group_id,
       datediff(second, min(dtstart), max(dtend)) as total_seconds
from (select t.*,
             sum(NoOverlapBefore) over (partition by group_id order by dtstart, id) as grp
      from @tblstatus t
     ) t
group by group_id;

      



EDIT:

I misunderstood a few things about your data structure. SQL Fiddle is a big help. Here's the one at work that actually works.

Request:

WITH t AS (
      SELECT t.*, d.group_id,
             (CASE WHEN EXISTS (SELECT 1
                                FROM tblstatus t2 JOIN
                                     tbldevice d2
                                     ON d2.id = t2.device_id
                                WHERE d2.group_id = d.group_id AND
                                      t2.dtend > t.dtstart AND
                                      t2.dtstart <= t.dtstart AND
                                      t2.id <> t.id
                              )
                   THEN 0 ELSE 1
              END ) AS NoOverlapBefore
     FROM tblstatus t JOIN
          tblDevice d
          ON t.device_id = d.id
    )
SELECT group_id, SUM(total_seconds) as total_seconds
FROM (SELECT group_id, grp,
             DATEDIFF(SECOND, MIN(dtstart), MAX(dtend)) AS total_seconds
      FROM (SELECT t.*,
                   sum(t.NoOverlapBefore) over (partition BY group_id
                                                ORDER BY t.dtstart, t.id) AS grp
            FROM t
           ) t
      GROUP BY grp, group_id
     ) t
GROUP BY group_id;

      

+2


source


A bit collapsed, but I have a working solution. The trick was to change the presentation of the data.

EDIT: This solution works until there are two events on the same device at the same time.



I left the SQL script here: http://sqlfiddle.com/#!6/59e80/8/0

declare @tblGroup table (id int, name varchar(20))
insert into @tblGroup (id, name) values (1, 'Room 1'), (2, 'Room 2'), (3, 'Room 3'), (4, 'Room 4')

declare @tblDevice table (id int, name varchar(20), group_id int)
insert into @tblDevice (id, name, group_id) values (1, 'Computer', 1), (2, 'Camera', 1), (3, 'Computer', 2), (4, 'Switch', 2)

declare @tblStatus table (id int, device_id int, dtStart datetime, dtEnd datetime)
insert into @tblStatus (id, device_id, dtStart, dtEnd) values
(1, 1, '2015-05-12 01:40:00.0', '2015-05-12 01:40:20.0'),
(2, 2, '2015-05-12 01:40:01.0', '2015-05-12 01:40:27.0'),
(3, 3, '2015-05-12 03:43:03.0', '2015-05-12 03:46:14.0'),
(4, 4, '2015-05-12 03:43:00.0', '2015-05-12 03:46:12.0'),
(5, 2, '2015-05-12 07:12:10.0', '2015-05-12 07:12:22.0');





WITH eventlist as
(select
    s.id,
    s.device_id,
    g.Id AS groupId,
    g.name as groupName,
    d.name as deviceName,
    s.dtStart AS dt,
    'GO_DOWN' AS eventtype,
    1 AS eventcount

from
    @tblStatus s
inner join
    @tblDevice d on d.id = s.device_id
inner join
    @tblGroup g on g.id = d.group_id
UNION
select
    s.id,
    s.device_id,
    g.Id AS groupId,
    g.name as groupName,
    d.name as deviceName,
    s.dtEND AS dt,
    'BACK_UP' AS eventtype,
     -1 AS eventcount
from
    @tblStatus s
inner join
    @tblDevice d on d.id = s.device_id
inner join
    @tblGroup g on g.id = d.group_id
),
breakdown AS(
SELECT 
    principal.groupId
    ,principal.groupName
    ,principal.dt
    ,principal.deviceName
    ,principal.eventtype
    ,was_broken = ISNULL(SUM(before.eventcount),0) 
    ,is_broken = ISNULL(SUM(before.eventcount),0) + principal.eventcount
    FROM  
eventlist principal 
LEFT JOIN  eventlist before ON before.groupId = principal.groupId 
AND 1 = CASE WHEN before.dt < principal.dt  THEN 1
               WHEN before.dt = principal.dt AND before.device_id < principal.device_id THEN 1 
              ELSE 0 END
GROUP BY 
         principal.eventcount
         ,principal.deviceName
        ,principal.eventtype
        ,principal.groupId
        ,principal.groupName
        ,principal.dt
)
,breakdownstart AS
( SELECT groupId,dt, r = RANK() OVER (PARTITION BY groupId ORDER BY dt) FROM breakdown WHERE was_broken = 0  AND is_broken =1 )
,breakdownend AS
( SELECT groupId,dt, r = RANK() OVER (PARTITION BY groupId ORDER BY dt) FROM breakdown WHERE was_broken = 1  AND is_broken = 0 )
,breakgroup as
(SELECT s.groupId
,s.r
, break_start = s.dt
, break_end = e.dt FROM breakdownstart s INNER JOIN breakdownend e ON e.r = s.r AND e.groupId = s.groupId)
SELECT groupId,SUM(DATEDIFF(SECOND,break_start,break_end)) AS break_length FROM breakgroup GROUP BY breakgroup.groupId

      

+1


source


Try the following:

select
    g.id, SUM(DATEDIFF(SECOND, s.dtStart, s.dtEnd))
from
    @tblStatus s
    inner join  @tblDevice d on d.id = s.device_id
    inner join  @tblGroup g on g.id = d.group_id
group by
    g.id

      

You group the GroupId, then for every state you have in that group, you get the difference in seconds between the start time and the end time, and the SUM at the GroupId level.

0


source


I suggest grouping by id, the goal here is to get the difference between times, only then can you SUM.

SELECT
    group.id, SUM(DATEDIFF(SECOND, status.dtStart, status.dtEnd))
FROM
    @tblStatus status
    inner join  @tblDevice device ON device.id = status.device_id
    inner join  @tblGroup group ON group.id = device.group_id
GROUP BY
    group.id

      

0


source







All Articles