如果准确时间不可用,如何根据上次可用时间戳返回值?

时间:2015-07-14 13:29:33

标签: sql sql-server tsql sql-server-2014

我试图以十五分钟的间隔返回数据。我想做的第一件事是:

select * from myTable where DATEPART(minute, Timestamp) % 15 = 0

但这种方法存在两个问题。第一个是在给定的分钟内不一定总是存在具有时间戳的数据,另一个是在给定的分钟有时存在具有不同的第二值的多个数据点。我想在每个十五分钟组中只有一行,分别是:00,:15,:30等。

此数据仅在发生变化时记录,因此,如果我在12:30没有数据点,我可以在此之前获取最接近的数据点,并在12:30使用该值。这是正确的。

所以基本上我需要能够准确地返回时间戳:00,:30等,以及最接近那个时间的记录中的数据。

数据可能跨越数年,但更可能是更短的时间,天或周。这就是预期的输出结果:

Timestamp               Value
1/1/2015 12:30:00       25
1/1/2015 12:45:00       41
1/1/2015 1:00:00        45

我在思考SQL中的方法时遇到了麻烦。有可能吗?

7 个答案:

答案 0 :(得分:4)

给定一个固定的开始时间,您只需要一个数字表来添加您的间隔。如果您还没有数字表(这些数据很有用),那么快速生成数字的方法就是

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 @StartDateTime SMALLDATETIME = '20150714 14:00',
        @EndDateTime SMALLDATETIME = '20150715 15:00';

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  Interval = DATEADD(MINUTE, 15 * (N - 1), @StartDateTime)
FROM    Numbers
WHERE   DATEADD(MINUTE, 15 * (N - 1), @StartDateTime) <= @EndDateTime

其中包括:

Interval
----------------------
2015-07-14 14:00:00
2015-07-14 14:15:00
2015-07-14 14:30:00
2015-07-14 14:45:00
2015-07-14 15:00:00
2015-07-14 15:15:00
2015-07-14 15:30:00

然后,您只需要使用APPLYTOP在每个时间间隔之前或之前找到最接近的值:'

/*****************************************************************
SAMPLE DATA
*****************************************************************/

DECLARE @T TABLE ([Timestamp] DATETIME, Value INT);
INSERT @T ([Timestamp], Value)
SELECT  DATEADD(SECOND, RAND(CHECKSUM(NEWID())) * -100000, GETDATE()),
        CEILING(RAND(CHECKSUM(NEWID())) * 100)
FROM sys.all_objects;

/*****************************************************************
QUERY
*****************************************************************/

DECLARE @StartDateTime SMALLDATETIME = '20150714 14:00',
        @EndDateTime SMALLDATETIME = '20150715 15:00';

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  Interval = DATEADD(MINUTE, 15 * (N - 1), @StartDateTime)
    FROM    Numbers
    WHERE   DATEADD(MINUTE, 15 * (N - 1), @StartDateTime) <= @EndDateTime
)
SELECT  i.Interval, t.[Timestamp], t.Value
FROM    Intervals AS i
        OUTER APPLY
        (   SELECT  TOP 1 t.[Timestamp], t.Value
            FROM    @T AS t
            WHERE   t.[Timestamp] <= i.Interval
            ORDER BY t.[Timestamp] DESC, t.Value
        ) AS t
ORDER BY i.Interval;

修改

需要注意的一点是,如果有两个相等的时间戳同时或者最接近某个时间间隔,我已经应用了Value的第二级排序:

SELECT  i.Interval, t.[Timestamp], t.Value
FROM    Intervals AS i
        OUTER APPLY
        (   SELECT  TOP 1 t.[Timestamp], t.Value
            FROM    @T AS t
            WHERE   t.[Timestamp] <= i.Interval
            ORDER BY t.[Timestamp] DESC, t.Value --- ORDERING HERE
        ) AS t
ORDER BY i.Interval;

这是任意的,可以是您选择的任何内容,建议您确保订购足够的项目以确保结果具有确定性,也就是说,如果您对同一数据运行查询多次相同将返回结果,因为只有一行满足条件。如果你有这样的两行:

    Timestamp    |  Value  | Field1
-----------------+---------+--------
2015-07-14 14:00 |   100   |   1
2015-07-14 14:00 |   100   |   2
2015-07-14 14:00 |   50    |   2

如果您只是按时间戳排序,对于间隔2015-07-14 14:00,您不知道您是否会获得50或100的值,并且执行之间可能会有所不同,具体取决于统计信息和执行计划。同样,如果您按TimestampValue订购,那么您不知道Field1是1还是2。

答案 1 :(得分:2)

与Shnugo一样,您可以使用计数表以15分钟的间隔获取数据,就像这样。

我正在使用CTE创建动态计数表,但您甚至可以根据需要使用物理日历表。

DECLARE @StartTime DATETIME = '2015-01-01 00:00:00',@EndTime DATETIME = '2015-01-01 14:00:00'

DECLARE @TimeData TABLE ([Timestamp] datetime, [Value] int);

INSERT INTO @TimeData([Timestamp], [Value])
VALUES ('2015-01-01 12:30:00', 25),
    ('2015-01-01 12:45:00', 41),
    ('2015-01-01 01:00:00', 45);

