Группировать временные ряды по временным интервалам (например, дням) с совокупностью продолжительности - PullRequest
0 голосов
/ 09 ноября 2018

У меня есть таблица, содержащая временной ряд со следующей информацией.Каждая запись представляет событие «изменения режима».

 Timestamp        | Mode 
------------------+------
 2018-01-01 12:00 |  1   
 2018-01-01 18:00 |  2   
 2018-01-02 01:00 |  1   
 2018-01-02 02:00 |  2   
 2018-01-04 04:00 |  1   

Используя функцию LEAD , я могу создать запрос со следующим результатом.Теперь каждая запись содержит информацию, когда и как долго «режим был активен».

Пожалуйста, проверьте 2-ую и 4-ую запись.Они «принадлежат» нескольким дням.

 StartDT          | EndDT            | Mode | Duration
------------------+------------------+------+----------
 2018-01-01 12:00 | 2018-01-01 18:00 |  1   |   6:00
 2018-01-01 18:00 | 2018-01-02 01:00 |  2   |   7:00
 2018-01-02 01:00 | 2018-01-02 02:00 |  1   |   1:00
 2018-01-02 02:00 | 2018-01-04 04:00 |  2   |  50:00
 2018-01-04 04:00 | (NULL)           |  1   | (NULL)

Теперь я хотел бы получить запрос, который группирует данные по day и mode и агрегирует продолжительность.

Требуется эта таблица результатов:

 Date       | Mode | Total
------------+------+-------
 2018-01-01 |  1   |  6:00
 2018-01-01 |  2   |  6:00
 2018-01-02 |  1   |  1:00
 2018-01-02 |  2   | 23:00
 2018-01-03 |  2   | 24:00
 2018-01-04 |  2   | 04:00

Я не знал, как обрабатывать записи, которые "принадлежат" нескольким дням.Есть идеи?

Ответы [ 4 ]

0 голосов
/ 09 ноября 2018

Спасибо всем!

Ответ от Катона поставил меня на правильный путь. Вот мое окончательное решение:

DECLARE @Start AS datetime;
DECLARE @End AS datetime;
DECLARE @Interval AS int;


SET @Start = '2018-01-01';
SET @End = '2018-01-05';
SET @Interval = 24 * 60 * 60;



WITH 

cteDurations AS 
    (SELECT [Timestamp] AS StartDT,
            LEAD ([Timestamp]) OVER (ORDER BY [Timestamp]) AS EndDT,
            Mode
     FROM tblLog
     WHERE [Timestamp] BETWEEN @Start AND @End
    ),

cteTimeslots AS
    (SELECT @Start AS StartDT,
            DATEADD(SECOND, @Interval, @Start) AS EndDT
     UNION ALL
     SELECT EndDT,
            DATEADD(SECOND, @Interval, EndDT)
     FROM cteTimeSlots WHERE StartDT < @End
    ),

cteDurationsPerTimesplot AS 
    (SELECT CASE WHEN S.StartDT > C.StartDT THEN S.StartDT ELSE C.StartDT END AS StartDT,
            CASE WHEN S.EndDT < C.EndDT THEN S.EndDT ELSE C.EndDT END AS EndDT,
            C.StartDT AS Slot,
            S.Mode
     FROM cteDurations S 
        JOIN cteTimeslots C ON NOT(S.EndDT <= C.StartDT OR S.StartDT >= C.EndDT)
    )


SELECT  Slot,
        Mode,
        SUM(DATEDIFF(SECOND, StartDT, EndDT)) AS Duration

FROM cteDurationsPerTimesplot
GROUP BY Slot, Mode
ORDER BY Slot, Mode;

С помощью переменной @ Interval вы можете определить размер временных интервалов.

CTE cteDurations создает подрезультат с продолжительностью всех необходимых записей с помощью функции TSQL LEAD (доступно в MSSQL> = 2012). Это будет намного быстрее, чем НАРУЖНОЕ ПРИМЕНЕНИЕ.

