Оператор SQL, который вычисляет рост за интервал

В нашей базе данных есть таблица, в которой отслеживается энергопотребление устройства. Скорость, с которой вставляются новые значения, не фиксирована, они записываются только тогда, когда действительно есть изменение, поэтому временное расстояние между значениями варьируется и может составлять от 1 секунды до нескольких минут. Записи состоят из отметки времени и значения. Значение всегда увеличивается с каждой новой строкой, так как оно учитывает кВтч.

Я хочу достичь следующего: я хочу указать дату и время начала и окончания, скажем, месяц. Я также хочу указать интервал, например 15 минут, 1 час, 1 день или аналогичный. Результат, который мне нужно получить, имеет вид [Начало интервала как дата и время], [потребление энергии в этом интервале], например вот так (где интервал будет установлен на 1 час):

2015-01.01 08:00:00   -   65
2015-01.01 09:00:00   -   43
2015-01.01 10:00:00   -   56

Вот как выглядит таблица:

TimeStamp            Value
-------------------------
2015-01-08 08:29:47, 5246
2015-01-08 08:36:15, 5247
2015-01-08 08:37:10, 5248
2015-01-08 08:38:01, 5249
2015-01-08 08:38:38, 5250
2015-01-08 08:38:51, 5251
2015-01-08 08:39:33, 5252
2015-01-08 08:40:20, 5253
2015-01-08 08:41:10, 5254
2015-01-09 08:56:25, 5255
2015-01-09 08:56:43, 5256
2015-01-09 08:57:31, 5257
2015-01-09 08:57:36, 5258
2015-01-09 08:58:02, 5259
2015-01-09 08:58:57, 5260
2015-01-09 08:59:27, 5261
2015-01-09 09:00:06, 5262
2015-01-09 09:00:59, 5263
2015-01-09 09:01:54, 5265
2015-01-09 09:02:44, 5266
2015-01-09 09:03:39, 5267
2015-01-09 09:04:22, 5268
2015-01-09 09:05:11, 5269
2015-01-09 09:06:08, 5270

У меня такое чувство, что мне придется комбинировать функцию SUM() с GROUP BY, но я понятия не имею, как это сделать, потому что, насколько я могу судить, мне также пришлось бы учитывать только рост за интервал, а не сумму абсолютных значений в этом интервале. Было бы здорово, если бы кто-нибудь мог направить меня на верный путь.


person Rob    schedule 22.04.2015    source источник
comment
рост за интервал? По сравнению с самым высоким значением последнего интервала или первым значением в том же интервале? Логика диктует первое. Какую версию sqlserver вы используете?   -  person t-clausen.dk    schedule 22.04.2015
comment
Столбец значений похож на постоянно увеличивающийся счетчик, который сохраняет общее потребление до этого момента, как счетчик воды в вашем доме. Таким образом, прирост за интервал - это максимальное (или последнее) значение за вычетом минимального (= первого) значения в данном интервале.   -  person Rob    schedule 23.04.2015


Ответы (3)


Данные вашей выборки не соответствуют интервалам результатов, поэтому вы можете пропустить увеличение в пределах интервала в конце или в начале. Поэтому я предположил линейное увеличение между строками выборки данных и сопоставил их с интервалом результатов.

declare @start datetime2 = '2015-01-09 09:00:00'
declare @end datetime2 = '2015-01-09 09:30:00'
declare @intervalMinutes int = 5

