获取每个组的前1行


527

我有一张表格,我想获取每个组的最新条目。这是桌子:

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   |

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

我的首选输出:

| DocumentID | Status | DateCreated |
| 1          | S1     | 8/02/2011   |
| 2          | S3     | 8/01/2011   |
| 3          | S1     | 8/02/2011   |
  • 是否有任何汇总函数只能从每个组中获得最高排名?请参见GetOnlyTheTop下面的伪代码:

    SELECT
      DocumentID,
      GetOnlyTheTop(Status),
      GetOnlyTheTop(DateCreated)
    FROM DocumentStatusLogs
    GROUP BY DocumentID
    ORDER BY DateCreated DESC
    
  • 如果不存在这样的功能,有什么办法可以实现所需的输出?

  • 或者首先,这可能是由于数据库未规范化引起的吗?我在想,因为我要查找的只是一行,所以该行status也应该位于父表中吗?

请参阅父表以获取更多信息:

当前Documents

| DocumentID | Title  | Content  | DateCreated |
| 1          | TitleA | ...      | ...         |
| 2          | TitleB | ...      | ...         |
| 3          | TitleC | ...      | ...         |

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

| DocumentID | Title  | Content  | DateCreated | CurrentStatus |
| 1          | TitleA | ...      | ...         | s1            |
| 2          | TitleB | ...      | ...         | s3            |
| 3          | TitleC | ...      | ...         | s1            |

UPDATE 我刚刚学习了如何使用“应用”,这使得解决此类问题更加容易。


2
有关更详细的讨论和可能的解决方案的比较,我建议阅读dba.se上的类似问题:每组检索n行
弗拉基米尔·巴拉诺夫

我看着帖子,尝试了一下。使用按StoreID分组会产生错误。
UltraJ

Answers:


753
;WITH cte AS
(
   SELECT *,
         ROW_NUMBER() OVER (PARTITION BY DocumentID ORDER BY DateCreated DESC) AS rn
   FROM DocumentStatusLogs
)
SELECT *
FROM cte
WHERE rn = 1

如果您希望每天输入2个条目,则可以任意选择一个条目。要获得一天的两个条目,请改用DENSE_RANK

至于是否标准化,取决于您是否要:

  • 在两个地方保持地位
  • 保留状态记录
  • ...

就目前而言,您将保留状态历史记录。如果您还想要父表中的最新状态(这是非规范化),则需要触发器来维护父表中的“状态”。或删除此状态历史记录表。


5
还有...是什么Partition ByWith对我也是新的:(无论如何,我正在使用mssql2005。–
dpp

6
@domanokz:分区依据重置计数。因此,在这种情况下,它说
要按

1
嗯,我担心性能,我将查询数百万行。SELECT * FROM(SELECT ...)是否会影响性能?另外,ROW_NUMBER每行都有某种子查询吗?
dpp

1
@domanokz:不,它不是子查询。如果您有正确的索引,那么数百万应该不是问题。无论如何,只有2种基于集合的方式:这和聚合(Ariel的解决方案)。所以都尝试一下...
gbn

1
@domanokz:只要改变ORDER BY dateCreated会DESC到ORDER BY ID DESC
GBN

184

我刚刚学会了使用方法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

2
实际上,这没有什么区别,因为该问题仍在解决。
dpp 2012年

19
我刚刚发布了针对所有拟议解决方案的时序测试结果,而您的解决方案名列前茅。让您投票:-)
约翰·费尔班克斯

3
+1可大幅提高速度。这比诸如ROW_NUMBER()的窗口函数要快得多。如果SQL像查询一样识别ROW_NUMBER()= 1并将其优化为“应用”,那就太好了。注意:我需要结果时就使用OUTER APPLY,即使应用程序中不存在它们也是如此。
TamusJRoyce,2015年

8
@TamusJRoyce不能一概而论,因为一旦情况总是如此,它就会更快。这取决于。如此处所述sqlmag.com/database-development/optimizing-top-n-group-queries
Martin Smith

2
我的评论是关于多行,并且每个组只希望这些多行之一。联接适用于您想要一对多的情况。适用于一对多但想要过滤除一对一之外的所有内容。场景:对于100个成员,给我每个人最好的电话号码(每个人可以有几个电话号码)。这就是Apply的优势所在。更少的读取=更少的磁盘访问=更好的性能。鉴于我的经验,是设计不良的非规范化数据库。
TamusJRoyce

53

我对这里的各种建议进行了一些计时,结果实际上取决于所涉及表的大小,但是最一致的解决方案是使用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

我认为真正令人惊讶的是,无论涉及多少行,CROSS APPLY的时间是多么一致。