CTE cteTimeslots создает список временных интервалов с временем начала и окончания.

CTE cteDurationsPerTimesplot является подрезультатом с JOIN между cteDurations и cteTimeslots. Это волшебное заявление от JOIN Cato !

И, наконец, оператор SELECT выполнит группировку и вычисление суммы для слота и режима.

Еще раз: большое спасибо всем! Особенно для Катона! Вы спасли мои выходные!

С уважением Оливер

0 голосов
/ 09 ноября 2018
create table ChangeMode ( ModeStart datetime2(7), Mode int )

insert into ChangeMode ( ModeStart, Mode ) values
( '2018-11-15T21:00:00.0000000', 1 ),
( '2018-11-16T17:18:19.1231234', 2 ),
( '2018-11-16T18:00:00.5555555', 1 ),
( '2018-11-16T18:00:01.1234567', 2 ),
( '2018-11-16T19:02:22.8888888', 1 ),
( '2018-11-16T20:00:00.9876543', 2 ),
( '2018-11-17T09:00:00.0000000', 1 ),
( '2018-11-17T23:23:23.0230450', 2 ),
( '2018-11-19T17:00:00.0172839', 1 ),
( '2018-11-20T03:07:00.7033077', 2 )

;
with 
-- Determine the earliest and latest dates.
-- Cast to date to remove the time portion.
-- Cast results back to datetime because we're going to add hours later.
MinMaxDates 
as 
(select cast(min(cast(ModeStart as date))as datetime) as MinDate, 
        cast(max(cast(ModeStart as date))as datetime) as MaxDate from ChangeMode),

-- How many days have passed during that period
Dur
as
(select datediff(day,MinDate,MaxDate) as Duration from MinMaxDates),

-- Create a list of numbers.
-- These will be added to MinDate to get a list of dates.
NumList
as
( select 0 as Num
    union all
    select Num+1 from NumList,Dur where Num<Duration ),

-- Create a list of dates by adding those numbers to MinDate
DayList 
as
( select dateadd(day,Num,MinDate)as ModeDate from NumList, MinMaxDates  ),

-- Create a list of day periods
PeriodList
as
( select ModeDate as StartTime,
            dateadd(day,1,ModeDate) as EndTime
            from DayList                        ),

-- Use LEAD to get periods for each record
-- Final record would return NULL for ModeEnd
-- We replace that with end of last day
ModePeriodList
as
( select ModeStart, 
            coalesce( lead(ModeStart)over(order by ModeStart),
                    dateadd(day,1,MaxDate) ) as ModeEnd, 
            Mode from ChangeMode, MinMaxDates               ),

ModeDayList
as
( select * from ModePeriodList, PeriodList 
where ModeStart<=EndTime and ModeEnd>=StartTime
),

-- Keep the later   of the mode start time, and the day start time
-- Keep the earlier of the mode   end time, and the day   end time
ModeDayPeriod
as
( select case when ModeStart>=StartTime then ModeStart  else StartTime end as StartTime,
            case when ModeEnd<=EndTime  then ModeEnd else EndTime   end as EndTime,
            Mode from ModeDayList ),

SumDurations
as
( select cast(StartTime as date) as ModeDate, 
        Mode, 
        DateDiff_Big(nanosecond,StartTime,EndTime)
        /3600000000000 
            as DurationHours from ModeDayPeriod   )                        

-- List the results in order
-- Use MaxRecursion option in case there are more than 100 days 
select ModeDate as [Date], Mode, sum(DurationHours) as [Total Duration Hours]
     from SumDurations 
group by ModeDate, Mode
order by ModeDate, Mode
option (maxrecursion 0)

Результат:

