如何检测和绑定SQL表中行值之间的更改?

bpo*_*ter 18 t-sql sql-server sql-server-2008

我有一个表记录随时间变化的值,类似于以下内容:

RecordId  Time   Name
========================
1         10     Running
2         18     Running
3         21     Running
4         29     Walking
5         33     Walking
6         57     Running
7         66     Running
Run Code Online (Sandbox Code Playgroud)

查询此表后,我需要一个类似于以下的结果:

FromTime  ToTime  Name
=========================
10        29      Running
29        57      Walking
57        NULL    Running
Run Code Online (Sandbox Code Playgroud)

我玩弄了一些集合函数(例如MIN,MAX等),PARTITION和CTE,但我似乎无法找到正确的解决方案.我希望SQL大师可以帮助我,或者至少指出我正确的方向.是否有一种相当直接的方式来查询(最好没有光标?)

Eri*_*ikE 21

通过聚合而不是加入来查找"ToTime"

我想分享一个非常疯狂的查询,只需要对表进行1次扫描,并进行1次逻辑读取.相比之下,页面上最好的其他答案,Simon Kingston的查询,需要进行2次扫描.

在一组非常大的数据(17,408个输入行,产生8,193个结果行)上,它需要CPU 574和时间2645,而Simon Kingston的查询需要CPU 63,820和时间37,108.

使用索引可能会使页面上的其他查询执行得更好一些,但我很有兴趣通过重写查询来实现111x CPU改进和14x速度提升.

(请注意:我的意思是对西蒙金斯顿或其他任何人都没有任何不尊重;我对这个问题的看法感到非常兴奋.他的查询比我的好,因为它的表现很丰富,实际上是可以理解和可维护的,不像我的.)

这是不可能的查询.很难理解.写得很难.但它太棒了.:)

WITH Ranks AS (
   SELECT
      T = Dense_Rank() OVER (ORDER BY Time, Num),
      N = Dense_Rank() OVER (PARTITION BY Name ORDER BY Time, Num),
      *
   FROM
      #Data D
      CROSS JOIN (
         VALUES (1), (2)
      ) X (Num)
), Items AS (
   SELECT
      FromTime = Min(Time),
      ToTime = Max(Time),
      Name = IsNull(Min(CASE WHEN Num = 2 THEN Name END), Min(Name)),
      I = IsNull(Min(CASE WHEN Num = 2 THEN T - N END), Min(T - N)),
      MinNum = Min(Num)
   FROM
      Ranks
   GROUP BY
      T / 2
)
SELECT
   FromTime = Min(FromTime),
   ToTime = CASE WHEN MinNum = 2 THEN NULL ELSE Max(ToTime) END,
   Name
FROM Items
GROUP BY
   I, Name, MinNum
ORDER BY
   FromTime
Run Code Online (Sandbox Code Playgroud)

注意:这需要SQL 2008或更高版本.要使它在SQL 2005中工作,请将VALUES子句更改为SELECT 1 UNION ALL SELECT 2.

更新的查询

在考虑了这一点之后,我意识到我正在同时完成两个单独的逻辑任务,这使得查询不必要地复杂化:1)删除与最终解决方案无关的中间行(不开始的行)一个新的任务)和2)从下一行拉出"ToTime"值.通过在#2 之前执行#1 ,查询更简单,并且执行大约一半的CPU!

所以这里是简化的查询,首先,修剪我们不关心的行,然后使用聚合而不是JOIN获取ToTime值.是的,它确实有3个窗口函数而不是2个,但最终由于行数较少(修剪后我们不关心的那些),所以做的工作较少:

WITH Ranks AS (
   SELECT
      Grp =
         Row_Number() OVER (ORDER BY Time)
         - Row_Number() OVER (PARTITION BY Name ORDER BY Time),
      [Time], Name
   FROM #Data D
), Ranges AS (
   SELECT
      Result = Row_Number() OVER (ORDER BY Min(R.[Time]), X.Num) / 2,
      [Time] = Min(R.[Time]),
      R.Name, X.Num
   FROM
      Ranks R
      CROSS JOIN (VALUES (1), (2)) X (Num)
   GROUP BY
      R.Name, R.Grp, X.Num
)
SELECT
   FromTime = Min([Time]),
   ToTime = CASE WHEN Count(*) = 1 THEN NULL ELSE Max([Time]) END,
   Name = IsNull(Min(CASE WHEN Num = 2 THEN Name ELSE NULL END), Min(Name))
