获得每组的前1行

dpp*_*dpp 474 sql t-sql group-by sql-server-2005 greatest-n-per-group

我有一张桌子,我希望得到每组的最新条目.这是表格:

DocumentStatusLogs

|ID| DocumentID | Status | DateCreated |
| 2| 1          | S1     | 7/29/2011   |
| 3| 1          | S2     | 7/30/2011   |
| 6| 1          | S1     | 8/02/2011   |
| 1| 2          | S1     | 7/28/2011   |
| 4| 2          | S2     | 7/30/2011   |
| 5| 2          | S3     | 8/01/2011   |
| 6| 3          | S1     | 8/02/2011   |
Run Code Online (Sandbox Code Playgroud)

该表将按降序分组DocumentID并按DateCreated降序排序.对于每一个DocumentID,我想获得最新状态.

我的首选输出:

| DocumentID | Status | DateCreated |
| 1          | S1     | 8/02/2011   |
| 2          | S3     | 8/01/2011   |
| 3          | S1     | 8/02/2011   |
Run Code Online (Sandbox Code Playgroud)
  • 是否有任何聚合函数只能从每个组中获得顶部?请参阅GetOnlyTheTop下面的伪代码:

    SELECT
      DocumentID,
      GetOnlyTheTop(Status),
      GetOnlyTheTop(DateCreated)
    FROM DocumentStatusLogs
    GROUP BY DocumentID
    ORDER BY DateCreated DESC
    
    Run Code Online (Sandbox Code Playgroud)
  • 如果这样的功能不存在,有什么方法可以实现我想要的输出吗?

  • 或者首先,这可能是由非标准化数据库引起的吗?我在想,因为我正在寻找的只是一行,是否status也应该位于父表中?

有关更多信息,请参阅父表:

当前Documents

| DocumentID | Title  | Content  | DateCreated |
| 1          | TitleA | ...      | ...         |
| 2          | TitleB | ...      | ...         |
| 3          | TitleC | ...      | ...         |
Run Code Online (Sandbox Code Playgroud)

父表是否应该像这样,以便我可以轻松访问其状态?

| DocumentID | Title  | Content  | DateCreated | CurrentStatus |
| 1          | TitleA | ...      | ...         | s1            |
| 2          | TitleB | ...      | ...         | s3            |
| 3          | TitleC | ...      | ...         | s1            |
Run Code Online (Sandbox Code Playgroud)

更新 我刚学会了如何使用"apply",这样可以更容易地解决这些问题.

gbn*_*gbn 696

;WITH cte AS
(
   SELECT *,
         ROW_NUMBER() OVER (PARTITION BY DocumentID ORDER BY DateCreated DESC) AS rn
   FROM DocumentStatusLogs
)
SELECT *
FROM cte
WHERE rn = 1
Run Code Online (Sandbox Code Playgroud)

如果您希望每天有2个条目,那么这将随意选择一个.要获得一天的两个条目,请改用DENSE_RANK

至于规范化与否,取决于你是否想要:

  • 保持2个地方的状态
  • 保存状态历史
  • ...

