如果确切时间不可用,如何根据最后一个可用时间戳 return 取值?
How to return value based on the last available timestamp if the exact time is unavailable?
我正在尝试 return 以十五分钟为间隔的数据。我首先想到的是:
select * from myTable where DATEPART(minute, Timestamp) % 15 = 0
但是这种方法有两个问题。第一个是在给定分钟不一定总是有具有时间戳的数据,另一个是有时在给定分钟有多个数据点具有不同的秒值。我想在 :00、:15、:30 等处为每十五分钟组恰好一行。
此数据仅在发生变化时才会记录,因此如果我在 12:30 处没有数据点,例如,我可以获取之前最近的数据点并将该值用于 12:30,这将是正确的。
所以基本上我需要能够 return 恰好在 :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 中的方法时遇到了麻烦。可能吗?
给定一个固定的开始时间,您只需要一个 table 的数字来添加您的间隔。如果您还没有 table 个数字(很有用),那么一种快速生成数字的方法是
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。
我会使用 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
您可以使用下一个查询按 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]
就像 Shnugo 提到的那样,您可以使用计数 table 以 15 分钟为间隔获取数据,类似这样。
我正在使用 CTE 创建动态计数 table,但是您甚至可以根据需要使用物理日历 table。
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
非常棘手,但按照以下思路可能会奏效:
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
)
当然,如果有两个读数具有完全相同的时间戳,这将不起作用,在这种情况下,您需要另一个分组依据。随便乱查询。
现在你真的有足够多的方法来创建你的理货了table :-)
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;
问题缺少原始数据和模式信息,因此我将主要以一般形式解决问题。
您正在寻找范围内没有任何缺失记录的结果,涵盖可能有缺失记录的数据。鉴于该要求,正常的解决方案是创建一个 projection 仅用于左侧所需的值,使用像 Numbers 这样的源table 与您的实际数据没有任何关系。 Numbers table 将保证不会丢失您范围内的任何记录。对于日期预测,您只需将适当的天数或分钟数添加到您的起始值,以获得您期望的结果记录数。
一旦你有了投影,你就可以根据你的实际数据从投影中进行外部连接。在这种情况下,由于您有一些日期值额外的记录,因此 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
我正在尝试 return 以十五分钟为间隔的数据。我首先想到的是:
select * from myTable where DATEPART(minute, Timestamp) % 15 = 0
但是这种方法有两个问题。第一个是在给定分钟不一定总是有具有时间戳的数据,另一个是有时在给定分钟有多个数据点具有不同的秒值。我想在 :00、:15、:30 等处为每十五分钟组恰好一行。
此数据仅在发生变化时才会记录,因此如果我在 12:30 处没有数据点,例如,我可以获取之前最近的数据点并将该值用于 12:30,这将是正确的。
所以基本上我需要能够 return 恰好在 :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 中的方法时遇到了麻烦。可能吗?
给定一个固定的开始时间,您只需要一个 table 的数字来添加您的间隔。如果您还没有 table 个数字(很有用),那么一种快速生成数字的方法是
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。
我会使用 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
您可以使用下一个查询按 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]
就像 Shnugo 提到的那样,您可以使用计数 table 以 15 分钟为间隔获取数据,类似这样。
我正在使用 CTE 创建动态计数 table,但是您甚至可以根据需要使用物理日历 table。
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
非常棘手,但按照以下思路可能会奏效:
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
)
当然,如果有两个读数具有完全相同的时间戳,这将不起作用,在这种情况下,您需要另一个分组依据。随便乱查询。
现在你真的有足够多的方法来创建你的理货了table :-)
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;
问题缺少原始数据和模式信息,因此我将主要以一般形式解决问题。
您正在寻找范围内没有任何缺失记录的结果,涵盖可能有缺失记录的数据。鉴于该要求,正常的解决方案是创建一个 projection 仅用于左侧所需的值,使用像 Numbers 这样的源table 与您的实际数据没有任何关系。 Numbers table 将保证不会丢失您范围内的任何记录。对于日期预测,您只需将适当的天数或分钟数添加到您的起始值,以获得您期望的结果记录数。
一旦你有了投影,你就可以根据你的实际数据从投影中进行外部连接。在这种情况下,由于您有一些日期值额外的记录,因此 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