FROM Ranges R
WHERE Result > 0
GROUP BY Result
ORDER BY FromTime;
Run Code Online (Sandbox Code Playgroud)

这个更新的查询具有我在解释中提出的所有相同的问题,但是,它们更容易解决,因为我没有处理额外不需要的行.我也看到Row_Number() / 20 的值我必须排除,我不知道为什么我没有从先前的查询中排除它,但无论如何这完美地运行并且速度惊人!

外面应用整洁的东西

最后,这是一个与Simon Kingston的查询基本相同的版本,我认为这是一种更容易理解的语法.

SELECT
   FromTime = Min(D.Time),
   X.ToTime,
   D.Name
FROM
   #Data D
   OUTER APPLY (
      SELECT TOP 1 ToTime = D2.[Time]
      FROM #Data D2
      WHERE
         D.[Time] < D2.[Time]
         AND D.[Name] <> D2.[Name]
      ORDER BY D2.[Time]
   ) X
GROUP BY
   X.ToTime,
   D.Name
ORDER BY
   FromTime;
Run Code Online (Sandbox Code Playgroud)

如果要在较大的数据集上进行性能比较,请使用以下设置脚本:

CREATE TABLE #Data (
    RecordId int,
    [Time]  int,
    Name varchar(10)
);
INSERT #Data VALUES
    (1, 10, 'Running'),
    (2, 18, 'Running'),
    (3, 21, 'Running'),
    (4, 29, 'Walking'),
    (5, 33, 'Walking'),
    (6, 57, 'Running'),
    (7, 66, 'Running'),
    (8, 77, 'Running'),
    (9, 81, 'Walking'),
    (10, 89, 'Running'),
    (11, 93, 'Walking'),
    (12, 99, 'Running'),
    (13, 107, 'Running'),
    (14, 113, 'Walking'),
    (15, 124, 'Walking'),
    (16, 155, 'Walking'),
    (17, 178, 'Running');
GO
insert #data select recordid + (select max(recordid) from #data), time + (select max(time) +25 from #data), name from #data
GO 10
Run Code Online (Sandbox Code Playgroud)

说明