就目前而言,您可以保留状态历史记录.如果你想要父表中的最新状态(这是非规范化),你需要一个触发器来维持父状态中的"状态".或删除此状态历史记录表.

  • @domanokz:Partition By重置计数.所以在这种情况下,它表示按每个DocumentID计数 (6认同)
  • 而且......什么是`Partition By`?`With`对我来说也是新的:(无论如何我正在使用mssql 2005. (5认同)
  • 嗯,我担心性能,我将查询数百万行。SELECT * FROM (SELECT ...) 是否影响性能?另外,“ROW_NUMBER”是每行的某种子查询吗? (2认同)

dpp*_*dpp 161

我刚刚学会了如何使用cross apply.以下是在此场景中如何使用它:

 select d.DocumentID, ds.Status, ds.DateCreated 
 from Documents as d 
 cross apply 
     (select top 1 Status, DateCreated
      from DocumentStatusLogs 
      where DocumentID = d.DocumentId
      order by DateCreated desc) as ds
Run Code Online (Sandbox Code Playgroud)

  • 我刚刚针对所有提议的解决方案发布了我的计时测试结果,而你的结果却排在首位.给你一个投票:-) (14认同)
  • @TamusJRoyce你不能仅仅因为一旦情况总是更快而推断出来.这取决于.如此处所述http://sqlmag.com/database-development/optimizing-top-n-group-queries (7认同)
  • 当您已经有一个单独的“文档”表(根据输出中的需要为每组提供一行)时,这种方法效果很好。但是,如果您只使用一个表(本例中为“DocumentStatusLogs”),则首先必须对“DocumentID”(或“ROW_NUMBER()”、“MAX(ID”) 执行某种“DISTINCT”操作)`等),失去所有获得的性能。 (3认同)
  • 这实际上没有任何区别,因为该问题仍在解决中。 (2认同)
  • +1可以提高速度.这比窗口函数(如ROW_NUMBER())快得多.如果SQL识别出ROW_NUMBER()= 1之类的查询并将它们优化为Applies,那就太好了.注意:我使用OUTER APPLY作为我需要的结果,即使它们在apply中不存在. (2认同)
  • 我的评论是关于有多行,并且每组只需要这些多行之一。加入适用于您想要一对多的情况。应用程序适用于一对多,但想要过滤掉除一对一之外的所有内容的情况。场景:对于 100 个成员,给我每个人最好的电话号码(每个人可以有几个号码)。这就是 Apply 的优势所在。更少的读取 = 更少的磁盘访问 = 更好的性能。鉴于我的经验是设计糟糕的非规范化数据库。 (2认同)

Joh*_*nks 50

我已经对这里的各种建议做了一些定时,结果实际上取决于所涉及的表的大小,但最一致的解决方案是使用CROSS APPLY这些测试是针对SQL Server 2008-R2运行的,使用的是6,500条记录,另一条(相同的架构),有1.37亿条记录.被查询的列是表上主键的一部分,表宽度非常小(约30个字节).SQL Server根据实际执行计划报告时间.

Query                                  Time for 6500 (ms)    Time for 137M(ms)

CROSS APPLY                                    17.9                17.9
SELECT WHERE col = (SELECT MAX(COL)…)           6.6               854.4
DENSE_RANK() OVER PARTITION                     6.6               907.1
Run Code Online (Sandbox Code Playgroud)

我认为真正令人惊奇的是CROSS APPLY的时间是多么一致,无论涉及的行数如何.

  • 这一切都取决于数据分布和可用索引.在[dba.se](http://dba.stackexchange.com/questions/86415/retrieving-n-rows-per-group)上对此进行了详尽的讨论. (7认同)

Jos*_*lan 28

我知道这是一个旧线程,但TOP 1 WITH TIES解决方案非常好,可能有助于阅读解决方案.

select top 1 with ties
   DocumentID
  ,Status
  ,DateCreated
from DocumentStatusLogs
order by row_number() over (partition by DocumentID order by DateCreated desc)
Run Code Online (Sandbox Code Playgroud)

有关TOP条款的更多信息,请点击此处.

  • 这是imo最优雅的解决方案 (3认同)
  • 同意 - 这最好地复制了在其他版本的 SQL 和其他语言中很容易做到的事情 (2认同)
  • 希望我能不止一次投票。我已经返回这个答案大约 7000 次了。也许有一天,我会花时间去理解这一点,这样我就不必回来了。但现在不是这一天。 (2认同)
  • 嗯,“With Ties”可能会导致返回的行数多于表达式 (TOP 1) 中指定的值。如果OP只想要1,那么你需要删除这个短语,对吗? (2认同)
  • @TKBruin,这就是为什么需要按 row_number() 进行排序的原因。这允许检索每个分区的顶部记录。 (2认同)

Ari*_*iel 26

SELECT * FROM
DocumentStatusLogs JOIN (
  SELECT DocumentID, MAX(DateCreated) DateCreated
  FROM DocumentStatusLogs
  GROUP BY DocumentID
  ) max_date USING (DocumentID, DateCreated)
Run Code Online (Sandbox Code Playgroud)

什么数据库服务器 此代码不适用于所有这些代码.

关于你问题的后半部分,将状态列为专栏似乎是合理的.您可以将其保留DocumentStatusLogs为日志,但仍会将最新信息存储在主表中.

顺便说一句,如果你已经DateCreated在Documents表中有了这个列,你可以DocumentStatusLogs使用它来加入(只要DateCreated是唯一的DocumentStatusLogs).

编辑:MsSQL不支持USING,因此将其更改为:

ON DocumentStatusLogs.DocumentID = max_date.DocumentID AND DocumentStatusLogs.DateCreated = max_date.DateCreated
Run Code Online (Sandbox Code Playgroud)

  • @gbn愚蠢的版主通常会删除标题中的重要关键字,就像他们在这里所做的那样.在搜索结果或Google中找到正确答案非常困难. (5认同)
  • 线索的标题是:MSSQL.SQL Server没有USING,但这个想法没问题. (4认同)
  • 只是要指出,如果您在 `max(DateCreated)` 上有平局,这个“解决方案”仍然可以为您提供多条记录 (2认同)

Dan*_*ter 24

如果您担心性能,也可以使用MAX()执行此操作:

SELECT *
FROM DocumentStatusLogs D
WHERE DateCreated = (SELECT MAX(DateCreated) FROM DocumentStatusLogs WHERE ID = D.ID)
Run Code Online (Sandbox Code Playgroud)

ROW_NUMBER()需要SELECT语句中的所有行,而MAX则不需要.应该大大加快您的查询速度.

  • 使用datetime,您无法保证在同一日期和时间不会添加两个条目.精度不够高. (7认同)
  • ROW_NUMBER()的性能问题无法通过正确的索引解决吗?(我认为无论如何都应该这样做) (2认同)

Cli*_*int 10

这是一个相当古老的线索,但我认为我会把我的两分钱差不多,因为接受的答案对我来说并不是特别好.我在一个大型数据集上尝试了gbn的解决方案,发现它非常慢(在SQL Server 2012中500万条以上的记录> 45秒).看一下执行计划,很明显问题是它需要一个SORT操作,这会大大减慢速度.

这是我从实体框架中解除的另一种选择,它不需要SORT操作并进行非聚集索引搜索.这将上述记录集的执​​行时间减少到<2秒.

SELECT 
[Limit1].[DocumentID] AS [DocumentID], 
[Limit1].[Status] AS [Status], 
[Limit1].[DateCreated] AS [DateCreated]
FROM   (SELECT DISTINCT [Extent1].[DocumentID] AS [DocumentID] FROM [dbo].[DocumentStatusLogs] AS [Extent1]) AS [Distinct1]
OUTER APPLY  (SELECT TOP (1) [Project2].[ID] AS [ID], [Project2].[DocumentID] AS [DocumentID], [Project2].[Status] AS [Status], [Project2].[DateCreated] AS [DateCreated]
    FROM (SELECT 
        [Extent2].[ID] AS [ID], 
        [Extent2].[DocumentID] AS [DocumentID], 
        [Extent2].[Status] AS [Status], 
        [Extent2].[DateCreated] AS [DateCreated]
        FROM [dbo].[DocumentStatusLogs] AS [Extent2]
        WHERE ([Distinct1].[DocumentID] = [Extent2].[DocumentID])
    )  AS [Project2]
    ORDER BY [Project2].[ID] DESC) AS [Limit1]
Run Code Online (Sandbox Code Playgroud)

现在我假设在原始问题中没有完全指定的东西,但如果你的表格设计是你的ID列是一个自动增量ID,并且DateCreated设置为每个插入的当前日期,那么甚至如果没有运行上面的查询,你实际上可以从gbn的解决方案中获得相当大的性能提升(大约是执行时间的一半),只需从ID上订购而不是在DateCreated上进行排序,因为这将提供相同的排序顺序,并且它的排序速度更快.


Ran*_*all 9

这是该主题上最容易找到的问题之一,因此我想对此问题给出一个现代的答案(以供参考并帮助他人)。通过使用first_valueover,可以简化上述查询:

Select distinct DocumentID
  , first_value(status) over (partition by DocumentID order by DateCreated Desc) as Status
  , first_value(DateCreated) over (partition by DocumentID order by DateCreated Desc) as DateCreated
From DocumentStatusLogs
Run Code Online (Sandbox Code Playgroud)

这应该可以在Sql Server 2008及更高版本中使用。First_value可以认为是Select Top 1使用over子句时的一种完成方式。Over允许在选择列表中进行分组,因此与其编写嵌套的子查询(就像许多现有答案一样),而是以更具可读性的方式进行。希望这可以帮助。

  • 这在SQL Server 2008 R2中不起作用。我认为first_value是在2012年推出的! (2认同)
  • 非常快!我正在使用@dpp 提供的交叉应用解决方案,但是这个解决方案速度更快。 (2认同)

小智 9

这里有 3 种不同的方法来解决手头的问题,以及为每个查询建立索引的最佳选择(请自己尝试索引并查看逻辑读取、经过的时间、执行计划。我已经根据我的经验提供了建议此类查询而不针对此特定问题执行)。

方法 1:使用 ROW_NUMBER()。如果行存储索引无法提高性能,您可以尝试使用非聚集/聚集列存储索引,对于具有聚合和分组的查询以及始终在不同列中排序的表,列存储索引通常是最佳选择。

;WITH CTE AS
    (
       SELECT   *,
                RN = ROW_NUMBER() OVER (PARTITION BY DocumentID ORDER BY DateCreated DESC)
       FROM     DocumentStatusLogs
    )
    SELECT  ID      
        ,DocumentID 
        ,Status     
        ,DateCreated
    FROM    CTE
    WHERE   RN = 1;
Run Code Online (Sandbox Code Playgroud)

方法 2:使用 FIRST_VALUE。如果行存储索引无法提高性能,您可以尝试使用非聚集/聚集列存储索引,对于具有聚合和分组的查询以及始终在不同列中排序的表,列存储索引通常是最佳选择。

SELECT  DISTINCT
    ID      = FIRST_VALUE(ID) OVER (PARTITION BY DocumentID ORDER BY DateCreated DESC)
    ,DocumentID
    ,Status     = FIRST_VALUE(Status) OVER (PARTITION BY DocumentID ORDER BY DateCreated DESC)
    ,DateCreated    = FIRST_VALUE(DateCreated) OVER (PARTITION BY DocumentID ORDER BY DateCreated DESC)
FROM    DocumentStatusLogs;
Run Code Online (Sandbox Code Playgroud)

方法 3:使用 CROSS APPLY。在 DocumentStatusLogs 表上创建行存储索引覆盖查询中使用的列应该足以覆盖查询而无需列存储索引。

SELECT  DISTINCT
    ID      = CA.ID
    ,DocumentID = D.DocumentID
    ,Status     = CA.Status 
    ,DateCreated    = CA.DateCreated
FROM    DocumentStatusLogs D
    CROSS APPLY (
            SELECT  TOP 1 I.*
            FROM    DocumentStatusLogs I
            WHERE   I.DocumentID = D.DocumentID
            ORDER   BY I.DateCreated DESC
            ) CA;
Run Code Online (Sandbox Code Playgroud)


小智 5

我的代码从每个组中选择前1名

select a.* from #DocumentStatusLogs a where 
 datecreated in( select top 1 datecreated from #DocumentStatusLogs b
where 
a.documentid = b.documentid
order by datecreated desc
)


Tam*_*yce 5

从上面验证克林特的精彩而正确的​​答案:

下面两个查询之间的性能很有趣。52%位列第一。48% 是第二位。使用 DISTINCT 代替 ORDER BY 性能提高 4%。但 ORDER BY 的优点是可以按多列排序。

IF (OBJECT_ID('tempdb..#DocumentStatusLogs') IS NOT NULL) BEGIN DROP TABLE #DocumentStatusLogs END

CREATE TABLE #DocumentStatusLogs (
    [ID] int NOT NULL,
    [DocumentID] int NOT NULL,
    [Status] varchar(20),
    [DateCreated] datetime
)

INSERT INTO #DocumentStatusLogs([ID], [DocumentID], [Status], [DateCreated]) VALUES (2, 1, 'S1', '7/29/2011 1:00:00')
INSERT INTO #DocumentStatusLogs([ID], [DocumentID], [Status], [DateCreated]) VALUES (3, 1, 'S2', '7/30/2011 2:00:00')
INSERT INTO #DocumentStatusLogs([ID], [DocumentID], [Status], [DateCreated]) VALUES (6, 1, 'S1', '8/02/2011 3:00:00')
INSERT INTO #DocumentStatusLogs([ID], [DocumentID], [Status], [DateCreated]) VALUES (1, 2, 'S1', '7/28/2011 4:00:00')
INSERT INTO #DocumentStatusLogs([ID], [DocumentID], [Status], [DateCreated]) VALUES (4, 2, 'S2', '7/30/2011 5:00:00')
INSERT INTO #DocumentStatusLogs([ID], [DocumentID], [Status], [DateCreated]) VALUES (5, 2, 'S3', '8/01/2011 6:00:00')
INSERT INTO #DocumentStatusLogs([ID], [DocumentID], [Status], [DateCreated]) VALUES (6, 3, 'S1', '8/02/2011 7:00:00')
Run Code Online (Sandbox Code Playgroud)

选项1:

    SELECT
    [Extent1].[ID], 
    [Extent1].[DocumentID],
    [Extent1].[Status], 
    [Extent1].[DateCreated]
FROM #DocumentStatusLogs AS [Extent1]
    OUTER APPLY (
        SELECT TOP 1
            [Extent2].[ID], 
            [Extent2].[DocumentID],
            [Extent2].[Status], 
            [Extent2].[DateCreated]
        FROM #DocumentStatusLogs AS [Extent2]
        WHERE [Extent1].[DocumentID] = [Extent2].[DocumentID]
        ORDER BY [Extent2].[DateCreated] DESC, [Extent2].[ID] DESC
    ) AS [Project2]
WHERE ([Project2].[ID] IS NULL OR [Project2].[ID] = [Extent1].[ID])
Run Code Online (Sandbox Code Playgroud)

选项2:

SELECT 
    [Limit1].[DocumentID] AS [ID], 
    [Limit1].[DocumentID] AS [DocumentID], 
    [Limit1].[Status] AS [Status], 
    [Limit1].[DateCreated] AS [DateCreated]
FROM (
    SELECT DISTINCT [Extent1].[DocumentID] AS [DocumentID] FROM #DocumentStatusLogs AS [Extent1]
) AS [Distinct1]
    OUTER APPLY  (
        SELECT TOP (1) [Project2].[ID] AS [ID], [Project2].[DocumentID] AS [DocumentID], [Project2].[Status] AS [Status], [Project2].[DateCreated] AS [DateCreated]
        FROM (
            SELECT 
                [Extent2].[ID] AS [ID], 
                [Extent2].[DocumentID] AS [DocumentID], 
                [Extent2].[Status] AS [Status], 
                [Extent2].[DateCreated] AS [DateCreated]
            FROM #DocumentStatusLogs AS [Extent2]
            WHERE [Distinct1].[DocumentID] = [Extent2].[DocumentID]
        )  AS [Project2]
        ORDER BY [Project2].[ID] DESC
    ) AS [Limit1]
Run Code Online (Sandbox Code Playgroud)

在 Microsoft SQL Server Management Studio 中:突出显示并运行第一个块后,突出显示选项 1 和选项 2,右键单击 -> [显示估计执行计划]。然后运行整个程序来查看结果。

选项 1 结果:

ID  DocumentID  Status  DateCreated
6   1   S1  8/2/11 3:00
5   2   S3  8/1/11 6:00
6   3   S1  8/2/11 7:00
Run Code Online (Sandbox Code Playgroud)

选项 2 结果:

ID  DocumentID  Status  DateCreated
6   1   S1  8/2/11 3:00
5   2   S3  8/1/11 6:00
6   3   S1  8/2/11 7:00
Run Code Online (Sandbox Code Playgroud)

笔记:

当我希望连接是一对(多对一)时,我倾向于使用 APPLY。

如果我希望连接是一对多或多对多,我会使用 JOIN。

我使用 ROW_NUMBER() 来避免 CTE,除非我需要做一些高级的事情并且可以接受窗口性能损失。

我还避免在 WHERE 或 ON 子句中使用 EXISTS / IN 子查询,因为我经历过这会导致一些糟糕的执行计划。但里程不同。随时随地查看执行计划并分析性能!


小智 5

此解决方案可用于获取每个分区的 TOP N 最近行(在示例中,WHERE 语句中的 N 为 1,分区为 doc_id):

SELECT T.doc_id, T.status, T.date_created FROM 
(
    SELECT a.*, ROW_NUMBER() OVER (PARTITION BY doc_id ORDER BY date_created DESC) AS rnk FROM doc a
) T
WHERE T.rnk = 1;
Run Code Online (Sandbox Code Playgroud)