;with intervals as (
      select @start iStart, dateadd(minute, @intervalMinutes, @start) iEnd

      union all

      select iEnd, dateadd(minute, @intervalMinutes, iEnd) from intervals
      where iEnd < @end

), increases as (
        select 
             T.Timestamp sStart, 
             lead(T.Timestamp, 1, null ) over (order by T.Timestamp) sEnd, -- the start of the next period if there is one, null else
             lead(T.value, 1, null ) over (order by T.Timestamp) - T.value increase  -- the increase within this period
        from @T T
), rates as (
        select 
           sStart rStart, 
           sEnd rEnd, 
           (cast(increase as float))/datediff(second, sStart, sEnd) rate -- increase/second
        from increases where increase is not null
), samples as (
        select *, 
            case when iStart > rStart then iStart else rStart end sStart, -- debug
            case when rEnd>iEnd then iEnd else rEnd end sEnd,  -- debug                
            datediff(second, case when iStart > rStart then iStart else rStart end, case when rEnd>iEnd then iEnd else rEnd end)*rate x -- increase within the period within the interval
        from intervals i
        left join rates r on rStart between iStart and iEnd or rEnd between iStart and iEnd or iStart between rStart and rEnd -- overlaps
)
select iStart, iEnd, isnull(sum(x), 0) from samples 
group by iStart, iEnd

CTE:

  • intervals содержит интервалы, для которых вы хотите получить данные
  • increaese вычисляет приросты за периоды выборочных данных
  • rates вычисляет увеличение в секунду периодов выборочных данных.
  • samples сопоставляет интервалы результатов с интервалами выборки, соблюдая перекрытия между границами

Наконец, выбор суммирует периоды выборки, сопоставленные с одним интервалом.

ПРИМЕЧАНИЯ:

  • Для количества интервала> [ваша максимальная глубина рекурсии] вы должны использовать другое решение для упаковки intervals CTE (см. Решение @GarethD)
  • Подсказка отладки: просто используя select * from samples, вы можете увидеть периоды выборки, соответствующие вашим интервалам результатов
person flo    schedule 22.04.2015
comment
Спасибо! Это решение обеспечивает надежные и очень точные значения. - person Rob; 23.04.2015

Я думаю, что лучший способ справиться с этим - сначала сгенерировать свои интервалы, а затем присоединиться к вашим данным, так как это, во-первых, значительно упрощает группировку для переменных интервалов, а также означает, что вы по-прежнему получаете результаты для интервалов без данных. Для этого вам понадобится таблица чисел, так как у многих людей ее нет, ниже вы можете быстро создать ее на лету:

WITH N1 AS (SELECT N FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) t (N)),
N2 (N) AS (SELECT 1 FROM N1 AS N1 CROSS JOIN N1 AS N2),
Numbers (N) AS (SELECT ROW_NUMBER() OVER(ORDER BY N1.N) FROM N2 AS N1 CROSS JOIN N2 AS N2)
SELECT *
FROM Numbers;

Это просто генерирует последовательность от 1 до 10 000. Для получения дополнительной информации об этом см. Следующие серии:

Затем вы можете определить время начала, интервал и количество отображаемых записей, а вместе с таблицей чисел вы можете сгенерировать свои данные:

DECLARE @Start DATETIME2 = '2015-01-09 08:00',
        @Interval INT = 60,     -- INTERVAL IN MINUTES
        @IntervalCount INT = 3; -- NUMBER OF INTERVALS TO SHOW

WITH N1 AS (SELECT N FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) t (N)),
N2 (N) AS (SELECT 1 FROM N1 AS N1 CROSS JOIN N1 AS N2),
Numbers (N) AS (SELECT ROW_NUMBER() OVER(ORDER BY N1.N) FROM N2 AS N1 CROSS JOIN N2 AS N2)

SELECT  TOP (@IntervalCount)
        Interval = DATEADD(MINUTE, (N - 1) * @Interval, @Start)
FROM    Numbers;

Наконец, вы можете LEFT JOIN это к своим данным, чтобы получить минимальные и максимальные значения для каждого интервала.

DECLARE @Start DATETIME2 = '2015-01-09 08:00',
        @Interval INT = 60,     -- INTERVAL IN MINUTES
        @IntervalCount INT = 3; -- NUMBER OF INTERVALS TO SHOW

