从日期时间表中获取包含开始值和结束值的列表
Posted
技术标签:
【中文标题】从日期时间表中获取包含开始值和结束值的列表【英文标题】:Get list with start and end values from table of datetimes 【发布时间】:2011-05-16 13:51:33 【问题描述】:目前我有一个这样建立的表
DeviceID Timestamp Value
----------------------------------------
Device1 1.1.2011 10:00:00 3
Device1 1.1.2011 10:00:01 4
Device1 1.1.2011 10:00:02 4
Device1 1.1.2011 10:00:04 3
Device1 1.1.2011 10:00:05 4
Device1 1.1.2011 14:23:14 8
Device1 1.1.2011 14:23:15 7
Device1 1.1.2011 14:23:17 4
Device1 1.1.2011 14:23:18 2
如您所见,某些值来自具有给定时间戳的设备(列类型为日期时间)。
问题是设备可以在任何时候启动和停止,并且数据中没有直接信息表明发生了启动或停止。但是从给定的时间戳列表中很容易判断何时开始和停止发生,因为只要两行的时间戳在 5 秒内,它们就属于同一个度量值。
现在我想从这些数据中得到一个这样的列表:
DeviceID Started Ended
Device1 1.1.2011 10:00:00 1.1.2011 10:00:05
Device1 1.1.2011 14:23:14 1.1.2011 14:23:18
那么有什么想法可以快速做到这一点吗?我所能想到的就是使用某种游标并手动比较每个日期时间对。但我认为这会变得非常慢,因为我们必须检查每一行中的每个值。
那么有没有更好的 SQL 解决方案不能使用游标?
更新
目前我测试了所有给定的答案。通过阅读,它们看起来都很好,并且有一些有趣的方法。不幸的是,所有这些(到目前为止)在真实数据上都失败了。最大的问题似乎是数据的海量(目前它们在表中大约有 350 万个条目)。仅对一小部分子集执行给定查询会产生预期的结果,但将查询滚动到整个表只会导致非常糟糕的性能。
我必须进一步测试和检查我是否可以对数据进行分块,并且只将部分数据传递给这些给定算法之一以使这件事滚动起来。但也许你们中的某个人有另一个聪明的想法,可以更快地获得结果。
更新(有关结构的更多信息)
好的,这些信息也可能会有所帮助: 目前,表中约有 350 万个条目。以下是给定的列类型和索引:
_ID int 主键 分组索引 在我的示例中没有提到此列,因为此查询不需要它 设备ID int 不为空 索引 时间戳 日期时间 不为空 索引 价值 几个不同类型(int、real、tinyint)的未索引列 都可以为空也许这有助于改进给定问题的现有(或新)解决方案。
【问题讨论】:
在这种情况下,开始和结束是否必须以 5 秒为间隔?我的意思是,只要从 1.1.2011 14:23:14 开始的行在 1 到 5 秒内,是否可以以 1.1.2011 14:23:14 开始并以 1.1.2011 14:23:38 结束与上一行的差距? @shahkalpesh:不,它们不需要精确的间隔(实际上它们就像显示在 1-2 秒间隔的某个地方,但有时由于四舍五入会错过一秒) .但是,如果您发现间隙 > 5 秒,则会发生停止/启动。所以你的意思是正确的。 此问题的每个解决方案都将“必须检查每一行中的每个值”,因此游标解决方案仍然可能是最有效的。除了游标之外,SQL 没有提供任何方法来指定顺序处理,例如比较每对连续行之间的值(当然是根据某种排序顺序连续的,而不是物理上的磁盘)。 关于 _ID 列和索引的信息很重要。据我所知,您的设置由于在 DeviceID 和 Timestamp 列上都缺少索引而引入了额外的低效率。我们都需要它们,并且我们需要按它们对记录进行分组和排序。现在我们不仅需要对记录进行排序(或建立临时查找表),而且在使用任何现有索引时,我们仍然需要在主表中使用 _ID 进行书签查找。 (注意:在查询和数据修改的效率方面,使用聚集索引非常受限制。没有充分理由不应该这样做。) 【参考方案1】:-- Table var to store the gaps
declare @T table
(
DeviceID varchar(10),
PrevPeriodEnd datetime,
NextPeriodStart datetime
)
-- Get the gaps
;with cte as
(
select *,
row_number() over(partition by DeviceID order by Timestamp) as rn
from data
)
insert into @T
select
C1.DeviceID,
C1.Timestamp as PrevPeriodEnd,
C2.Timestamp as NextPeriodStart
from cte as C1
inner join cte as C2
on C1.rn = C2.rn-1 and
C1.DeviceID = C2.DeviceID and
datediff(s, C1.Timestamp, C2.Timestamp) > 5
-- Build islands from gaps in @T
;with cte1 as
(
-- Add first and last timestamp to gaps
select DeviceID, PrevPeriodEnd, NextPeriodStart
from @T
union all
select DeviceID, max(TimeStamp) as PrevPeriodEnd, null as NextPeriodStart
from data
group by DeviceID
union all
select DeviceID, null as PrevPeriodEnd, min(TimeStamp) as PrevPeriodEnd
from data
group by DeviceID
),
cte2 as
(
select *,
row_number() over(partition by DeviceID order by PrevPeriodEnd) as rn
from cte1
)
select
C1.DeviceID,
C1.NextPeriodStart as PeriodStart,
C2.PrevPeriodEnd as PeriodEnd
from cte2 as C1
inner join cte2 as C2
on C1.DeviceID = C2.DeviceID and
C1.rn = C2.rn-1
order by C1.DeviceID, C1.NextPeriodStart
【讨论】:
这真的像一个魅力!查询在 20 秒后返回,我选择了一些随机条目来验证它是否正常工作。一切看起来都和预期的一样。如果可以的话,我会不止一次地赞成它。 ;-) @Oliver:很高兴你跟进了。出于好奇:您是否将此解决方案与其他正确解决方案进行了比较?例如。使用基于光标的解决方案。使用 ff ro 游标,您只需花费:索引扫描,将每个周期的一行附加到 tmp 表并返回 tmp 表。这个比较如何? @Tomek:不幸的是没有。目前没有,但由于大量数据和检查 sql server 的成本计划,我认为这个解决方案不会表现得更好,所以我坚持你的,只是感到高兴。 ;-) @Oliver:不是我的解决方案,Mikael 的。我只是对使用这种复杂的方法来获得相当简单的结果的优点感到好奇。所以我检查了上述解决方案的执行计划。 Q1:2 集群。在data
上进行索引扫描(使用primary key clustered (DeviceId, [Timestamp])
)。 Q2:4 集群。 data
上的索引扫描,@T
上的 2 次表扫描,1 次表假脱机和 3 次排序。不要告诉我你不认为它可以做得更好。也许使用更大的数据集,data
上的一半扫描可能会变成搜索,并且来自两个主要分支的结果可能会被重用。但是排序总是会扼杀性能。【参考方案2】:
我玩过一些数据类型和名称(只是因为我可以,而且因为 timestamp 是保留字),并且可以使用您的示例数据获得您请求的结果。
样本数据:
create table Measures (
DeviceID int not null,
Occurred datetime not null,
Value int not null,
constraint PK_Measures PRIMARY KEY (DeviceID,Occurred)
)
go
insert into Measures (DeviceID,Occurred,Value)
select 1,'2011-01-01T10:00:00',3 union all
select 1,'2011-01-01T10:00:01',4 union all
select 1,'2011-01-01T10:00:02',4 union all
select 1,'2011-01-01T10:00:04',3 union all
select 1,'2011-01-01T10:00:05',4 union all
select 1,'2011-01-01T14:23:14',8 union all
select 1,'2011-01-01T14:23:15',7 union all
select 1,'2011-01-01T14:23:17',4 union all
select 1,'2011-01-01T14:23:18',2
现在是查询:
;with StartPeriods as (
select m1.DeviceID,m1.Occurred as Started
from Measures m1 left join Measures m2 on m1.DeviceID = m2.DeviceID and m2.Occurred < m1.Occurred and DATEDIFF(second,m2.Occurred,m1.Occurred) < 6
where m2.DeviceID is null
), ExtendPeriods as (
select DeviceID,Started,Started as Ended from StartPeriods
union all
select
ep.DeviceID,ep.Started,m2.Occurred
from
ExtendPeriods ep
inner join
Measures m2
on
ep.DeviceID = m2.DeviceID and
ep.Ended < m2.Occurred and
DATEDIFF(SECOND,ep.Ended,m2.Occurred) < 6
)
select DeviceID,Started,MAX(Ended) from ExtendPeriods group by DeviceID,Started
StartPeriods
公用表表达式 (CTE) 从度量表中查找在 5 秒内没有前一行的行。然后,ExtendPeriods
CTE 递归地扩展这些时间段,方法是从度量值中查找新行,这些行发生在当前找到的时间段结束后 5 秒内。
然后,我们会找到句点的结尾与开始尽可能远的行。
【讨论】:
正如我已经注意到的,我有很多数据(目前大约有 350 万个条目),您的查询将无休止地运行(45 分钟并且仍在运行)... @Oliver - 您可能需要检查此表上的索引。如果您无法在生产环境中添加适当的索引,您可能需要将此表的副本复制到另一个数据库中并在那里运行您的分析。【参考方案3】:试试这个:
select DeviceID,MIN(Timestamp),MAX(Timestamp)
from @table group by DATEPART(hh,Timestamp),DeviceID
【讨论】:
不幸的是这不起作用,因为每个设备有不止一个开始和停止迭代,所以输出很尴尬。【参考方案4】:以下解决方案的基本思想借鉴自this answer。
WITH data (DeviceID, Timestamp, Value) AS (
SELECT 'Device1', CAST('1.1.2011 10:00:00' AS datetime), 3 UNION ALL
SELECT 'Device1', '1.1.2011 10:00:01', 4 UNION ALL
SELECT 'Device1', '1.1.2011 10:00:02', 4 UNION ALL
SELECT 'Device1', '1.1.2011 10:00:04', 3 UNION ALL
SELECT 'Device1', '1.1.2011 10:00:05', 4 UNION ALL
SELECT 'Device1', '1.1.2011 14:23:14', 8 UNION ALL
SELECT 'Device1', '1.1.2011 14:23:15', 7 UNION ALL
SELECT 'Device1', '1.1.2011 14:23:17', 4 UNION ALL
SELECT 'Device1', '1.1.2011 14:23:18', 2
),
ranked AS (
SELECT
*,
rn = ROW_NUMBER() OVER (PARTITION BY DeviceID ORDER BY Timestamp)
FROM data
),
starts AS (
SELECT
r1.DeviceID,
r1.Timestamp,
rank = ROW_NUMBER() OVER (PARTITION BY r1.DeviceID ORDER BY r1.Timestamp)
FROM ranked r1
LEFT JOIN ranked r2 ON r1.DeviceID = r2.DeviceID
AND r1.rn = r2.rn + 1
AND r1.Timestamp <= DATEADD(second, 5, r2.Timestamp)
WHERE r2.DeviceID IS NULL
),
ends AS (
SELECT
r1.DeviceID,
r1.Timestamp,
rank = ROW_NUMBER() OVER (PARTITION BY r1.DeviceID ORDER BY r1.Timestamp)
FROM ranked r1
LEFT JOIN ranked r2 ON r1.DeviceID = r2.DeviceID
AND r1.rn = r2.rn - 1
AND r1.Timestamp >= DATEADD(second, -5, r2.Timestamp)
WHERE r2.DeviceID IS NULL
)
SELECT
s.DeviceID,
Started = s.Timestamp,
Ended = e.Timestamp
FROM starts s
INNER JOIN ends e ON s.DeviceID = e.DeviceID AND s.rank = e.rank
【讨论】:
正如我已经注意到的,我有很多数据(目前大约有 350 万个条目),您的查询将无休止地运行(45 分钟并且仍在运行)...【参考方案5】:试试这个,虽然我不确定它在处理大量数据时表现如何
SELECT a.TS AS [StartTime], (SELECT TOP 1 c.TS FROM TestTime c WHERE c.TS >= a.TS AND
NOT EXISTS(SELECT * FROM TestTime d WHERE d.TS > c.TS AND DATEDIFF(SECOND, c.TS, d.TS) <= 5) ORDER BY c.TS) AS [StopTime]
FROM TestTime a WHERE NOT EXISTS (SELECT * FROM TestTime b WHERE a.TS > b.TS AND DATEDIFF(SECOND, b.TS, a.TS) <= 5)
我的表称为 TestTime,列称为 TS,因此请为您的表调整它。我已经使用 NOT EXISTS 来检查时间戳 =该时间戳(如果它是单个条目,因此是开始/停止一个),并且再次使用 NOT EXISTS 在 5 秒内检查大于它的记录 - 所以,如果没有找到记录(仅第一个),再次显示。您可以调整和改进它,但这可能是一个很好的基础。
请注意,如果它仍在运行,它将列出最后一次找到的时间作为最后一次启动事件的停止时间。
为简单起见,我没有在此处输入设备名称,因此您需要将其放入 StopTime 和 WHERE 子句中
【讨论】:
正如我已经注意到的,我有很多数据(目前大约有 350 万个条目),您的查询将无休止地运行(45 分钟并且仍在运行)... 啊,我没看到 - 在我回答之前我已经打开了你的问题很长一段时间,所以也许它在我打开和回答之间更新了。正如我所说,我不知道它会如何处理大量数据。我认为,有了这么多的数据,不管你怎么做,它都不会很快,不幸的是,游标可能是你最好的选择。我认为存储的数据存在一个根本问题,您确实需要单独记录开始和停止事件,但我猜不幸的是,这不是一个选择......? 我认为我还需要建立一个包含所有开始/停止持续时间的新表。但我必须根据给定的数据建立它。因此,在这种情况下,性能并不是最重要的考虑因素(因为它只需要运行一次),但它应该在合理的时间内恢复,而不是运行几个小时。 我只是在考虑这个问题并想出了一个类似的解决方案 - 将所有现有数据发布到一个启停表,然后有一个每天/每小时/等例程来发布新数据(显然您需要检查该时间段中的第一个条目是否是上一个时期的延续)或者您可以在表格上设置一个触发器,检查该设备在 5 秒内是否有上一个条目,如果没有找到上一个时间,将此记录为停止事件,然后将当前记录的时间记录为开始事件。这样您的启停数据就会实时【参考方案6】:DECLARE @t TABLE
(DeviceID VARCHAR(10),
[Timestamp] DATETIME,
VALUE INT
)
INSERT @t
SELECT 'Device1','20110101 10:00:00', 3
UNION SELECT 'Device1','20110101 10:00:01', 4
UNION SELECT 'Device1','20110101 10:00:02', 4
UNION SELECT 'Device1','20110101 10:00:04', 3
UNION SELECT 'Device1','20110101 10:00:05', 4
UNION SELECT 'Device1','20110101 14:23:14', 8
UNION SELECT 'Device1','20110101 14:23:15', 7
UNION SELECT 'Device1','20110101 14:23:17', 4
UNION SELECT 'Device1','20110101 14:23:18', 2
;WITH myCTE
AS
(
SELECT DeviceID, [Timestamp],
ROW_NUMBER() OVER (PARTITION BY DeviceID
ORDER BY [TIMESTAMP]
) AS rn
FROM @t
)
, recCTE
AS
(
SELECT DeviceID, [Timestamp], 0 as groupID, rn FROM myCTE
WHERE rn = 1
UNION ALL
SELECT r.DeviceID, g.[Timestamp], CASE WHEN DATEDIFF(ss,r.[Timestamp], g.[Timestamp]) <= 5 THEN r.groupID ELSE r.groupID + 1 END, g.rn
FROM recCTE AS r
JOIN myCTE AS g
ON g.rn = r.rn + 1
)
SELECT DeviceID, MIN([Timestamp]) AS [started], MAX([Timestamp]) AS ended
FROM recCTE
GROUP BY DeviceId, groupId
OPTION (MAXRECURSION 0);
【讨论】:
我已经注意到我有很多数据(目前大约有 350 万个条目),您的查询将在 11 分钟后中止,声称已达到最大递归深度 100。 @Oliver - 已更新以允许继续查询(添加了MAXRECURSION 0
选项)。您的问题中没有提及数据量。
@Oliver - 可能会使用其他索引来加快处理速度。您可以使用源表上的索引详细信息更新问题吗?【参考方案7】:
您应该能够为此使用窗口函数(假设 15 分钟在下面定义了一个新会话):
SELECT DeviceId,
Timestamp,
COALESCE((Timestamp - lag(Timestamp) OVER w) > interval '15 min', TRUE)
as session_begins
COALESCE((lead(Timestamp) OVER w - Timestamp) > interval '15 min', TRUE)
as session_ends
FROM YourTable
WINDOW w AS (PARTITION BY DeviceId ORDER BY Timestamp);
根据您的 where 子句,您可能希望删除 coalesce/true 部分,因为获取的第一行/最后一行可能会变得无效。
如果您只需要边界,您可以在子查询和group by DeviceId, session_begins, session_ends having session_begins or session_ends
中使用上述内容。另外,如果你这样做,不要忘记将 where 子句放在子查询中,而不是在主查询中,否则你最终会因为窗口聚合而对整个表进行 seq 扫描。
【讨论】:
不幸的是这不起作用,因为每个设备有不止一个开始和停止迭代,所以输出很尴尬。 哦...我在想这是 DeviceId 是一个奇怪的会话 ID。如何区分来自同一设备的两个会话? 这就是问题所在。您只能通过检查时间戳是否大于 5 秒来区分两个会话。 在这种情况下,您需要在上述查询中使用领先/滞后。我会相应地编辑它。以上是关于从日期时间表中获取包含开始值和结束值的列表的主要内容,如果未能解决你的问题,请参考以下文章