;WITH CTE(rn) AS
(
    SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL 
    SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
), CTE2 as 
(
    SELECT C1.rn
    FROM CTE C1 CROSS JOIN CTE C2
), CTE3 as 
(
    SELECT TOP (CEILING(DATEDIFF(minute,@StartTime,@EndTime)/15)) ROW_NUMBER()OVER(ORDER BY C1.rn) - 1 rn 
    FROM CTE2 C1 CROSS JOIN CTE2 C2
)
SELECT DATEADD(minute,rn*15,@StartTime) CurrTime,T.Value
FROM CTE3
CROSS APPLY (SELECT TOP 1 Value FROM @TimeData WHERE [Timestamp] <= DATEADD(minute,rn*15,@StartTime) ORDER BY [Timestamp] DESC) T;

<强>输出

CurrTime    Value
2015-01-01 01:00:00.000 45
2015-01-01 01:15:00.000 45
.
.
.
2015-01-01 12:00:00.000 45
2015-01-01 12:15:00.000 45
2015-01-01 12:30:00.000 25
2015-01-01 12:45:00.000 41
2015-01-01 13:00:00.000 41
2015-01-01 13:15:00.000 41
2015-01-01 13:30:00.000 41
2015-01-01 13:45:00.000 41

答案 2 :(得分:1)

现在你真的有足够的方法来创建你的计数表: - )

DECLARE @startdate DATETIME={ts'2015-06-01 00:00:00'};
WITH JumpsOf15 AS
(
    SELECT ROW_NUMBER() OVER(ORDER BY object_id) * 15 AS Step
    FROM sys.objects --take any large table here (should have many rows...)
)
SELECT Step,steppedDate.steppedDate
FROM JumpsOf15
CROSS APPLY(SELECT DATEADD(MINUTE,Step,@startdate) AS steppedDate ) AS steppedDate
WHERE GETDATE()>steppedDate.steppedDate;

答案 3 :(得分:1)

问题是缺少原始数据和架构信息,因此我将主要以一般形式解决问题。

您正在寻找不会丢失任何记录的范围内的结果,涵盖可能丢失记录的数据。鉴于这一要求,通常的解决方案是使用像这样的数字表来源,只为左侧需要的值创建 投影 没有与您的实际数据有关。我们保证Numbers表不会遗漏您范围内的任何记录。对于日期预测,您只需在起始值中添加适当的天数或分钟数,即为结果中预期的记录数。

完成投影后,您可以根据实际数据从投影中进行OUTER JOIN。在这种情况下,由于您有一些日期值额外记录,JOIN变得复杂。我知道解决这个问题的两种方法。一种方法是GROUP BY投影中的值。另一种是使用OUTER APPLY而不是连接。使用OUTER APPLY,您可以在应用的查询中使用TOP 1过滤器将结果限制为一个项目。

总之,这里有一些伪代码可以帮助你到达你需要的地方:

WITH Numbers AS 
( 
   --select numbers here
),
DateProjection As
(
   SELECT DATEADD(minute, 15*Numbers.Number, '2015-01-01') As RangeStart,
          DATEADD(minute, 15*(Numbers.Number+1), '2015-01-01') AS RangeEnd
   FROM Numbers
)
SELECT dp.RangeStart as TimeStamp, oa.Value
FROM DateProjection dp
OUTER APPLY (SELECT TOP 1 Value FROM [myTable] WHERE myTable.TimeStamp >= dp.RangeStart AND myTable.TimeStamp < dp.RangeEnd) oa

答案 4 :(得分:0)

我会使用OVER子句按时间戳划分行,四舍五入到最接近的四分之一小时。然后按时间戳和舍入时间戳之间的差异对每个分区进行排序,升序,并抓住每个分区的第一行。我想那会做你想要的。这将为您提供最接近15分钟标记的行。但是,如果在15分钟内没有行,则不会添加外推值。

SELECT ROW_NUMBER() OVER(PARTITION BY [Timestamp Moded to 15 minutes] ORDER BY [Diff timestamp - timestamp moded to 15 minutes] ASC) AS RowNum, *
FROM MyTable where RowNum = 1

答案 5 :(得分:0)

您可以使用下一个查询以15分钟的间隔对数据进行分组:

select *, CASE DATEPART(minute, timestamp) /15 
  WHEN 0 THEN '0-15' WHEN 1 THEN '15-30' WHEN 2 THEN '30-45' WHEN 3 THEN '45-60' END 
AS [Time Group] 
from myTable where
DATEPART(minute, timestamp) /15 = 2 /* for group 30-45 min*/

考虑日期和时间:

select *, 
    CAST(CAST(timestamp as date) AS VARCHAR(MAX))+ ' ' + 
    CAST(DATEPART(hour, timestamp) AS  VARCHAR(MAX))  + ':' + 
    CAST(
        CASE DATEPART(minute, timestamp) /15 
            WHEN 0 THEN '0-15' 
            WHEN 1 THEN '15-30' 
            WHEN 2 THEN '30-45' 
            WHEN 3 THEN '45-60' END 
            AS VARCHAR(MAX)) AS [Interval] 
from myTable
order by [Interval]

答案 6 :(得分:0)

非常棘手,但这些内容可能有效:

select * from mytable where TimeStamp in (
  select max(TimeStamp) from (
    select date(TimeStamp) dt, hour(TimeStamp) as hr, 
      case when minute(TimeStamp) < 15 then 15 else 
      case when minute(TimeStamp) < 30 then 30 else 
      case when minute(TimeStamp) < 45 then 45 else 60 end end end as mint 
    from mytable where TimeStamp between <some TS> and <some other TS>
  ) t group by dt, hr, mint
)

当然,如果有两个具有完全相同时间戳的读数,这将不起作用,在这种情况下,您需要另一个组。凌乱的查询无论如何。