如何在 SQL 中获取每组的最后一条记录

Posted

技术标签:

【中文标题】如何在 SQL 中获取每组的最后一条记录【英文标题】:How to get the last record per group in SQL 【发布时间】:2011-09-06 06:33:18 【问题描述】:

我正面临一个相当有趣的问题。我有一个具有以下结构的表:

CREATE TABLE [dbo].[Event]
(
    Id int IDENTITY(1,1) NOT NULL,
    ApplicationId nvarchar(32) NOT NULL,
    Name nvarchar(128) NOT NULL,
    Description nvarchar(256) NULL,
    Date nvarchar(16) NOT NULL,
    Time nvarchar(16) NOT NULL,
    EventType nvarchar(16) NOT NULL,
    CONSTRAINT Event_PK PRIMARY KEY CLUSTERED ( Id ) WITH (
        PAD_INDEX = OFF, 
        STATISTICS_NORECOMPUTE = OFF, 
        IGNORE_DUP_KEY = OFF, 
        ALLOW_ROW_LOCKS = ON, 
        ALLOW_PAGE_LOCKS  = ON
    )
)

所以问题是我必须在网格中显示这些数据。有两个要求。第一个是显示所有事件,无论是什么应用程序抛出它们。这很简单 - 一个 select 语句会很容易地完成这项工作。

第二个要求是能够按Application 对事件进行分组。换句话说,如果ApplicationId 重复多次,则显示所有事件,只获取每个应用程序的最后一个条目。此时,此查询/视图中不再需要 Event (Id) 的主键。

您可能还注意到事件日期和时间是字符串格式。这没关系,因为它们遵循标准日期时间格式:mm/dd/yyyy 和 hh:mm:ss。我可以如下拉取:

Convert( DateTime, (Date + ' ' +  Time)) AS 'TimeStamp'

我的问题是,如果我在其余列上使用 AGGREGATE 函数,我不知道它们会如何表现:

SELECT
    ApplicationId,
    MAX(Name),
    MAX(Description),
    MAX( CONVERT(DateTime, (Date + ' ' + Time))) AS 'TimeStamp',
    MAX( EventType )
FROM
    Event
GROUP BY
    ApplicationId

我之所以犹豫是因为MAX 之类的函数将返回(子)记录集中给定列的最大值。不需要拉最后一条记录!

关于如何在每个应用程序的基础上仅选择最后一条记录的任何想法?

【问题讨论】:

使用窗口函数(在 Oracle 中,类似于 row_number() over (partition by...)),AFAIK SQL server 具有类似的功能。 【参考方案1】:

您可以使用ranking function 和common table expression。

WITH e AS
(
     SELECT *,
         ROW_NUMBER() OVER
         (
             PARTITION BY ApplicationId
             ORDER BY CONVERT(datetime, [Date], 101) DESC, [Time] DESC
         ) AS Recency
     FROM [Event]
)
SELECT *
FROM e
WHERE Recency = 1

【讨论】:

您不能只按日期和时间排序而不转换为日期时间值,因为mm/dd/yyyy 格式无法正确排序为字符串。 谢谢@Anthony Faull。这可行,但我不明白如何。 @damien 很好。我更新了 ORDER BY 子句以将美国日期(月-日-年)转换为可排序的日期。 即使这是一个非常晚的评论并且不再帮助@bleepzter,它可能会帮助其他人理解它是如何工作的:分区将数据(事件)划分为每个子集都有的子集相同的应用程序ID。每个子集按日期和时间排序。然后每一行得到一个行号。这描述了“与”部分。下面的 select 获取“with”语句的结果,并输出所有行号为 1 的条目。【参考方案2】:

从 SQL Server 2012 开始,您可以简单地

SELECT 
    [Month]
    , [First] = FIRST_VALUE(SUM([Clicks])) OVER (ORDER BY [Month])
    , [Last]  = FIRST_VALUE(SUM([Clicks])) OVER (ORDER BY [Month] DESC)
FROM 
    [dbo].[Table]
GROUP BY [Month]
ORDER BY [Month]

【讨论】:

FIRST_VALUE 与 OVER - 非常令人印象深刻!今天学到了新东西!!谢谢。 SQL 版本参考也是一个加号!【参考方案3】:

您可以使用带有 group by 的子查询 - group by 参数不需要在选择中。这假设 Id 是自动递增的,因此最大的就是最新的。

SELECT
    ApplicationId,
    Name,
    Description,
    CONVERT(DateTime, (Date + ' ' + Time)) AS 'TimeStamp',
    EventType
FROM
    Event e
WHERE
    Id in (select max(Id) from Event GROUP BY ApplicationId)

【讨论】:

【参考方案4】:
SELECT
    E.ApplicationId,
    E.Name,
    E.Description,
    CONVERT(DateTime, (E.Date + ' ' + E.Time)) AS 'TimeStamp',
    E.EventType
