Pas*_*sca 9 performance sql-server t-sql greatest-n-per-group query-performance
我正在寻找返回每个参考号的最新记录的最快方法。
我非常喜欢BrentOzar.com 的解决方案,但是当我添加第三个条件 (SequenceId) 时,它似乎不起作用。它似乎只有在我指定 Id 和创建日期时才有效。
要理解我的问题,您需要创建修改后的示例表,它本质上是上述参考网站上的表的副本,但稍有改动。
CREATE TABLE [dbo].[TestTable](
[Id] [int] NOT NULL,
[EffectiveDate] [date] NOT NULL,
[SequenceId] [bigint] IDENTITY(1,1) NOT NULL,
[CustomerId] [varchar](50) NOT NULL,
[AccountNo] [varchar](50) NOT NULL,
CONSTRAINT [PK_TestTable] PRIMARY KEY CLUSTERED
(
[Id] ASC,
[EffectiveDate] ASC,
[SequenceId] ASC
) ON [PRIMARY]
) ON [PRIMARY]
GO
SET IDENTITY_INSERT [dbo].[TestTable] ON
INSERT [dbo].[TestTable] ([Id], [EffectiveDate], [SequenceId], [CustomerId], [AccountNo])
VALUES (1, CAST(0xDF300B00 AS Date), 1, N'Blue', N'Green')
INSERT [dbo].[TestTable] ([Id], [EffectiveDate], [SequenceId], [CustomerId], [AccountNo])
VALUES (2, CAST(0xDF300B00 AS Date), 2, N'Yellow', N'Blue')
INSERT [dbo].[TestTable] ([Id], [EffectiveDate], [SequenceId], [CustomerId], [AccountNo])
VALUES (1, CAST(0xE0300B00 AS Date), 3, N'Red', N'Yellow')
INSERT [dbo].[TestTable] ([Id], [EffectiveDate], [SequenceId], [CustomerId], [AccountNo])
VALUES (3, CAST(0xE0300B00 AS Date), 4, N'Green', N'Purple')
INSERT [dbo].[TestTable] ([Id], [EffectiveDate], [SequenceId], [CustomerId], [AccountNo])
VALUES (1, CAST(0xE1300B00 AS Date), 5, N'Orange', N'Purple')
INSERT [dbo].[TestTable] ([Id], [EffectiveDate], [SequenceId], [CustomerId], [AccountNo])
VALUES (2, CAST(0xE3300B00 AS Date), 6, N'Blue', N'Orange')
INSERT [dbo].[TestTable] ([Id], [EffectiveDate], [SequenceId], [CustomerId], [AccountNo])
VALUES (3, CAST(0xE6300B00 AS Date), 7, N'Red', N'Blue')
SET IDENTITY_INSERT [dbo].[TestTable] OFF
GO
Run Code Online (Sandbox Code Playgroud)
如果我在网站上运行类似的查询,我会得到完全相同的结果。
SELECT tt.*
FROM dbo.TestTable tt
LEFT OUTER JOIN dbo.TestTable ttNewer
ON tt.id = ttNewer.id AND tt.EffectiveDate < ttNewer.EffectiveDate
WHERE ttNewer.id IS NULL
Run Code Online (Sandbox Code Playgroud)
然而,有点小变化是我向表中添加了一个 SequenceId 列,您可能已经注意到了。此列的目的是因为客户可能想要为过去的日期做一个发布日期的条目。此条目必须取代在过去的同一日期所做的其他条目。如果我在添加发布日期的条目之前运行查询,我会得到与以前相同的结果。
SELECT tt.*
FROM dbo.TestTable tt
LEFT OUTER JOIN dbo.TestTable ttNewer
ON (
tt.id = ttNewer.id AND tt.EffectiveDate < ttNewer.EffectiveDate
AND tt.SequenceId < ttNewer.SequenceId
)
WHERE ttNewer.Id IS NULL
Run Code Online (Sandbox Code Playgroud)
如果我添加两个发布日期的条目,如下所示,然后我开始得到有趣的结果。
INSERT INTO TestTable(Id,EffectiveDate,CustomerId,AccountNo) values
(
2,'20090103','Blue','Orange'
);
INSERT INTO TestTable(Id,EffectiveDate,CustomerId,AccountNo) values
(
2,'20090105','Blue','Orange'
);
Run Code Online (Sandbox Code Playgroud)
您应该注意的是,无论我使用与您网站上的内容相似的查询还是使用添加另一个条件 (SequenceId) 的查询,下面的两个查询都不再返回最后一条记录
SELECT tt.*
FROM dbo.TestTable tt
LEFT OUTER JOIN dbo.TestTable ttNewer
ON tt.id = ttNewer.id AND tt.EffectiveDate < ttNewer.EffectiveDate
WHERE ttNewer.id IS NULL
SELECT tt.*
FROM dbo.TestTable tt
LEFT OUTER JOIN dbo.TestTable ttNewer
ON (
tt.id = ttNewer.id AND tt.EffectiveDate < ttNewer.EffectiveDate
AND tt.SequenceId < ttNewer.SequenceId
)
WHERE ttNewer.Id IS NULL
Run Code Online (Sandbox Code Playgroud)
我希望查询做的是根据任何给定日期的最后一个序列号返回参考号 (Id) 的最后一条记录。换句话说,在最近的 EffectiveDate 上具有最后一个序列号的记录。
Aar*_*and 10
自联接在低行数时看起来很便宜,但随着行数的增加,I/O 是指数级的。我更愿意以 CTE 方式解决这个问题,除非您使用的是 SQL Server 2000(请始终使用特定于版本的标签指定您需要支持的版本):
;WITH cte AS
(
SELECT Id, EffectiveDate, SequenceId, CustomerId, AccountNo,
rn = ROW_NUMBER() OVER (PARTITION BY Id
ORDER BY EffectiveDate DESC, SequenceId DESC)
FROM dbo.TestTable
)
SELECT Id, EffectiveDate, SequenceId, CustomerId, AccountNo
FROM cte
WHERE rn = 1
ORDER BY Id; -- because you can't rely on sorting without ORDER BY
Run Code Online (Sandbox Code Playgroud)
这仍然需要扫描,但它只需要扫描一次,与所有自联接变体相比,它总是有两次扫描(或者可能多次执行扫描和查找,具有更好的索引)。
如果您想要更高效的查询(消除昂贵的排序,以潜在的写入成本,以及可能不需要支持此排序的其他查询),请更改主键以匹配查询模式:
PRIMARY KEY CLUSTERED
(
[Id] ASC,
[EffectiveDate] DESC,
[SequenceId] DESC
)
Run Code Online (Sandbox Code Playgroud)
辅助列的方向对唯一性没有影响,只要表不是非常宽并且每个 id 的行数不是非常多,就应该最少地更改写入。
在最大的正每组标签有一些问题和答案有关这种类型的问题,与SQL服务器作为典型的例子:
两个主要选项是:
ROW_NUMBER
(如亚伦的回答);和APPLY
因此,虽然该问题很可能是该问题的重复(从答案技术相同的角度来看),但这里是APPLY
针对您的特定问题的解决方案模式的具体实现:
SELECT
CA.Id,
CA.EffectiveDate,
CA.SequenceId,
CA.CustomerId,
CA.AccountNo
FROM
(
-- Per Id
SELECT DISTINCT Id
FROM dbo.TestTable
) AS TT
CROSS APPLY
(
-- Single row with the highest EffectiveDate then SequenceId
SELECT TOP (1) TT2.*
FROM dbo.TestTable AS TT2
WHERE TT2.Id = TT.Id
ORDER BY TT2.EffectiveDate DESC, TT2.SequenceId DESC
) AS CA
ORDER BY
CA.Id;
Run Code Online (Sandbox Code Playgroud)
逻辑相当简单:
现有的索引使执行计划同样简单:
此计划形状的结果将在它们可用时立即流式传输到客户端(而不是在服务器端处理结束时一次性全部传输)。Stream Aggregate 是计划中唯一的部分阻塞运算符:它按 ID 顺序接收行,因此只要遇到第二个 ID,聚合就可以将其第一个结果返回给嵌套循环连接,依此类推。
聚集索引对于按顺序向流聚合提供行以及对每个 ID(按降序)进行非常有效的单行查找都很有用。这避免了计划中任何不必要的阻塞排序。这应该是一个非常有效的解决方案,除非有非常多的 ID,每个 ID 的行数平均很少,并且为替代方法提供了合适的索引。
该ROW_NUMBER
解决方案可能同样有效 - 可能更有效,具体取决于数据分布 - 但 SQL Server 查询处理器目前无法使用提供的索引来避免排序(尽管在逻辑上可以)。
在Mister Magoo 的回答中提供的较大数据集上,执行计划基本保持不变,但使用并行性:
三种方法在我的机器上的测试结果是:
当然,这对ROW_NUMBER
方法有点不公平,因为提供的索引对于该解决方案不是最佳的。