WITH N1 AS (SELECT N FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) t (N)),
N2 (N) AS (SELECT 1 FROM N1 AS N1 CROSS JOIN N1 AS N2),
Numbers (N) AS (SELECT ROW_NUMBER() OVER(ORDER BY N1.N) FROM N2 AS N1 CROSS JOIN N2 AS N2),
Intervals AS
(   SELECT  TOP (@IntervalCount)
            IntervalStart = DATEADD(MINUTE, (N - 1) * @Interval, @Start),
            IntervalEnd = DATEADD(MINUTE, N  * @Interval, @Start)
    FROM    Numbers AS n
)
SELECT  i.IntervalStart,
        MinVal = MIN(t.Value),
        MaxVal = MAX(t.Value),
        Difference = ISNULL(MAX(t.Value)  - MIN(t.Value), 0)
FROM    Intervals AS i
        LEFT JOIN T AS t
            ON t.timestamp >= i.IntervalStart
            AND t.timestamp < i.IntervalEnd
GROUP BY i.IntervalStart;

Если ваши значения могут увеличиваться и уменьшаться в пределах инвертора, тогда вам нужно будет использовать функцию ранжирования, чтобы получить первую и последнюю запись для каждого часа, а не min и max:

DECLARE @Start DATETIME2 = '2015-01-09 08:00',
        @Interval INT = 60,     -- INTERVAL IN MINUTES
        @IntervalCount INT = 3; -- NUMBER OF INTERVALS TO SHOW

WITH N1 AS (SELECT N FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) t (N)),
N2 (N) AS (SELECT 1 FROM N1 AS N1 CROSS JOIN N1 AS N2),
Numbers (N) AS (SELECT ROW_NUMBER() OVER(ORDER BY N1.N) FROM N2 AS N1 CROSS JOIN N2 AS N2),
Intervals AS
(   SELECT  TOP (@IntervalCount)
            IntervalStart = DATEADD(MINUTE, (N - 1) * @Interval, @Start),
            IntervalEnd = DATEADD(MINUTE, N  * @Interval, @Start)
    FROM    Numbers AS n
), RankedData AS
(   SELECT  i.IntervalStart,
            t.Value,
            t.timestamp,
            RowNum = ROW_NUMBER() OVER(PARTITION BY i.IntervalStart ORDER BY t.timestamp),
            TotalRows = COUNT(*) OVER(PARTITION BY i.IntervalStart)
    FROM    Intervals AS i
            LEFT JOIN T AS t
                ON t.timestamp >= i.IntervalStart
                AND t.timestamp < i.IntervalEnd
)
SELECT  r.IntervalStart,
        Difference = ISNULL(MAX(CASE WHEN RowNum = TotalRows THEN r.Value END) - 
                            MAX(CASE WHEN RowNum = 1 THEN r.Value END), 0)
FROM    RankedData AS r
WHERE   RowNum = 1
OR      TotalRows = RowNum
GROUP BY r.IntervalStart;

Пример скрипта SQL с интервалом в 1 час

Пример скрипта SQL с 15-минутными интервалами

Пример скрипта SQL с интервалом в 1 день


ИЗМЕНИТЬ

Как указано в комментариях, ни одно из вышеперечисленных решений не учитывает опережение границ периода, ниже это будет учтено:

DECLARE @Start DATETIME2 = '2015-01-09 08:25',
        @Interval INT = 5,     -- INTERVAL IN MINUTES
        @IntervalCount INT = 18; -- NUMBER OF INTERVALS TO SHOW

