根据SQL Server中多个先前行的值提取行

时间:2013-02-07 07:25:27

标签: sql tsql

我有一个包含这样的样本数据的表:

ID  Key   User
--  ----  -----
1   a     test
2   ab    test
3   abc   test
4   abcd  test
5   e     test1
6   ef    test1
7   efg   test1
8   efgh  test1
9   t     test1
10  ty    test1
11  tyu   test1
12  tyui  test1

数据由用户构建的值的顺序“快照”组成。我想为每个用户实例返回最后一行,构建一个不同的最终Key值。请注意大多数行Key如何包含整个前一行的Key加上另一个字母?我只想要终止这样一个序列的行,并且是Keys的每个链中可能的最长值,它们连续包含先前的Key值。

以上示例数据应返回以下内容:

ID  Key   User
--  ----  -----
4   abcd  test
8   efgh  test1
12  tyui  test1

我该怎么做?

7 个答案:

答案 0 :(得分:6)

如果没有我的问题的答案,我必须做出这些假设:

  • ID列代表年表,并且总是增加一个,没有间隙。
  • SQL Server 2005或更高版本

(更新:我做了一个小调整,使得这个工作与来自不同用户的“交错”数据一起使用,并为我的小提琴添加了一些交错和一些棘手的数据。)

所以这是我的解决方案。 See it in a SqlFiddle。值得注意的是,它在没有LEAD的情况下模拟SQL Server 2012中的JOIN分析。

WITH Info AS (
  SELECT
     Grp = Row_Number() OVER (PARTITION BY UserName ORDER BY ID, Which) / 2,
     *
  FROM
     dbo.UserEntry U
     CROSS JOIN (
        SELECT 1 UNION ALL SELECT 2
     ) X (Which)
)
SELECT
   ID = Max(V.ID),
   DataKey = Max(V.DataKey),
   UserName = Max(V.UserName)
FROM
   Info I
   OUTER APPLY (SELECT I.* WHERE Which = 2) V
WHERE I.Grp > 0
GROUP BY
   I.UserName,
   I.Grp
HAVING
   Max(I.DataKey) NOT LIKE Min(I.DataKey) + '_';

输入:

INSERT dbo.UserEntry (ID, DataKey, UserName)
VALUES
(1, 'a', 'test'),
(2, 'ab', 'test'),
(3, 'e', 'test1'),
(4, 'ef', 'test1'),
(5, 'abc', 'test'),
(6, 'abcd', 'test'),
(7, 'efg', 'test1'),
(8, 'efgh', 'test1'),
(9, 't', 'test1'),
(10, 'ty', 'test1'),
(11, 'tyu', 'test1'),
(12, 'tyui', 'test1'),
(13, 't', 'test1'),
(14, 'a', 'test'),
(15, 'a', 'test'),
(16, 'ab', 'test'),
(17, 'abc', 'test'),
(18, 'abcd', 'test'),
(19, 'to', 'test1'), 
(20, 'abcde', 'test'),
(21, 'top', 'test1');

输出:

ID  DataKey  UserName
--  -------  --------
6   abcd     test
8   efgh     test1
12  tyui     test1
14  a        test
20  abcde    test
21  top      test1

注意:我使用了不同的列名,因为使用保留字作为列名不是最佳做法(它会强制您在名称周围放置方括号)。

我使用的技术可以使用单次扫描。它没有连接。正确构造的基于连接的查询利用适当的索引可能在CPU和时间上略胜一筹,但此解决方案肯定会有最少的读取。

<强>更新

虽然我的查询可能很好,但这个问题中的特定数据结构有助于我在第一次回答时没有考虑的非常优雅的解决方案。感谢Andriy的基本想法,这里有一个炸药和超简单的查询(与上面相同的小提琴)。

WITH Info AS (
   SELECT
      Grp = Row_Number() OVER (PARTITION BY UserName ORDER BY ID) - Len(DataKey),
         *
   FROM
      dbo.UserEntry U
)
SELECT
   ID = Max(I.ID),
   DataKey = Max(I.DataKey),
   I.UserName
FROM
   Info I
GROUP BY
   I.UserName,
   I.Grp;

答案 1 :(得分:2)

这是另一种方法:

  1. 使用首字母作为序列的分组标准。

  2. 为每个用户分别对行进行排名,按ID排序,并从排名中减去Key值的长度。将结果用作另一个序列分组标准。

  3. 再次对行进行排名,这次按用户和#1和#2的标准对它们进行分区,然后按ID的降序对它们进行排序。

  4. 获取排名为1的行。

  5. 这是一个实现:

    WITH partitioned AS (
      SELECT
        *,
        SeqKey = LEFT([Key], 1),
        SeqGrp = ROW_NUMBER() OVER (
          PARTITION BY UserName
          ORDER BY ID
        ) - LEN([Key])
      FROM dbo.UserEntry
    ),
    ranked AS (
      SELECT
        ID,
        [Key],
        UserName,
        rnk = ROW_NUMBER() OVER (
          PARTITION BY UserName, SeqKey, SeqGrp
          ORDER BY ID DESC
        )
      FROM partitioned
    )
    SELECT
      ID,
      [Key],
      UserName
    FROM ranked
    WHERE rnk = 1
    ;
    

    与@ ErikE的解决方案一样,假设ID列定义了序列的顺序。但是,如果与相同序列相关的ID值存在间隙,则上述查询仍将正常工作。

    您也可以尝试此查询at SQL Fiddle。 (注意:该演示使用@ ErikE的DDL。)