FROM
    Event E
    JOIN (SELECT ApplicationId,
                 MAX(CONVERT(DateTime, (Date + ' ' + Time))) AS max_date
            FROM Event
        GROUP BY ApplicationId) EM 
      on EM.ApplicationId = E.ApplicationId
     and EM.max_date = CONVERT(DateTime, (E.Date + ' ' + E.Time)))

【讨论】:

【参考方案5】:

您可以使用 subqery 或 CTE 表来执行此操作:

;WITH CTE_LatestEvents as (
SELECT
    ApplicationId,    
    MAX( CONVERT(DateTime, (Date + ' ' + Time))) AS 'LatestTimeStamp',
FROM
    Event
GROUP BY
    ApplicationId
)
SELECT
    ApplicationId,
    Name,
    Description,
    CONVERT(DateTime, (Date + ' ' + Time))) AS 'TimeStamp',
    EventType
FROM
    Event e
    Join CTE_LatestEvents le 
        on e.applicationid = le.applicationid
        and CONVERT(DateTime, (e.Date + ' ' + e.Time))) = le.LatestTimeStamp

【讨论】:

【参考方案6】:

因为那里没有 where 子句,所以记录子集就是所有记录。但是我认为你把 max 放在了错误的列上。此查询将为您提供所需的内容。

Select max(applicationid), name, description, CONVERT(DateTime, (Date + ' ' + Time)) 
from event
group by name, description, CONVERT(DateTime, (Date + ' ' + Time)) 

【讨论】:

【参考方案7】:

我认为它适用于许多愿意获取最后插入的记录的人,它应该按以下方式分组:

select * from (select * from TableName ORDER BY id DESC) AS x GROUP BY FieldName

它适用于以下情况:

表结构 ID名称状态 1 朱奈德 是的 2贾瓦德没有 3 法赫德 是的 4朱奈德没有 5 Kashif 是的

以上查询后的结果 ID名称状态 4朱奈德没有 2贾瓦德没有 3 法赫德 是的 4 Kashif 是的

这只是根据名称生成最后的分组记录。

【讨论】:

【参考方案8】:

6 年后 SQL Server 的另一个答案:

select t1.[Id], t2.[Value]  
from [dbo].[Table] t1  
  outer apply (  
    select top 1 [Value]  
      from [dbo].[Table] t2  
        where t2.[Month]=t1.[Month]  
      order by [dbo].[Date] desc  
  )  

虽然我更喜欢 Postgresql 解决方案,因为它独特的功能更易于输入且效率更高:

select distinct on (id),val  
from tbl  
order by id,val  

【讨论】:

【参考方案9】:

一开始我使用 CTE 和 row_number,但 SQL Server 认证课程中的一个示例向我展示了更好的示例(通过始终获得更好的执行计划来判断):

SELECT
  ApplicationId,
  Name,
  Description,
  CONVERT(DateTime, (Date + ' ' + Time)) AS 'TimeStamp',
  EventType
FROM
  Event AS E
WHERE
  NOT EXISTS(SELECT * FROM Event AS Newer WHERE Newer.ApplicationId = E.ApplicationId AND Newer.Id > E.Id)
GROUP BY
  ApplicationId

我假设较大的 Id 意味着较大的日期 + 时间(否则我会使用转换为日期时间,但这不是 SARGable)。此查询将找到最年轻的记录 - 不存在较年轻的记录。如果索引设置正确,这将使用索引查找。具有排名功能的替代方案通常使用表扫描,因为它对所有记录进行排名。

【讨论】:

【参考方案10】:

我有同样的问题。现在,我不想让 CTE 和“OVER”过于复杂。这是一个简单的例子。我用 MAX(DateEntered) 组编写了一个子查询。例如,如果它是 int,您可能希望按 ID 执行,这将比日期/时间更准确。在任何情况下,一旦你有了这个子查询,你只需将它内连接到你的主查询中,作为记录的过滤器。就这么简单。

a 是我的用户表。表 b 是子查询,表 c 是我想要“过滤”的表。

SELECT DISTINCT a.FirstName,a.LastName,a.ImagePath, c.MessageText
        FROM [AuthUsers] a 
            INNER JOIN (SELECT MessageFromId,MAX(DateEntered) AS LastEntered FROM ChatRoomConversation GROUP BY MessageFrom) AS b
                ON a.Id=b.MessageFromId
            INNER JOIN ChatRoomConversation c
                ON b.LastEntered=c.DateEntered

【讨论】:

以上是关于如何在 SQL 中获取每组的最后一条记录的主要内容,如果未能解决你的问题,请参考以下文章

获取每组的最后一行

sql按字段分组,并且找出每组的第一条数据

从每组的第一行和最后一行获取值

如何从 sql 查询中获取第一条和最后一条记录?

SQL:如何在 sql 中找到每组的最小值?

如何获取最后一条记录/最大值