这是我查询背后的基本思想.

  1. 表示开关的时间必须出现在两个相邻的行中,一个用于结束先前的活动,一个用于开始下一个活动.对此的自然解决方案是连接,以便输出行可以从其自己的行(对于开始时间)和下一个已更改的行(对于结束时间)拉出.

  2. 但是,我的查询通过重复两次行来完成将结束时间显示在两个不同行中的需要CROSS JOIN (VALUES (1), (2)).我们现在所有行都重复了.我们的想法是,不是使用JOIN来跨列进行计算,而是使用某种形式的聚合将每个所需的行折叠为一个.

  3. 接下来的任务是使每个重复的行正确分割,以便一个实例与前一对一起,另一个与下一对一起.这是通过T列完成的,按ROW_NUMBER()顺序排序Time,然后除以2(虽然我改变它做DENSE_RANK()用于对称,因为在这种情况下它返回与ROW_NUMBER相同的值).为了提高效率,我在下一步中执行了除法,以便行号可以在另一个计算中重复使用(保持读数).由于行号从1开始,除以2隐式转换为int,这具有产生0 1 1 2 2 3 3 4 4 ...具有所需结果的序列的效果:通过按此计算值分组,因为我们也按顺序排序Num在行号中,我们现在已经完成了第一个之后的所有集合都包含来自"先前"行的Num = 2,以及来自"下一个"行的Num = 1.

  4. 下一个困难的任务是找出一种方法来消除我们不关心的行,并以某种方式将块的开始时间折叠到与块的结束时间相同的行中.我们想要的是一种方法,让每个离散的Running或Walking组都有自己的编号,这样我们就可以按它分组.DENSE_RANK()是一个自然的解决方案,但问题是它注意到ORDER BY子句中的每个值- 我们没有语法这样DENSE_RANK() OVER (PREORDER BY Time ORDER BY Name)做,除非每次更改,否则Time不会导致RANK计算更改Name.经过一番思考后,我意识到我可以从Itzik Ben-Gan的分组岛解决方案背后的逻辑中找到一点点,并且我发现排序的行Time的等级,从被划分的行的等级中减去Name并且按顺序排序Time,将产生一个值,该值对于同一组中的每一行是相同的,但与其他组不同.通用分组岛屿技术是创建都与行诸如锁步上升2个计算值4 5 61 2 3,即减去时将产生相同的值(在本示例性情况下3 3 3为的结果4 - 1,5 - 26 - 3).注意:我最初是ROW_NUMBER()为了N计算而开始的,但它没有用.正确的答案是,DENSE_RANK()虽然我很遗憾地说我不记得为什么我当时得出这个结论,我将不得不再次深入了解它.但无论如何,那是什么T-N 计算:可以分组的数字,以隔离一个状态(跑步或步行)的每个"岛屿".

  5. 但这不是结束,因为有一些皱纹.首先,每个组中的"下一个"行包含了不正确的值Name,NT.我们通过从每个组中选择Num = 2存在的行中的值来解决这个问题(但如果不存在,那么我们使用剩余的值).这会产生如下表达式CASE WHEN NUM = 2 THEN x END:这将正确地清除不正确的"下一个"行值.

  6. 经过一些实验,我意识到单独分组是不够的T - N,因为Walking组和Running组都可以有相同的计算值(在我提供的样本数据最多为17的情况下,有两个T - N值6).但简单地分组Name也解决了这个问题."Running"或"Walking"中的任何一组都不会从相反类型中获得相同数量的中间值.也就是说,由于第一组以"Running"开头,并且在下一个"Running"组之前有两个"Walking"行介入,因此N的值将比T下一个"Running"组中的值小2..我刚刚意识到,考虑到这一点的一种方法是T - N计算计算当前行之前不属于相同值"Running"或"Walking"的行数.有些想法会证明这是真的:如果我们转到第三个"运行"组,由于有一个"行走"组将它们分开,它只是第三组,所以它有不同数量的中间行进入在它之前,并且由于它从更高的位置开始,它足够高以使得值不能重复.

  7. 最后,由于我们的最后一组只包含一行(没有结束时间而我们需要显示一个NULL),我不得不投入一个可用于确定我们是否有结束时间的计算.这是通过Min(Num)表达式完成的,然后最终检测到当Min(Num)为2(意味着我们没有"下一行")时,则显示a NULL而不是Max(ToTime)值.

我希望这种解释对人们有用.我不知道我的"行倍增"技术是否通常对生产环境中的大多数SQL查询编写者有用并且适用,因为难以理解它并且维护的难度肯定会出现给下一个访问的人.代码(反应可能是"它到底在做什么!?!"然后快速"重写时间!").

如果你已经做到这一点,那么我感谢你的时间,并在我的小旅行中沉迷于令人难以置信的乐趣sql-puzzle-land.

亲自看看吧

Aka模拟"PREORDER BY":

最后一点.要查看T - N作业 - 并注意到使用我的方法的这一部分可能通常不适用于SQL社区 - 如何对示例数据的前17行运行以下查询:

WITH Ranks AS (
   SELECT
      T = Dense_Rank() OVER (ORDER BY Time),
      N = Dense_Rank() OVER (PARTITION BY Name ORDER BY Time),
      *
   FROM
      #Data D
)
SELECT
   *,
   T - N
FROM Ranks
ORDER BY
   [Time];
Run Code Online (Sandbox Code Playgroud)

这会产生:

RecordId    Time Name       T    N    T - N
----------- ---- ---------- ---- ---- -----
1           10   Running    1    1    0
2           18   Running    2    2    0
3           21   Running    3    3    0
4           29   Walking    4    1    3
5           33   Walking    5    2    3
6           57   Running    6    4    2
7           66   Running    7    5    2
8           77   Running    8    6    2
9           81   Walking    9    3    6
10          89   Running    10   7    3
11          93   Walking    11   4    7
12          99   Running    12   8    4
13          107  Running    13   9    4
14          113  Walking    14   5    9
15          124  Walking    15   6    9
16          155  Walking    16   7    9
17          178  Running    17   10   7
Run Code Online (Sandbox Code Playgroud)