8
这完全取决于数据分布和可用索引。在dba.se上进行了详尽的讨论。
弗拉基米尔·巴拉诺夫

48

我知道这是一个旧线程,但是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)

有关TOP子句的更多信息,请参见此处


7
这是imo最优雅的解决方案
George Menoutis

1
同意-最好的做法是复制在其他版本的SQL和其他语言中非常容易执行的操作imo
Chris Umphlett

27

如果您担心性能,也可以使用MAX()来做到这一点:

SELECT *
FROM DocumentStatusLogs D
WHERE DateCreated = (SELECT MAX(DateCreated) FROM DocumentStatusLogs WHERE ID = D.ID)

ROW_NUMBER()要求SELECT语句中的所有行都属于某种类型,而MAX则不需要。应该大大加快您的查询。


2
ROW_NUMBER()的性能问题无法通过正确的索引解决吗?(我认为无论如何都应该这样做)
Kristoffer L

8
使用datetime,您不能保证不会在同一日期和时间添加两个条目。精度不够高。
TamusJRoyce,2015年

为简单起见,+ 1。@TamusJRoyce是正确的。关于什么?“从DocumentStatusLog D中选择*,其中ID =(从DocumentsStatusLog中选择ID,其中D.DocumentID =通过DateCreated DESC限制1的DocumentID顺序);”
cibercitizen17年

SELECT * FROM EventScheduleTbl D WHERE DatesPicked =(选择顶部1分钟(DatesPicked)FROM EventScheduleTbl WHERE EventIDf = D.EventIDf和DatesPicked> = convert(date,getdate()))
Arun Prasad ES

肯定在某些情况下,row_number()即使使用正确的索引,此操作也将胜过。我发现它在自我加入方案中特别有价值。不过,需要认识到的是,尽管报告的子树成本很低,但是该方法通常会产生更多的逻辑读取和扫描计数。您需要权衡特定情况下的成本/收益,以确定它实际上是否更好。
pimbrouwers

26
SELECT * FROM
DocumentStatusLogs JOIN (
  SELECT DocumentID, MAX(DateCreated) DateCreated
  FROM DocumentStatusLogs
  GROUP BY DocumentID
  ) max_date USING (DocumentID, DateCreated)

什么数据库服务器?此代码不适用于所有代码。

关于您问题的后半部分,我认为将状态列为一栏似乎很合理。您可以保留DocumentStatusLogs日志,但仍将最新信息存储在主表中。

顺便说一句,如果您已经DateCreated在Documents表中拥有该列,则可以使用该列进行联接DocumentStatusLogs(只要在中DateCreated是唯一的DocumentStatusLogs)。

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

ON DocumentStatusLogs.DocumentID = max_date.DocumentID AND DocumentStatusLogs.DateCreated = max_date.DateCreated

5
线索在标题中:MSSQL。SQL Server没有USING,但是想法还可以。
gbn

7
@gbn愚蠢的主持人通常会从标题中删除重要的关键字,就像在这里所做的那样。在搜索结果或Google中很难找到正确的答案。
NickG

2
Jus指出,如果您在“ max(DateCreated)
MoonKnight”游戏中

12

这是该主题上最容易找到的问题之一,因此,我想对此给出一个现代的答案(以供参考并帮助其他人)。通过使用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

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


2
这在SQL Server 2008 R2中不起作用。我认为first_value是在2012年推出的!
ufo

1
非常快!我使用的是@dpp提供的Cross Apply解决方案,但是这个速度更快。
MattSlay

11

这是一个很老的话题,但是我认为我会把两分钱投给我,因为接受的答案对我来说不是特别好。我在大型数据集上尝试了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]

现在,我假设原始问题中没有完全指定的内容,但是如果您的表设计是这样的,那么您的ID列是一个自动递增的ID,并且每次插入时DateCreated都设置为当前日期,那么即使无需在上面运行我的查询,您实际上可以仅通过按ID排序而不是按DateCreated排序就可以大大提高gbn解决方案的性能(大约执行时间的一半),因为这将提供相同的排序顺序,并且排序速度更快。


5

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

从#DocumentStatus中选择一个* 
 创建日期(从#DocumentStatusLogs b中选择前1个创建日期)
哪里 
a.documentid = b.documentid
按创建日期降序排序
)

3

从上面验证克林特的真棒和正确答案:

下面两个查询之间的性能很有趣。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')

选项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])

选项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]

M $的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

选项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

注意:

当我希望联接为1对(多个)中的1个时,我倾向于使用APPLY。

如果希望联接是一对多或多对多,我可以使用JOIN。

我避免使用ROW_NUMBER()进行CTE,除非我需要进行高级操作并且可以接受开窗性能损失。

