如果确切时间不可用,如何根据最后可用的时间戳返回值?
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
然后您只需要使用APPLY
和TOP
找到每个间隔上或之前的最接近的值:'
/*****************************************************************
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,并且执行之间可能会有所不同,具体取决于统计信息和执行计划。同样,如果您通过Timestamp
和Value
订购,那么您不知道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]
【讨论】:
以上是关于如果确切时间不可用,如何根据最后可用的时间戳返回值?的主要内容,如果未能解决你的问题,请参考以下文章