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
至于规范化与否,取决于你是否想要:
就目前而言,您可以保留状态历史记录.如果你想要父表中的最新状态(这是非规范化),你需要一个触发器来维持父状态中的"状态".或删除此状态历史记录表.
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)
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的时间是多么一致,无论涉及的行数如何.
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条款的更多信息,请点击此处.
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)
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则不需要.应该大大加快您的查询速度.
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上进行排序,因为这将提供相同的排序顺序,并且它的排序速度更快.
这是该主题上最容易找到的问题之一,因此我想对此问题给出一个现代的答案(以供参考并帮助他人)。通过使用first_value
和over
,可以简化上述查询:
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
允许在选择列表中进行分组,因此与其编写嵌套的子查询(就像许多现有答案一样),而是以更具可读性的方式进行。希望这可以帮助。
小智 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 )
从上面验证克林特的精彩而正确的答案:
下面两个查询之间的性能很有趣。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)