如果确切时间不可用,如何根据最后可用的时间戳返回值?

Posted

技术标签:

【中文标题】如果确切时间不可用,如何根据最后可用的时间戳返回值?【英文标题】:How to return value based on the last available timestamp if the exact time is unavailable? 【发布时间】:2015-07-14 13:29:32 【问题描述】:

我正在尝试每隔 15 分钟返回一次数据。我首先想到的是:

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

但是这种方法有两个问题。第一个是在给定分钟不一定总是有带有时间戳的数据,另一个是有时在给定分钟有多个数据点具有不同的秒值。我想在 :00、:15、:30 等处为每 15 分钟组设置一行。

此数据仅在发生变化时记录,因此如果我在 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 中做到这一点时遇到了麻烦。有可能吗?

【问题讨论】:

你报道的是什么时期?几个月?年?您可以使用所有 15 分钟间隔的计数表,然后按小于您给定边界的降序查找 TOP 1。如果您需要更多帮助,尽管问。 你能给我们一些样本数据和预期的输出吗? 同意@Shnugo 并补充说,如果您需要进一步的帮助,您应该使用示例数据和所需结果编辑您的问题。 @Shnugo 更新为预期输出。用户将输入开始日期时间和结束日期时间,在这两个时间之间,我希望每十五分钟显示一次数据。你能详细说明一下计数表吗? 给你我的例子,但是现在有很多计数表:-) 顺便说一句:我同意@ughai,物理日历表可能是最好的选择...... 【参考方案1】:

给定一个固定的开始时间,您只需要一张数字表即可将您的间隔添加到其中。如果您还没有数字表(这很有用),那么一种快速生成数字表的方法是

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 的序列。有关此内容的更多信息,请参阅以下系列:

Generate a set or sequence without loops – part 1 Generate a set or sequence without loops – part 2 Generate a set or sequence without loops – part 3

然后,一旦你有了你的数字,你就可以生成你的间隔:

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。

【讨论】:

【参考方案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

【讨论】:

【参考方案3】:

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

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;

【讨论】:

谢谢!我使用了您的答案和我接受的答案的组合,希望我可以同时标记:)【参考方案4】:

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

您正在寻找没有任何缺失记录的范围内的结果,涵盖可能有缺失记录的数据。鉴于该要求,通常的解决方案是使用具有 与您的实际数据无关。 Numbers 表将保证不会丢失您范围内的任何记录。对于日期预测,您只需将适当的天数或分钟数添加到您的起始值,以获得您期望在结果中的记录数。

一旦有了投影,就可以根据实际数据从投影中进行 OUTER JOIN。在这种情况下,JOIN 由于您有一些日期值额外记录这一事实而变得复杂。我知道解决这个问题的两种方法。一种方法是对投影中的值进行分组。另一种是使用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

【讨论】:

【参考方案5】:

非常棘手,但类似的方法可能会奏效:

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
)

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

【讨论】:

我的答案与我输入时已经建议的几个答案非常相似,特别是@Stepan 的方法非常相似,只是他没有进行外部查询。【参考方案6】:

我会使用 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

【讨论】:

【参考方案7】:

您可以使用下一个查询按 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]

【讨论】:

以上是关于如果确切时间不可用,如何根据最后可用的时间戳返回值?的主要内容,如果未能解决你的问题,请参考以下文章

蒙德里安的日期范围

创建缺少键值数据记录的视图

如果字典键不可用,则返回 None

如果值不可用,则获取上个月的值

选择查询以获取确切的时间戳

ajax