WITH N1 AS (SELECT N FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) t (N)),
N2 (N) AS (SELECT 1 FROM N1 AS N1 CROSS JOIN N1 AS N2),
Numbers (N) AS (SELECT ROW_NUMBER() OVER(ORDER BY N1.N) FROM N2 AS N1 CROSS JOIN N2 AS N2),
Intervals AS
(   SELECT  TOP (@IntervalCount)
            IntervalStart = DATEADD(MINUTE, (N - 1) * @Interval, @Start),
            IntervalEnd = DATEADD(MINUTE, (N - 0) * @Interval, @Start)
    FROM    Numbers AS n
), LeadData AS
(   SELECT  T.timestamp,
            T.Value,
            NextValue = nxt.value,
            AdvanceRate = ISNULL(1.0 * (nxt.Value - T.Value) / DATEDIFF(SECOND, T.timestamp, nxt.timestamp), 0),
            NextTimestamp = nxt.timestamp
    FROM    T AS T
            OUTER APPLY 
            (   SELECT  TOP 1 T2.timestamp, T2.value
                FROM    T AS T2
                WHERE   T2.timestamp > T.timestamp
                ORDER BY T2.timestamp
            ) AS nxt
)
SELECT  i.IntervalStart,
        Advance = CAST(ISNULL(SUM(DATEDIFF(SECOND, d.StartTime, d.EndTime) * t.AdvanceRate), 0) AS DECIMAL(10, 4))
FROM    Intervals AS i
        LEFT JOIN LeadData AS t
            ON t.NextTimestamp >= i.IntervalStart 
            AND t.timestamp < i.IntervalEnd
        OUTER APPLY
        (   SELECT  CASE WHEN t.timestamp > i.IntervalStart THEN t.timestamp ELSE i.IntervalStart END,
                    CASE WHEN t.NextTimestamp < i.IntervalEnd THEN t.NextTimestamp ELSE i.IntervalEnd END
        ) AS d (StartTime, EndTime)
GROUP BY i.IntervalStart;
person GarethD    schedule 22.04.2015
comment
Одно замечание: интервалы роста не суммируются с общей разницей между первой и последней строкой данных. Это связано с тем, что обычно происходит увеличение между последней строкой данных в интервале 1 и первой строкой данных в следующем интервале 2. Этот рост не учитывается ни в i1, ни в i2. - person flo; 23.04.2015
comment
У меня также есть небольшое замечание: чтобы получить непрерывный список интервалов (без пробелов), мне пришлось изменить LEFT JOIN на LEFT OUTER JOIN и добавить условие ON, которое фильтруется по столбцу идентификатора устройства в моих значениях потребления table, о котором я не упомянул в своем вопросе. - person Rob; 23.04.2015
comment
@flo: Вы правы, значения неверны. В быстром тесте, который привел к совокупному потреблению 11 кВтч в течение 48 часов, что на самом деле было правильным потреблением 19 кВтч (общее потребление в дату окончания периода минус значение в дату начала периода). - person Rob; 23.04.2015
comment
Я добавил версию, которая будет учитывать этот прогресс, она довольно похожа на принятый ответ, но я видел, что один из ваших вопросов был помечен тегом SQL-Server-2008, поэтому опубликовал решение, которое будет работать в этой версии SQL Server. . FWIW LEFT JOIN и LEFT OUTER JOIN - это одно и то же. - person GarethD; 23.04.2015
comment
@Gareth: Спасибо за улучшенный ответ, теперь он работает. И вы правы насчет ЛЕВОГО ВНЕШНЕГО СОЕДИНЕНИЯ, я думаю, что перепутал вещи, потому что я не совсем привык к такой магии БД;) - person Rob; 23.04.2015

Быстрый способ сделать это - получить дату + час из TimeStamp, затем GROUP BY на нем, а значением энергопотребления будет MAX (значение) - MIN (значение). Вы можете манипулировать этой отметкой времени другими способами, чтобы получить другие интервалы, этот пример предназначен только для почасового потребления.

SELECT 
    CONVERT(datetime, CONVERT(varchar(10), TimeStamp, 120) + ' ' + CONVERT(varchar(2), DATEPART(hour, TimeStamp)) + ':00:00'),
    MAX(Value) - MIN(Value) AS Value
FROM [Table]
GROUP BY CONVERT(datetime, CONVERT(varchar(10), TimeStamp, 120) + ' ' + CONVERT(varchar(2), DATEPART(hour, TimeStamp)) + ':00:00')
person George T    schedule 22.04.2015