重要的部分是每个"行走"或"跑步"组具有相同的值,T - N与任何其他具有相同名称的组不同.

性能

我不想说我的查询比其他人更快.然而,鉴于差异是多么显着(当没有索引时)我想以表格格式显示数字.当需要这种行到行相关的高性能时,这是一种很好的技术.

在每个查询运行之前,我用过DBCC FREEPROCCACHE; DBCC DROPCLEANBUFFERS;.我为每个查询设置MAXDOP为1,以消除并行性的时间崩溃效应.我将每个结果集选择为变量而不是将它们返回给客户端,以便仅测量性能而不测量客户端数据传输.所有查询都被赋予相同的ORDER BY子句.所有测试使用17,408个输入行,产生8,193个结果行.

以下人员/原因未显示任何结果:

RichardTheKiwi *Could not test--query needs updating*
ypercube       *No SQL 2012 environment yet :)*
Tim S          *Did not complete tests within 5 minutes*
Run Code Online (Sandbox Code Playgroud)

没有索引:

               CPU         Duration    Reads       Writes
               ----------- ----------- ----------- -----------
ErikE          344         344         99          0
Simon Kingston 68672       69582       549203      49
Run Code Online (Sandbox Code Playgroud)

有索引CREATE UNIQUE CLUSTERED INDEX CI_#Data ON #Data (Time);:

               CPU         Duration    Reads       Writes
               ----------- ----------- ----------- -----------
ErikE          328         336         99          0
Simon Kingston 70391       71291       549203      49          * basically not worse
Run Code Online (Sandbox Code Playgroud)

有索引CREATE UNIQUE CLUSTERED INDEX CI_#Data ON #Data (Time, Name);:

               CPU         Duration    Reads       Writes
               ----------- ----------- ----------- -----------
ErikE          375         414         359         0           * IO WINNER
Simon Kingston 172         189         38273       0           * CPU WINNER
Run Code Online (Sandbox Code Playgroud)

所以故事的寓意是:

适当的索引比查询向导更重要

使用适当的索引,Simon Kingston的版本总体上获胜,特别是在包含查询复杂性/可维护性时.

很好地听好了这一课!38k的读数并不是那么多,西蒙金斯顿的版本在一半的时间内和我一样.我查询的速度增加完全是由于桌面上没有索引,并且伴随着任何需要加入的查询(我的没有)的灾难性成本:全表扫描哈希匹配杀死其性能.使用索引,他的查询能够使用聚簇索引查找(也称为书签查找)执行嵌套循环,这使得事情变得非常快.

有趣的是,仅有时间的聚集索引是不够的.尽管Times是唯一的,意味着每次只发生一个Name,但它仍然需要Name作为索引的一部分才能正确使用它.

当数据满载不到1秒时,将聚集索引添加到表中!不要忽视你的索引.

  • 这个答案也有效,但它让我头疼了一下.;) (4认同)

ype*_*eᵀᴹ 9

这在SQL Server 2008中不起作用,仅在具有LAG()LEAD()分析功能的 SQL Server 2012版本中有效,但我会将其保留给具有较新版本的任何人:

SELECT Time AS FromTime
     , LEAD(Time) OVER (ORDER BY Time) AS ToTime
     , Name
FROM
  ( SELECT Time 
         , LAG(Name) OVER (ORDER BY Time) AS PreviousName
         , Name
    FROM Data  
  ) AS tmp
WHERE PreviousName <> Name 
   OR PreviousName IS NULL ;
Run Code Online (Sandbox Code Playgroud)

SQL-Fiddle中测试过

使用索引(Time, Name)将需要索引扫描.

编辑:

如果NULL有效值Name需要作为有效条目,请使用以下WHERE子句:

WHERE PreviousName <> Name 
   OR (PreviousName IS NULL AND Name IS NOT NULL)
   OR (PreviousName IS NOT NULL AND Name IS NULL) ;
Run Code Online (Sandbox Code Playgroud)