Sql 获取每组的前1行
我有一个表,我想获得每个组的最新条目。这是桌子: DocumentStatusLogs表 该表将按DocumentID分组,并按日期降序排列。对于每个DocumentID,我希望获得最新状态 我的首选输出:Sql 获取每组的前1行,sql,tsql,sql-server-2005,group-by,greatest-n-per-group,Sql,Tsql,Sql Server 2005,Group By,Greatest N Per Group,我有一个表,我想获得每个组的最新条目。这是桌子: DocumentStatusLogs表 该表将按DocumentID分组,并按日期降序排列。对于每个DocumentID,我希望获得最新状态 我的首选输出: | DocumentID | Status | DateCreated | | 1 | S1 | 8/02/2011 | | 2 | S3 | 8/01/2011 | | 3 | S1 | 8/02/201
| DocumentID | Status | DateCreated |
| 1 | S1 | 8/02/2011 |
| 2 | S3 | 8/01/2011 |
| 3 | S1 | 8/02/2011 |
是否存在只从每个组中获取顶部的聚合函数?请参见下面的伪代码GetOnlyTheTop:
如果这样的功能不存在,有什么方法可以实现我想要的输出
或者首先,这可能是由未规范化的数据库引起的吗?我在想,既然我要查找的只是一行,那么该状态是否也应该位于父表中?
有关详细信息,请参见父表:
当前文档表
父表应该是这样的,以便我可以轻松访问其状态吗
| DocumentID | Title | Content | DateCreated | CurrentStatus |
| 1 | TitleA | ... | ... | s1 |
| 2 | TitleB | ... | ... | s3 |
| 3 | TitleC | ... | ... | s1 |
更新
我刚刚学会了如何使用apply,这使得解决这些问题变得更容易
;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个条目,那么这将任意选择一个条目。若要获取一天的两个条目,请改用密集排名
至于是否正常化,这取决于您是否希望:
在2个位置保持状态
保存状态历史记录
...
目前,您保留了状态历史记录。如果您希望父表中的最新状态也是非规范化的,那么您需要一个触发器来维护父表中的状态。或者删除此状态历史记录表
SELECT * FROM
DocumentStatusLogs JOIN (
SELECT DocumentID, MAX(DateCreated) DateCreated
FROM DocumentStatusLogs
GROUP BY DocumentID
) max_date USING (DocumentID, DateCreated)
什么数据库服务器?这段代码并不是对所有的人都有效
关于你问题的后半部分,在我看来,将状态列为一个专栏是合理的。您可以将DocumentStatusLogs保留为日志,但仍将最新信息存储在主表中
顺便说一句,如果Documents表中已经有DateCreated列,只要DateCreated在DocumentStatusLogs中是唯一的,就可以使用该列连接DocumentStatusLogs
编辑:MsSQL不支持使用,因此将其更改为:
ON DocumentStatusLogs.DocumentID = max_date.DocumentID AND DocumentStatusLogs.DateCreated = max_date.DateCreated
我刚刚学会了如何使用交叉应用。下面介绍如何在此场景中使用它:
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
在希望避免使用行计数的情况下,还可以使用左联接:
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
对于示例架构,还可以使用not in子查询,该子查询通常编译为与左联接相同的输出:
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),子查询模式将不起作用
这两种查询都比查询分析器测量的行计数查询更昂贵。但是,您可能会遇到这样的情况:它们返回结果更快,或者启用其他优化。My code从每个组中选择前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 )
如果您担心性能,也可以使用MAX:
SELECT *
FROM DocumentStatusLogs D
WHERE DateCreated = (SELECT MAX(DateCreated) FROM DocumentStatusLogs WHERE ID = D.ID)
ROW_NUMBER需要对SELECT语句中的所有行进行排序,而MAX则不需要。这将大大加快您的查询速度。在SQLite中检查,您可以将以下简单查询与GROUP BY一起使用 这里的MAX帮助可以获取每个组创建的最大日期
但MYSQL似乎没有将*列与max DateCreated的值联系起来:这是一个相当古老的线程,但我认为我应该投入我的两分钱,因为公认的答案对我来说并不特别有效。我在一个大数据集上尝试了gbn的解决方案,发现它在SQL Server 2012中的500多万条记录上的速度非常慢,超过45秒。从执行计划来看,很明显,问题在于它需要一个排序操作,这会大大降低速度 这里有一个我从实体框架中提取的替代方案,它不需要排序操作,并且执行非聚集索引搜索。这将上述记录集的执行时间减少到<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排序而不是按日期排序,实际获得gbn解决方案相当大的性能提升,大约一半的执行时间,因为这将提供相同的排序顺序,而且排序速度更快。我在这里对各种建议进行了一些计时,结果实际上取决于所涉及的表的大小,但最一致的解决方案是使用交叉应用。这些测试是针对SQL Server 2008-R2运行的,使用一个包含6500条记录的表和另一个包含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
我认为真正令人惊奇的是,无论涉及多少行,交叉应用的时间都是如此一致。这是我能做到的最普通的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
从上面验证Clint令人敬畏的正确答案: 下面两个查询之间的性能很有趣。52%是排名第一的。48%是第二位。使用DISTINCT而不是ORDER BY将性能提高4%。但是ORDERBY具有按多列排序的优势
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$的管理工作室:高亮显示并运行第一个块后,高亮显示选项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
如果希望联接为1对多或多对多,则使用联接
我避免使用行号的CTE,除非我需要做一些高级的事情,并且对窗口性能惩罚没有意见
我还避免WHERE或ON子句中的EXISTS/IN子查询,因为我经历过这会导致一些糟糕的执行计划。但里程数各不相同。在需要时随时审查执行计划和绩效档案 试试这个:
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]
如果您只想按日期返回最近创建的文档顺序,它将仅按文档ID返回排名前1的文档这是该主题中最容易找到的问题之一,因此我想给出一个现代的答案,以供参考和帮助他人。通过使用first_值和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
这在SQLServer2008及更高版本中应该可以使用。当使用over子句时,可以将第一个_值视为完成选择Top 1的一种方法。Over允许在选择列表中进行分组,因此它不像许多现有答案那样编写嵌套子查询,而是以更可读的方式进行。希望这能有所帮助。我知道这是一条老线索,但“带领带的前1个解决方案”非常不错,可能有助于阅读解决方案
select top 1 with ties
DocumentID
,Status
,DateCreated
from DocumentStatusLogs
order by row_number() over (partition by DocumentID order by DateCreated desc)
select top 1 with ties子句告诉SQL Server您希望返回每个组的第一行。但是SQL Server如何知道如何对数据进行分组呢?这就是按文档ID分区按行编号排序按日期排序创建的描述的位置。分区后的列定义了SQL Server如何对数据进行分组。在每个组中,将根据列的顺序对行进行排序。排序后,将在查询中返回每个组中的第一行
可以找到关于TOP子句的更多信息。此解决方案可用于获取示例中每个分区的前N个最新行,在WHERE语句中N为1,分区为doc\u 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;
这里有3种解决问题的方法,以及针对每个查询的最佳索引选择。请自己尝试索引,并查看逻辑读取、运行时间和执行计划。我根据自己在此类查询中的经验提供了建议,但没有针对此特定问题执行 方法1:使用行数。如果行存储索引无法提高性能,则可以尝试使用非聚集/聚集的列存储索引。对于具有聚合和分组的查询以及始终按不同列排序的表,列存储索引通常是最佳选择
;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;
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;
方法2:使用第一个_值。如果行存储索引无法提高性能,则可以尝试使用非聚集/聚集的列存储索引。对于具有聚合和分组的查询以及始终按不同列排序的表,列存储索引通常是最佳选择
;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;
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:使用交叉应用。在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;
交叉应用是我用于解决方案的方法,因为它适用于我和我的客户需求。据我所知,如果他们的数据库大幅增长,他们应该提供最佳的总体性能。我相信这可以像这样做到。这可能需要一些调整,但您可以从组中选择最大值 这些答案太过分了
SELECT
d.DocumentID,
MAX(d.Status),
MAX(d1.DateCreated)
FROM DocumentStatusLogs d, DocumentStatusLogs d1
USING(DocumentID)
GROUP BY d.DocumentID
ORDER BY DateCreated DESC
和什么是分区?With对我来说也是新的:反正我正在使用mssql 2005。@domanokz:Partition By重置计数。所以在这种情况下,它说要计算每个文档IDHM,我担心性能,我将查询数百万行。是选择*从选择。。。影响性能?另外,行号是每行的子查询吗?@domanokz:不,它不是子查询。如果你有正确的索引,那么数百万应该不是问题。无论如何,只有两种基于集合的方法:这和聚合Ariel的解决方案。所以试一下这两个…@domanokz:只要把ORDER BY DateCreated DESC改为ORDER BY ID DESC就行了。线索在标题中:MSSQL。SQL Server没有“使用”功能,但这个想法还可以。@gbn愚蠢的版主通常会从标题中删除重要的关键字,例如
他们在这里做了什么。这使得在搜索结果或Google中很难找到正确的答案。Jus要指出的是,如果您在maxDateCreatedRemove中使用maxDateCreatedRemove并完成连接代码,此解决方案仍然可以为您提供多条记录,那就行了。事实上,这没什么区别,因为问题仍然在解决。我刚刚发布了针对所有建议解决方案的计时测试结果,而你的结果排在了前面。给你一张赞成票:-+1表示速度大幅提升。这比诸如ROW_NUMBER之类的窗口功能快得多。如果SQL能够识别类似于行数=1的查询并将其优化为应用程序,那就太好了。注意:我在需要结果时使用了外部应用,即使它们在应用中不存在。@TamusJRoyce您不能仅仅因为它更快就推断出来,因为它总是这样。视情况而定。如本文所述,如果您已经有一个单独的Documents表,根据输出中的需要,该表为每个组提供一行,那么这种方法非常有效。但在这种情况下,如果您只使用一个表DocumentStatusLogs,则首先必须对DocumentID或ROW_NUMBER、MAXID等执行某种不同的操作,从而失去所有获得的性能。ROW_NUMBER的性能问题不能通过适当的索引来解决吗?我觉得无论如何都应该用datetime完成,你不能保证两个条目不会在同一个日期和时间添加。精度不够高。+1表示简单@塔穆斯杰罗伊斯是对的。那怎么办?”从DocumentStatusLog D中选择*,其中ID=从DocumentStatusLog中选择ID,其中D.DocumentID=DocumentID按日期排序已创建描述限制1;'SELECT*FROM EventScheduleTbl D WHERE DatesPicked=SELECT top 1 minDatesPicked FROM EventScheduleTbl WHERE EventIDf=D.EventIDf和DatesPicked>=convertdate,getdate在我的例子中,由于引入了子查询,这种方法比使用行号慢。您应该测试不同的方法,看看哪些方法最适合您的数据。不幸的是,MaxDate不是唯一的。可以同时输入两个日期。因此,这可能会导致每组重复。但是,您可以使用标识列或GUID。“身份”列将为您提供输入的最新信息使用默认身份计算,1…x步骤1。我有点同意,但作者要求提供最新信息-除非您包含自动递增身份列,否则意味着在同一时间添加的两个项目是相等的“最新”最新记录将是一条记录。所以是的。您需要考虑自动增量标识列,这取决于数据分布和可用索引。关于这个问题,我们在上进行了详细的讨论。为了更详细地讨论和比较可能的解决方案,我建议阅读dba.se.上的类似问题:。我看了这篇文章并尝试了一下。使用group by StoreID生成错误。相关:这是否回答了您的问题?这在SQL Server 2008 R2中不起作用。我认为第一个价值观是在2012年引入的!非常快!我使用的是@dpp提供的交叉应用解决方案,但这一个更快。对于大量列Status、DateCreated等,这会对每个列进行单独的分区/排序吗,这是我所同意的最优雅的解决方案——它最好地复制了在SQL和其他语言的其他版本中很容易做到的事情,我希望我能不止一次地向上投票。我已经回答了大约7000次了。也许有一天,我会花时间去理解这一点,这样我就不必回来了。但现在不是。嗯,‘With Ties’可能会导致返回的行数超过表达式TOP 1中指定的值。如果OP只需要1,那么您需要删除这个短语,对吗?@tkbruite这就是为什么需要按行编号排序的原因。这允许检索每个分区的顶部记录。您应该始终描述SQL语句的工作方式,并解决OP的查询。这将返回表中的所有内容。感谢提出的不同解决方案。我今天通过了第二个,救了我,伙计!我有一张100米行的表格,在那里我需要获得每组的第一个和最后一个记录。前两种方法需要几分钟才能执行。方法3用了不到一秒钟。这是t-sql吗?使用是不受支持的…mysql 8应该支持@PedroC88Yeah我提到它是因为OP指定了sql-server@PedroC88该问题似乎已更改,因此不再引用sql server。这是一个确定的答案,在标签上
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;
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;
SELECT documentid,
status,
datecreated
FROM documentstatuslogs dlogs
WHERE status = (SELECT status
FROM documentstatuslogs
WHERE documentid = dlogs.documentid
ORDER BY datecreated DESC
LIMIT 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