Date       Mode        Total Duration Hours
---------- ----------- ---------------------------------------
2018-11-15 1           3.00000000000000
2018-11-16 1           18.26605271947221
2018-11-16 2           5.73394728052777
2018-11-17 1           14.38972862361111
2018-11-17 2           9.61027137638888
2018-11-18 2           24.00000000000000
2018-11-19 1           6.99999519891666
2018-11-19 2           17.00000480108333
2018-11-20 1           3.11686202991666
2018-11-20 2           20.88313797008333
0 голосов
/ 09 ноября 2018

Следующее использует рекурсивный CTE для построения списка дат (календарь или таблица чисел работает одинаково хорошо). Затем он пересекает даты с датами, чтобы отсутствующие даты были заполнены соответствующими данными. Важным битом является то, что для каждой строки, если начальная дата и время относятся к предыдущему дню, она ограничивается 00:00. Аналогично для конечной даты и времени.

DECLARE @t TABLE (timestamp DATETIME, mode INT);
INSERT INTO @t VALUES
('2018-01-01 12:00', 1),
('2018-01-01 18:00', 2),
('2018-01-02 01:00', 1),
('2018-01-02 02:00', 2),
('2018-01-04 04:00', 1);

WITH cte1 AS (
    -- the min and max dates in your data
    SELECT
        CAST(MIN(timestamp) AS DATE) AS mindate,
        CAST(MAX(timestamp) AS DATE) AS maxdate
    FROM @t
), cte2 AS (
    -- build all dates between min and max dates using recursive cte
    SELECT mindate AS day_start, DATEADD(DAY, 1, mindate) AS day_end, maxdate
    FROM cte1
    UNION ALL
    SELECT DATEADD(DAY, 1, day_start), DATEADD(DAY, 2, day_start), maxdate
    FROM cte2
    WHERE day_start < maxdate
), cte3 AS (
    -- pull end datetime from next row into current
    SELECT
        timestamp AS dt_start,
        LEAD(timestamp) OVER (ORDER BY timestamp) AS dt_end,
        mode
    FROM @t
), cte4 AS (
    -- join datetime with date using date overlap query
    -- then clamp start datetime to 00:00 of the date
    -- and clamp end datetime to 00:00 of next date
    SELECT 
        IIF(dt_start < day_start, day_start, dt_start) AS dt_start_fix, 
        IIF(dt_end > day_end, day_end, dt_end) AS dt_end_fix,
        mode
    FROM cte2
    INNER JOIN cte3 ON day_end > dt_start AND dt_end > day_start
)
SELECT dt_start_fix, dt_end_fix, mode, datediff(minute, dt_start_fix, dt_end_fix) / 60.0 AS total
FROM cte4

DB Fiddle

0 голосов
/ 09 ноября 2018

вы можете использовать CTE для создания таблицы дней, а затем присоединить к ней временные интервалы

DECLARE @MAX as datetime2 = (SELECT MAX(CAST(Timestamp as date)) MX FROM process);
WITH StartEnd AS (select p1.Timestamp StartDT, 
                         P2.Timestamp  EndDT ,
                         p1.mode
                            from process p1
                            outer apply 
                            (SELECT TOP 1 pOP.*  FROM 
                                                    process pOP 
                                                    where pOP.Timestamp > p1.Timestamp 
                                                    order by pOP.Timestamp asc) P2
                 ),
    CAL AS (SELECT (SELECT MIN(cast(StartDT as date)) MN FROM StartEnd) DT
            UNION ALL
            SELECT DATEADD(day,1,DT) DT FROM CAL WHERE CAL.DT < @MAX
            ),
    TMS AS 
    (SELECT CASE WHEN S.StartDT > C.DT THEN S.StartDT ELSE C.DT END AS STP,
           CASE WHEN S.EndDT < DATEADD(day,1,C.DT) THEN S.ENDDT ELSE DATEADD(day,1,C.DT) END AS STE
     FROM StartEnd S JOIN CAL C ON NOT(S.EndDT <= C.DT OR S.StartDT>= DATEADD(day,1,C.dt))
    )
    SELECT *,datediff(MI ,TMS.STP, TMS.ste) as x from TMS
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...