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

时间:2012-11-28 21:08:18

标签: sql-server sql-server-2008 tsql

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

RecordId  Time   Name
========================
1         10     Running
2         18     Running
3         21     Running
4         29     Walking
5         33     Walking
6         57     Running
7         66     Running

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

FromTime  ToTime  Name
=========================
10        29      Running
29        57      Walking
57        NULL    Running

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

5 个答案:

答案 0 :(得分:21)

发现" ToTime"通过聚合而不是加入

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

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

使用索引可能会使页面上的其他查询的执行次数提高很多次,但仅通过重写查询就可以实现111倍的CPU改进和14倍的速度提升。

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

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

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

注意:这需要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;

这个更新的查询具有我在解释中提出的所有相同的问题,但是,它们更容易解决,因为我没有处理额外不需要的行。我也看到Row_Number() / 2的值为0我不得不排除,我不确定为什么我没有将它从先前的查询中排除,但无论如何这都完美且速度非常快! / p>

外部应用整理事项

最后,这是一个与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;

如果您想在更大的数据集上进行性能比较,请点击设置脚本:

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

<强>解释

这是我查询背后的基本思路。

  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 ...:通过按此计算值分组,因为我们也按{{1在行号中,我们现在已经完成了第一个之后的所有集合都包含来自&#34;先前&#34;的Num = 2。行,&#34;下一个&#34;中的Num = 1;行。

  4. 下一个艰巨的任务是找出一种方法来消除我们不关心的行,并以某种方式将块的开始时间折叠到与块的结束时间相同的行中。我们想要的是一种方法,让每个离散的Running或Walking组都有自己的编号,这样我们就可以按它分组。 Num是一种自然的解决方案,但问题在于它会关注DENSE_RANK()子句中的每个值 - 我们没有语法要做ORDER BY以便{除DENSE_RANK() OVER (PREORDER BY Time ORDER BY Name)中的每次更改外,{1}}不会导致Time计算更改。经过一番思考后,我意识到我可以从Itzik Ben-Gan's grouped islands solution后面的逻辑中找到一点点,并且我发现RANK排序的行的等级从{{1}划分的行的等级中减去由Name排序并生成一个值,该值对于同一组中的每一行都是相同的,但与其他组不同。通用分组岛技术是创建两个计算值,它们与TimeName等行同步提升,减去后将产生相同的值(在此示例中为Time } 4 5 61 2 33 3 3的结果。注意:我最初使用4 - 1开始进行5 - 2计算,但它不起作用。正确答案是6 - 3虽然我很遗憾地说我不记得为什么我当时得出结论,我将不得不再次深入了解它。但无论如何,这就是ROW_NUMBER()计算的数字:可以分组的数字,以隔离每个&#34;岛&#34;一个状态(跑步或走路)。

  5. 但这不是结束,因为有一些皱纹。首先,&#34; next&#34;每个组中的行包含NDENSE_RANK()T-N的错误值。我们通过从每个组中选择Name行中存在的值来解决这个问题(但如果它不存在,那么我们使用剩余的值)。这会产生类似N的表达式:这将正确地清除错误的&#34; next&#34;行值。

  6. 经过一些实验,我意识到仅仅按T分组是不够的,因为步行组和运行组都可以具有相同的计算值(在我的样本中)提供的数据最多为17,有两个Num = 2值为6)。但只需按CASE WHEN NUM = 2 THEN x END进行分组也可以解决这个问题。没有任何一组&#34;跑步&#34;或&#34;步行&#34;将具有相反类型的相同数量的介入值。也就是说,因为第一组以&#34; Running&#34;开头,并且有两个&#34; Walking&#34;在下一个&#34; Running&#34;之前介入的行。组,然后N的值将比下一个&#34;运行&#34;中的T - N的值小2。组。我只是意识到考虑这个问题的一种方法是T - N计算计算当前行之前不属于相同值的行数&#34;运行&#34;或者&#34;步行&#34;。一些想法会表明这是真的:如果我们继续第三个&#34;运行&#34;小组,它只是第三组,因为有一个&#34; Walking&#34;将它们分开的组,因此在它之前有不同数量的中间行,并且由于它从较高的位置开始,它足够高,因此值不能重复。

  7. 最后,由于我们的最后一组只包含一行(没有结束时间,我们需要显示Name),我不得不投入一个可用于确定是否我们有没有结束时间。这是通过T表达式完成的,然后最终检测到Min(Num)为2时(意味着我们没有&#34; next&#34;行)然后显示T - N代替NULL值。

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

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

    自己动手

    A.k.a。模拟&#34; PREORDER BY&#34;:

    最后一点。要查看Min(Num)如何完成工作 - 并注意到使用我的方法的这一部分可能一般不适用于SQL社区 - 对示例数据的前17行运行以下查询:

    NULL

    这会产生:

    Max(ToTime)

    重要的是每组&#34;行走&#34;或&#34;跑步&#34;具有与T - N相同的值,该值与任何其他具有相同名称的组不同。

    <强>性能

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

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

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

    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
    

    没有索引:

    T - N

    索引DBCC FREEPROCCACHE; DBCC DROPCLEANBUFFERS;

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

    索引 CPU Duration Reads Writes ----------- ----------- ----------- ----------- ErikE 344 344 99 0 Simon Kingston 68672 69582 549203 49

    CREATE UNIQUE CLUSTERED INDEX CI_#Data ON #Data (Time);

    故事的寓意是:

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

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

    很好地听取了这一课! 38k的读数并不是那么多,而西蒙金斯顿的版本在一半时间里都是我的。我查询速度的提高完全是由于桌面上没有索引,以及这给任何需要加入的查询带来了灾难性的成本(我没有这样做):全表扫描Hash Match杀死了它的性能。使用索引,他的查询能够使用聚簇索引查找(a.k.a.书签查找)执行嵌套循环,这使得真正快。

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

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

答案 1 :(得分:9)

这在SQL Server 2008中不起作用,仅在具有LAG() and LEAD() analytic functions的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 ;

SQL-Fiddle

中进行测试

索引在(Time, Name)时,需要进行索引扫描。

编辑:

如果NULLName的有效值,需要将其视为有效条目,请使用以下WHERE子句:

WHERE PreviousName <> Name 
   OR (PreviousName IS NULL AND Name IS NOT NULL)
   OR (PreviousName IS NOT NULL AND Name IS NULL) ;

答案 2 :(得分:4)

我假设RecordID并不总是顺序的,因此CTE创建一个不间断的序号。

SQLFiddle

;with SequentiallyNumbered as (
    select *, N = row_number() over (order by RecordId)
      from Data)
, Tmp as (
    select A.*, RN=row_number() over (order by A.Time)
      from SequentiallyNumbered A
 left join SequentiallyNumbered B on B.N = A.N-1 and A.name = B.name
     where B.name is null)
   select A.Time FromTime, B.Time ToTime, A.Name
     from Tmp A
left join Tmp B on B.RN = A.RN + 1;

我用来测试的数据集

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');

答案 3 :(得分:4)

这是一个CTE解决方案,可以获得您正在寻找的结果:

;WITH TheRecords (FirstTime,SecondTime,[Name])
AS
(
    SELECT [Time],
    (
        SELECT MIN([Time]) 
        FROM ActivityTable at2
        WHERE at2.[Time]>at.[Time]
        AND at2.[Name]<>at.[Name]
    ),
    [Name]
    FROM ActivityTable at
)
SELECT MIN(FirstTime) AS FromTime,SecondTime AS ToTime,MIN([Name]) AS [Name]
FROM TheRecords
GROUP BY SecondTime
ORDER BY FromTime,ToTime

答案 4 :(得分:4)

我认为你基本上对“名称”从一个记录到下一个记录的变化感兴趣(按照“时间”的顺序)。如果您可以确定发生这种情况的位置,您可以生成所需的输出。

由于您提到了CTE,我将假设您使用的是SQL Server 2005+,因此可以使用ROW_NUMBER()函数。您可以使用ROW_NUMBER()作为识别连续记录对的便捷方式,然后找到“名称”发生变化的记录。

这个怎么样:

WITH OrderedTable AS
(
    SELECT
        *,
        ROW_NUMBER() OVER (ORDER BY Time) AS Ordinal
    FROM
        [YourTable]
),
NameChange AS
(
    SELECT
        after.Time AS Time,
        after.Name AS Name,
        ROW_NUMBER() OVER (ORDER BY after.Time) AS Ordinal
    FROM
        OrderedTable before
        RIGHT JOIN OrderedTable after ON after.Ordinal = before.Ordinal + 1
    WHERE
        ISNULL(before.Name, '') <> after.Name
)

SELECT
    before.Time AS FromTime,
    after.Time AS ToTime,
    before.Name
FROM
    NameChange before
    LEFT JOIN NameChange after ON after.Ordinal = before.Ordinal + 1