答案 2 :(得分:1)

这个版本假设字符只添加到值(未删除):

SELECT *
FROM dbo.UserEntry t1
WHERE 
  NOT EXISTS (
      SELECT *
      FROM dbo.UserEntry t2
      WHERE t1.username = t2.username 
      AND t2.dataKey LIKE t1.dataKey + '%'
      AND t2.ID = t1.ID + 1
   )

您可以轻松更改此设置,以便删除dataKey中的字符。

SqlFiddle

以下是使用LEAD函数执行此操作的另一种方法。这样,您可以通过使用以下行中的值扩展原始行来使主选择更简单:

WITH UserEntryWithNext AS (
SELECT 
  t1.*, 
  LEAD(t1.DataKey,1,0) OVER (ORDER BY ID) AS NextDataKey
FROM dbo.UserEntry t1
)
SELECT * 
FROM UserEntryWithNext 
WHERE NOT NextDataKey LIKE DataKey + '%' 

答案 3 :(得分:1)

此查询应该为您提供正确的结果。我正在考虑ID之间可能存在一些差距(可能存在一些缺失的ID,或者用户可能同时构建序列)。

在内部查询中,我将返回表中的每一行,并使用引用同一用户的先前ID prevID进行处理。然后,我再次使用yourtable加入此查询的结果:

SELECT
  t.ID, t.DataKey, t.UserName
FROM
  yourtable t LEFT JOIN (
    SELECT t1.ID id,
           max(t1.DataKey) DataKey,
           max(t1.UserName) UserName,
           max(t2.ID) prevID
    FROM
      yourtable t1 LEFT JOIN yourtable t2
      ON
        t1.ID>t2.ID
        AND t1.UserName = t2.UserName
    GROUP BY t1.ID
  ) t2
  ON t2.prevID = t.ID
     AND t2.UserName = t.UserName
     AND t2.DataKey LIKE CONCAT(t.DataKey, '_')
WHERE t2.ID is NULL

只有当前行是序列的一部分才会成功连接。如果该行是序列的最后一个DataKey,则连接将不会成功,并且我将返回该行。

请参阅小提琴here

答案 4 :(得分:0)

EXISTS选项

SELECT *
FROM dbo.test37 t1
WHERE EXISTS (
              SELECT *
              FROM dbo.test37 t2
              WHERE t1.[user] = t2.[user]
              GROUP BY LEFT([Key], 1), [User]
              HAVING MAX([Key]) = t1.[Key]
              )

SQLFiddle上的演示

更新

;WITH cte AS
 (      
  SELECT t1.[Key], t1.[User], ROW_NUMBER() OVER(ORDER BY t1.[User], t1.[Key]) AS Id
  FROM dbo.test37 t1
  )
  SELECT c1.[Key], c1.[User]
  FROM cte c1 LEFT JOIN cte c2 ON c1.ID + 1 = c2.Id
  WHERE ISNULL(c2.[Key], '') NOT LIKE ISNULL(c1.[Key], '') + '%'

答案 5 :(得分:0)

因此,您存储了输入某些数据的完整历史记录,并且您只想获得最终的注册数据。有没有人在这看到问题?

为什么不存储最终提交的值?或者写一个javascript,它将使用ajax来推动离开该领域的价值?

对陷入困境的数据库表进行复杂而详尽的查询并不是一个好主意。总是试着以便于获取的方式存储您以后需要的内容,即使您必须花一些时间来处理数据或以编程方式获取数据。

想象一下,在这样的表中有数百万行。如果你必须经常执行它,双嵌套查询将终止你的数据库。

答案 6 :(得分:0)

请找到我的部分答案:我们如何知道序列是否已从新的开始?如果能够记录序列ID,它肯定会使答案更容易。

select * from UserEntry 
where ID in (
  select max(ID) from UserEntry group by SeqNum
)

-- that assumes seqnum globally unique.  If only unique per person then group by SeqNum, UserName

-- Create data:
CREATE TABLE dbo.UserEntry (
  ID int,
  DataKey varchar(20),
  SeqNum int,
  UserName varchar(10)
);

INSERT dbo.UserEntry (ID, DataKey, SeqNum, UserName)
VALUES
  (1, 'a', 1 , 'test'),
  (2, 'ab', 1 , 'test'),
  (3, 'abc',  1 ,'test'),
  (4, 'abcd',  1 ,'test'),
  (5, 'e', 2 , 'test1'),
  (6, 'ef', 2 , 'test1'),
  (7, 'efg', 2 , 'test1'),
  (8, 'efgh',  2 ,'test1'),
  (9, 't',  3 ,'test1'),
  (10, 'ty', 3 , 'test1'),
  (11, 'tyu', 3 , 'test1'),
  (12, 'tyui',  3 ,'test1'),
  (13, 't',  4 ,'test1'),
  (14, 'to',  4 ,'test1'), 
  (15, 'top',  4 ,'test1');

SQL Fiddle