我也避免在WHERE或ON子句中使用EXISTS / IN子查询,因为我经历过这种情况会导致一些糟糕的执行计划。但是里程不同。在需要的位置和时间检查执行计划并分析性能!


3

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

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

2
SELECT o.*
FROM `DocumentStatusLogs` o                   
  LEFT JOIN `DocumentStatusLogs` b                   
  ON o.DocumentID = b.DocumentID AND o.DateCreated < b.DateCreated
 WHERE b.DocumentID is NULL ;

如果您只想按DateCreated返回最近的文档订单,它将按DocumentID仅返回前1个文档


2

CROSS APPLY是我用于解决方案的方法,因为它可以为我服务,也可以满足客户的需求。从我所读的内容来看,如果他们的数据库大幅增长,应该提供最佳的整体性能。


1

这里有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;

方法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;

方法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;

1

我相信可以做到这一点。这可能需要一些调整,但是您可以从组中选择最大值。

这些答案太过分了。

SELECT
  d.DocumentID,
  MAX(d.Status),
  MAX(d1.DateCreated)
FROM DocumentStatusLogs d, DocumentStatusLogs d1
USING(DocumentID)
GROUP BY d.DocumentID
ORDER BY DateCreated DESC

0

在要避免使用row_count()的情况下,还可以使用左连接:

select ds.DocumentID, ds.Status, ds.DateCreated 
from DocumentStatusLogs ds
left join DocumentStatusLogs filter 
    ON ds.DocumentID = filter.DocumentID
    -- Match any row that has another row that was created after it.
    AND ds.DateCreated < filter.DateCreated
-- then filter out any rows that matched 
where filter.DocumentID is null 

对于示例架构,您还可以使用“不在子查询中”,通常将其编译为与左联接相同的输出:

select ds.DocumentID, ds.Status, ds.DateCreated 
from DocumentStatusLogs ds
WHERE ds.ID NOT IN (
    SELECT filter.ID 
    FROM DocumentStatusLogs filter
    WHERE ds.DocumentID = filter.DocumentID
        AND ds.DateCreated < filter.DateCreated)

请注意,如果表没有至少一个单列唯一键/约束/索引(在这种情况下为主键“ Id”),则子查询模式将不起作用。

这两个查询都比row_count()查询(由查询分析器衡量)更“昂贵”。但是,您可能会遇到这样的情况:它们更快地返回结果或启用其他优化。


0
SELECT documentid, 
       status, 
       datecreated 
FROM   documentstatuslogs dlogs 
WHERE  status = (SELECT status 
                 FROM   documentstatuslogs 
                 WHERE  documentid = dlogs.documentid 
                 ORDER  BY datecreated DESC 
                 LIMIT  1) 

0

尝试这个:

SELECT [DocumentID]
    ,[tmpRez].value('/x[2]', 'varchar(20)') AS [Status]
    ,[tmpRez].value('/x[3]', 'datetime') AS [DateCreated]
FROM (
    SELECT [DocumentID]
        ,cast('<x>' + max(cast([ID] AS VARCHAR(10)) + '</x><x>' + [Status] + '</x><x>' + cast([DateCreated] AS VARCHAR(20))) + '</x>' AS XML) AS [tmpRez]
    FROM DocumentStatusLogs
    GROUP BY DocumentID
    ) AS [tmpQry]

您应该始终描述您的SQL语句它将如何工作并解决OP的查询。
Suraj Kumar

-1

这是我能想到的最普通的TSQL

    SELECT * FROM DocumentStatusLogs D1 JOIN
    (
      SELECT
        DocumentID,MAX(DateCreated) AS MaxDate
      FROM
        DocumentStatusLogs
      GROUP BY
        DocumentID
    ) D2
    ON
      D2.DocumentID=D1.DocumentID
    AND
      D2.MaxDate=D1.DateCreated

不幸的是,MaxDate不是唯一的。可以在相同的确切时间输入两个日期。因此,这可能导致每个组重复。但是,您可以使用标识列或GUID。身份列将为您提供最新输入的身份列(正在使用默认身份计算,1 ... x步骤1)。
TamusJRoyce,2015年

好吧,我有点同意,但是作者要求提供最新的条目-除非您包括自动增量标识列,否则意味着完全相同的时间添加的两个项目都是“最新的”
富人

最新记录将是一个记录。是的 您需要考虑自动递增身份列。
TamusJRoyce '17

-2

在SQLite中检查您可以对GROUP BY使用以下简单查询

SELECT MAX(DateCreated), *
FROM DocumentStatusLogs
GROUP BY DocumentID

在这里,MAX帮助您从每个组中获取最大的DateCreated

但似乎MYSQL并不将*列与max DateCreated的值关联起来:(

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.