Pau*_*ite 97 sql-server t-sql greatest-n-per-group
我经常需要从结果集中的每个组中选择一些行。
例如,我可能想列出每个客户最近的“n”个最高或最低订单值。
在更复杂的情况下,要列出的行数可能因组而异(由分组/父记录的属性定义)。这部分绝对是可选的/为了额外的学分,而不是为了劝阻人们回答。
在 SQL Server 2005 及更高版本中解决这些类型问题的主要选项是什么?每种方法的主要优点和缺点是什么?
AdventureWorks 示例(为清晰起见,可选)
TransactionHistory,每个产品以从 M 到 R 的字母开头。n每个产品都有历史记录行,其中n是DaysToManufactureProduct 属性的五倍。TransactionDate, .tie-break on TransactionID.Rob*_*ley 81
让我们从基本场景开始。
如果我想从表中获取一些行,我有两个主要选择:排名函数;或TOP。
首先,让我们考虑Production.TransactionHistory一个特定的整个集合ProductID:
SELECT h.TransactionID, h.ProductID, h.TransactionDate
FROM Production.TransactionHistory h
WHERE h.ProductID = 800;
Run Code Online (Sandbox Code Playgroud)
这将返回 418 行,该计划显示它检查表中的每一行以寻找这一点 - 一个不受限制的聚集索引扫描,并使用一个 Predicate 来提供过滤器。797读到这里,丑死了。

所以让我们公平对待它,并创建一个更有用的索引。我们的条件要求在 上进行相等匹配ProductID,然后搜索最近的TransactionDate。我们需要TransactionID返回太多,让我们一起去:CREATE INDEX ix_FindingMostRecent ON Production.TransactionHistory (ProductID, TransactionDate) INCLUDE (TransactionID);。
完成此操作后,我们的计划发生了重大变化,将读取次数降至仅 3。因此,我们已经将事情改进了 250 多倍左右......

现在我们已经平衡了竞争环境,让我们看看最重要的选项 - 排名函数和TOP.
WITH Numbered AS
(
SELECT h.TransactionID, h.ProductID, h.TransactionDate, ROW_NUMBER() OVER (ORDER BY TransactionDate DESC) AS RowNum
FROM Production.TransactionHistory h
WHERE h.ProductID = 800
)
SELECT TransactionID, ProductID, TransactionDate
FROM Numbered
WHERE RowNum <= 5;
SELECT TOP (5) h.TransactionID, h.ProductID, h.TransactionDate
FROM Production.TransactionHistory h
WHERE h.ProductID = 800
ORDER BY TransactionDate DESC;
Run Code Online (Sandbox Code Playgroud)

您会注意到第二个 ( TOP) 查询比第一个查询简单得多,无论是在查询中还是在计划中。但非常重要的是,它们都用于TOP限制实际从索引中拉出的行数。成本只是估计值,值得忽略,但您可以看到两个计划中有很多相似之处,该ROW_NUMBER()版本做了少量额外工作来分配数字并相应地进行过滤,并且两个查询最终只执行 2 次读取他们的工作。查询优化器当然认识到过滤ROW_NUMBER()字段的想法,意识到它可以使用 Top 运算符来忽略不需要的行。这两个查询都足够好 -TOP并没有好到值得更改代码,但它更简单,对于初学者来说可能更清晰。
因此,这适用于单个产品。但是我们需要考虑如果我们需要跨多个产品这样做会发生什么。
迭代程序员将考虑循环遍历感兴趣的产品并多次调用此查询的想法,我们实际上可以避免以这种形式编写查询 - 不使用游标,而是使用APPLY. 我正在使用OUTER APPLY,认为如果没有事务,我们可能希望返回带有 NULL 的产品。
SELECT p.Name, p.ProductID, t.TransactionID, t.TransactionDate
FROM
Production.Product p
OUTER APPLY (
SELECT TOP (5) h.TransactionID, h.ProductID, h.TransactionDate
FROM Production.TransactionHistory h
WHERE h.ProductID = p.ProductID
ORDER BY TransactionDate DESC
) t
WHERE p.Name >= 'M' AND p.Name < 'S';
Run Code Online (Sandbox Code Playgroud)
对此的计划是迭代程序员的方法 - 嵌套循环,为每个产品执行 Top 操作和 Seek(我们之前进行过的 2 次读取)。这对 Product 进行了 4 次读取,对 TransactionHistory 进行了 360 次读取。

Using ROW_NUMBER(),方法是PARTITION BY在OVER子句中使用,这样我们就对每个Product重新编号。然后可以像以前一样过滤它。计划最终完全不同。TransactionHistory 上的逻辑读取减少了大约 15%,同时进行了完整的索引扫描以获取行。
WITH Numbered AS
(
SELECT p.Name, p.ProductID, h.TransactionID, h.TransactionDate, ROW_NUMBER() OVER (PARTITION BY h.ProductID ORDER BY h.TransactionDate DESC) AS RowNum
FROM Production.Product p
LEFT JOIN Production.TransactionHistory h ON h.ProductID = p.ProductID
WHERE p.Name >= 'M' AND p.Name < 'S'
)
SELECT Name, ProductID, TransactionID, TransactionDate
FROM Numbered n
WHERE RowNum <= 5;
Run Code Online (Sandbox Code Playgroud)

不过,值得注意的是,该计划有一个昂贵的 Sort 运算符。Merge Join 似乎没有维护 TransactionHistory 中的行顺序,必须使用数据才能找到行号。读取次数较少,但这种阻塞排序可能会让人感到痛苦。使用APPLY,嵌套循环将非常快速地返回第一行,只需读取几次,但使用 Sort,ROW_NUMBER()只会在大部分工作完成后返回行。
有趣的是,如果ROW_NUMBER()查询使用INNER JOIN而不是LEFT JOIN,则会出现不同的计划。

该计划使用嵌套循环,就像使用APPLY. 但是没有 Top 运算符,因此它会提取每个产品的所有事务,并使用比以前更多的读取 - 针对 TransactionHistory 的 492 读取。这里没有充分的理由不选择 Merge Join 选项,所以我想该计划被认为是“足够好”。仍然 - 它不会阻塞,这很好 - 只是不如APPLY.
在PARTITION BY我用于柱ROW_NUMBER()是h.ProductID在两种情况下,因为我想给QO生产加入到商品表之前的ROWNUM值的选项。如果我使用p.ProductID,我们会看到与INNER JOIN变化相同的形状计划。
WITH Numbered AS
(
SELECT p.Name, p.ProductID, h.TransactionID, h.TransactionDate, ROW_NUMBER() OVER (PARTITION BY p.ProductID ORDER BY h.TransactionDate DESC) AS RowNum
FROM Production.Product p
LEFT JOIN Production.TransactionHistory h ON h.ProductID = p.ProductID
WHERE p.Name >= 'M' AND p.Name < 'S'
)
SELECT Name, ProductID, TransactionID, TransactionDate
FROM Numbered n
WHERE RowNum <= 5;
Run Code Online (Sandbox Code Playgroud)
但是 Join 运算符说的是“Left Outer Join”而不是“Inner Join”。对 TransactionHistory 表的读取次数仍然不到 500 次。

无论如何 - 回到手头的问题......
我们已经回答了问题 1,有两个选项可供您选择。就个人而言,我喜欢这个APPLY选项。
要将其扩展为使用可变数(问题 2),5只需要相应地更改。哦,我添加了另一个索引,以便有一个Production.Product.Name包含该DaysToManufacture列的索引。
WITH Numbered AS
(
SELECT p.Name, p.ProductID, p.DaysToManufacture, h.TransactionID, h.TransactionDate, ROW_NUMBER() OVER (PARTITION BY h.ProductID ORDER BY h.TransactionDate DESC) AS RowNum
FROM Production.Product p
LEFT JOIN Production.TransactionHistory h ON h.ProductID = p.ProductID
WHERE p.Name >= 'M' AND p.Name < 'S'
)
SELECT Name, ProductID, TransactionID, TransactionDate
FROM Numbered n
WHERE RowNum <= 5 * DaysToManufacture;
SELECT p.Name, p.ProductID, t.TransactionID, t.TransactionDate
FROM
Production.Product p
OUTER APPLY (
SELECT TOP (5 * p.DaysToManufacture) h.TransactionID, h.ProductID, h.TransactionDate
FROM Production.TransactionHistory h
WHERE h.ProductID = p.ProductID
ORDER BY TransactionDate DESC
) t
WHERE p.Name >= 'M' AND p.Name < 'S';
Run Code Online (Sandbox Code Playgroud)
而且这两个计划几乎与之前的完全相同!

同样,忽略估计的成本 - 但我仍然喜欢 TOP 方案,因为它简单得多,而且该计划没有阻塞运算符。由于 中的大量零,TransactionHistory 上的读取较少DaysToManufacture,但在现实生活中,我怀疑我们会选择该列。;)
避免阻塞的一种方法是提出一个处理ROW_NUMBER()连接右侧(在计划中)的位的计划。我们可以通过在 CTE 之外进行连接来说服这种情况发生。
WITH Numbered AS
(
SELECT h.TransactionID, h.ProductID, h.TransactionDate, ROW_NUMBER() OVER (PARTITION BY ProductID ORDER BY TransactionDate DESC) AS RowNum
FROM Production.TransactionHistory h
)
SELECT p.Name, p.ProductID, t.TransactionID, t.TransactionDate
FROM Production.Product p
LEFT JOIN Numbered t ON t.ProductID = p.ProductID
AND t.RowNum <= 5 * p.DaysToManufacture
WHERE p.Name >= 'M' AND p.Name < 'S';
Run Code Online (Sandbox Code Playgroud)
这里的计划看起来更简单——不是阻塞,而是存在隐患。

请注意从 Product 表中提取数据的 Compute Scalar。这是在5 * p.DaysToManufacture计算价值。这个值没有被传递到从 TransactionHistory 表中提取数据的分支中,它被用于合并连接。作为残差。

所以合并连接消耗了所有的行,不仅仅是第一个需要的行,而是所有的行,然后做一个残差检查。随着交易数量的增加,这很危险。我不喜欢这种情况 - Merge Joins 中的残余谓词会迅速升级。我更喜欢这个APPLY/TOP场景的另一个原因。
在恰好为一行的特殊情况下,对于问题 3,我们显然可以使用相同的查询,但使用1代替5。但是我们还有一个额外的选择,那就是使用常规聚合。
SELECT ProductID, MAX(TransactionDate)
FROM Production.TransactionHistory
GROUP BY ProductID;
Run Code Online (Sandbox Code Playgroud)
像这样的查询将是一个有用的开始,我们可以很容易地修改它以提取 TransactionID 以及用于决胜局的目的(使用随后将被分解的串联),但我们要么查看整个索引,要么我们逐个产品地深入研究,在这种情况下,我们并没有真正获得比之前的大改进。
但我应该指出,我们正在研究一个特定的场景。对于真实数据以及可能不理想的索引策略,里程可能会有很大差异。尽管我们在APPLY这里看到它很强大,但在某些情况下它可能会更慢。但是它很少阻塞,因为它倾向于使用嵌套循环,很多人(包括我自己)都觉得它非常吸引人。
我没有尝试在这里探索并行性,也没有深入研究问题 3,我认为这是一种特殊情况,由于连接和拆分的复杂性,人们很少想要这种情况。这里要考虑的主要事情是这两个选项都非常强大。
我更喜欢APPLY。很明显,它很好地使用了Top运算符,并且很少导致阻塞。
Aar*_*and 48
在 SQL Server 2005 及更高版本中执行此操作的典型方法是使用 CTE 和窗口函数。对于每组前 n 个,您可以简单地使用ROW_NUMBER()一个PARTITION子句,并在外部查询中对其进行过滤。因此,例如,每个客户最近的前 5 个订单可以这样显示:
DECLARE @top INT;
SET @top = 5;
;WITH grp AS
(
SELECT CustomerID, OrderID, OrderDate,
rn = ROW_NUMBER() OVER
(PARTITION BY CustomerID ORDER BY OrderDate DESC)
FROM dbo.Orders
)
SELECT CustomerID, OrderID, OrderDate
FROM grp
WHERE rn <= @top
ORDER BY CustomerID, OrderDate DESC;
Run Code Online (Sandbox Code Playgroud)
你也可以这样做CROSS APPLY:
DECLARE @top INT;
SET @top = 5;
SELECT c.CustomerID, o.OrderID, o.OrderDate
FROM dbo.Customers AS c
CROSS APPLY
(
SELECT TOP (@top) OrderID, OrderDate
FROM dbo.Orders AS o
WHERE CustomerID = c.CustomerID
ORDER BY OrderDate DESC
) AS o
ORDER BY c.CustomerID, o.OrderDate DESC;
Run Code Online (Sandbox Code Playgroud)
使用 Paul 指定的附加选项,假设“客户”表有一列指示每个客户要包含的行数:
;WITH grp AS
(
SELECT CustomerID, OrderID, OrderDate,
rn = ROW_NUMBER() OVER
(PARTITION BY CustomerID ORDER BY OrderDate DESC)
FROM dbo.Orders
)
SELECT c.CustomerID, grp.OrderID, grp.OrderDate
FROM grp
INNER JOIN dbo.Customers AS c
ON grp.CustomerID = c.CustomerID
AND grp.rn <= c.Number_of_Recent_Orders_to_Show
ORDER BY c.CustomerID, grp.OrderDate DESC;
Run Code Online (Sandbox Code Playgroud)
再次,使用CROSS APPLY并合并添加的选项,即客户的行数由客户表中的某些列决定:
SELECT c.CustomerID, o.OrderID, o.OrderDate
FROM dbo.Customers AS c
CROSS APPLY
(
SELECT TOP (c.Number_of_Recent_Orders_to_Show) OrderID, OrderDate
FROM dbo.Orders AS o
WHERE CustomerID = c.CustomerID
ORDER BY OrderDate DESC
) AS o
ORDER BY c.CustomerID, o.OrderDate DESC;
Run Code Online (Sandbox Code Playgroud)
请注意,这些将根据数据分布和支持索引的可用性而不同,因此优化性能和获得最佳计划实际上取决于本地因素。
就个人而言,我更喜欢 CTE 和窗口解决方案而不是CROSS APPLY/TOP因为它们更好地分离了逻辑并且更直观(对我来说)。一般来说(在这种情况下和我的一般经验中),CTE 方法会产生更有效的计划(下面的示例),但这不应该被视为普遍真理 - 您应该始终测试您的场景,尤其是在索引已更改或数据出现明显偏差。
- 列出表中五个最近的交易日期和 ID
TransactionHistory,每个产品以从 M 到 R 的字母开头。
-- CTE / OVER()
;WITH History AS
(
SELECT p.ProductID, p.Name, t.TransactionID, t.TransactionDate,
rn = ROW_NUMBER() OVER
(PARTITION BY t.ProductID ORDER BY t.TransactionDate DESC)
FROM Production.Product AS p
INNER JOIN Production.TransactionHistory AS t
ON p.ProductID = t.ProductID
WHERE p.Name >= N'M' AND p.Name < N'S'
)
SELECT ProductID, Name, TransactionID, TransactionDate
FROM History
WHERE rn <= 5;
-- CROSS APPLY
SELECT p.ProductID, p.Name, t.TransactionID, t.TransactionDate
FROM Production.Product AS p
CROSS APPLY
(
SELECT TOP (5) TransactionID, TransactionDate
FROM Production.TransactionHistory
WHERE ProductID = p.ProductID
ORDER BY TransactionDate DESC
) AS t
WHERE p.Name >= N'M' AND p.Name < N'S';
Run Code Online (Sandbox Code Playgroud)
这两者在运行时指标中的比较:

CTE/OVER()计划:

CROSS APPLY 计划:

CTE计划看起来更复杂,但实际上效率更高。很少关注估计的成本百分比数字,而是关注更重要的实际观察结果,例如更少的读取和更短的持续时间。我也没有并行地运行这些,这不是区别。运行时指标和 CTE 计划(CROSS APPLY计划保持不变):


- 再次相同,但
n每个产品都有历史记录行,其中n是DaysToManufactureProduct 属性的五倍。
这里需要非常小的改动。对于CTE,我们可以在内部查询中添加一列,并在外部查询上进行过滤;对于CROSS APPLY,我们可以在相关TOP. 您可能认为这会给CROSS APPLY解决方案带来一些效率,但在这种情况下不会发生。查询:
-- CTE / OVER()
;WITH History AS
(
SELECT p.ProductID, p.Name, p.DaysToManufacture, t.TransactionID, t.TransactionDate,
rn = ROW_NUMBER() OVER
(PARTITION BY t.ProductID ORDER BY t.TransactionDate DESC)
FROM Production.Product AS p
INNER JOIN Production.TransactionHistory AS t
ON p.ProductID = t.ProductID
WHERE p.Name >= N'M' AND p.Name < N'S'
)
SELECT ProductID, Name, TransactionID, TransactionDate
FROM History
WHERE rn <= (5 * DaysToManufacture);
-- CROSS APPLY
SELECT p.ProductID, p.Name, t.TransactionID, t.TransactionDate
FROM Production.Product AS p
CROSS APPLY
(
SELECT TOP (5 * p.DaysToManufacture) TransactionID, TransactionDate
FROM Production.TransactionHistory
WHERE ProductID = p.ProductID
ORDER BY TransactionDate DESC
) AS t
WHERE p.Name >= N'M' AND p.Name < N'S';
Run Code Online (Sandbox Code Playgroud)
运行时结果:

并行 CTE/OVER()计划:

单线程 CTE/OVER()计划:

CROSS APPLY 计划:

- 同样,对于每个产品恰好需要一条历史记录行的特殊情况(最近的单个条目由
TransactionDate, .tie-break onTransactionID.
再次,这里的细微变化。在 CTE 解决方案中,我们添加TransactionID到OVER()子句中,并将外部过滤器更改为rn = 1. 对于CROSS APPLY,我们将 更改TOP为TOP (1),并添加TransactionID到内部ORDER BY。
-- CTE / OVER()
;WITH History AS
(
SELECT p.ProductID, p.Name, t.TransactionID, t.TransactionDate,
rn = ROW_NUMBER() OVER
(PARTITION BY t.ProductID ORDER BY t.TransactionDate DESC, TransactionID DESC)
FROM Production.Product AS p
INNER JOIN Production.TransactionHistory AS t
ON p.ProductID = t.ProductID
WHERE p.Name >= N'M' AND p.Name < N'S'
)
SELECT ProductID, Name, TransactionID, TransactionDate
FROM History
WHERE rn = 1;
-- CROSS APPLY
SELECT p.ProductID, p.Name, t.TransactionID, t.TransactionDate
FROM Production.Product AS p
CROSS APPLY
(
SELECT TOP (1) TransactionID, TransactionDate
FROM Production.TransactionHistory
WHERE ProductID = p.ProductID
ORDER BY TransactionDate DESC, TransactionID DESC
) AS t
WHERE p.Name >= N'M' AND p.Name < N'S';
Run Code Online (Sandbox Code Playgroud)
运行时结果:

并行 CTE/OVER()计划:

单线程 CTE/OVER() 计划:

CROSS APPLY 计划:

窗口函数并不总是最好的选择(试一试COUNT(*) OVER()),而且这不是解决每组 n 行问题的唯一两种方法,但在这种特定情况下 - 考虑到模式、现有索引和数据分布 -在所有有意义的账户中,CTE 的表现都更好。
但是,如果您添加一个支持索引,类似于Paul 在评论中提到的索引,但第 2 和第 3 列已排序DESC:
CREATE UNIQUE NONCLUSTERED INDEX UQ3 ON Production.TransactionHistory
(ProductID, TransactionDate DESC, TransactionID DESC);
Run Code Online (Sandbox Code Playgroud)
你实际上会得到更有利的计划,并且CROSS APPLY在所有三种情况下,指标都会转向支持该方法:

如果这是我的生产环境,我可能会对这种情况下的持续时间感到满意,并且不会费心进一步优化。
这在不支持APPLYorOVER()子句的SQL Server 2000 中更加丑陋。
ype*_*eᵀᴹ 25
在 DBMS 中,如 MySQL,没有窗口函数或CROSS APPLY,这样做的方法是使用标准 SQL (89)。缓慢的方式是与聚合体的三角形交叉连接。更快的方法(但仍然可能不如使用 cross apply 或 row_number 函数那么有效)就是我所说的“穷人的CROSS APPLY”。将此查询与其他查询进行比较会很有趣:
假设:Orders (CustomerID, OrderDate)有一个UNIQUE约束:
DECLARE @top INT;
SET @top = 5;
SELECT o.CustomerID, o.OrderID, o.OrderDate
FROM dbo.Customers AS c
JOIN dbo.Orders AS o
ON o.CustomerID = c.CustomerID
AND o.OrderID IN
( SELECT TOP (@top) oi.OrderID
FROM dbo.Orders AS oi
WHERE oi.CustomerID = c.CustomerID
ORDER BY oi.OrderDate DESC
)
ORDER BY CustomerID, OrderDate DESC ;
Run Code Online (Sandbox Code Playgroud)
对于每组自定义顶行的额外问题:
SELECT o.CustomerID, o.OrderID, o.OrderDate
FROM dbo.Customers AS c
JOIN dbo.Orders AS o
ON o.CustomerID = c.CustomerID
AND o.OrderID IN
( SELECT TOP (c.Number_of_Recent_Orders_to_Show) oi.OrderID
FROM dbo.Orders AS oi
WHERE oi.CustomerID = c.CustomerID
ORDER BY oi.OrderDate DESC
)
ORDER BY CustomerID, OrderDate DESC ;
Run Code Online (Sandbox Code Playgroud)
注意:在 MySQL 中,AND o.OrderID IN (SELECT TOP(@top) oi.OrderID ...)将使用AND o.OrderDate >= (SELECT oi.OrderDate ... LIMIT 1 OFFSET (@top - 1)). SQL-ServerFETCH / OFFSET在 2012 版本中添加了语法。此处的查询经过调整IN (TOP...)以适用于早期版本。
Sol*_*zky 23
我采取了一种稍微不同的方法,主要是为了看看这种技术与其他技术相比如何,因为有选择是好的,对吧?
为什么我们不先看看各种方法是如何相互叠加的。我做了三组测试:
TransactionDate基于Production.TransactionHistory.额外的测试细节:
AdventureWorks2012在 SQL Server 2012 SP2(开发人员版)上运行的。RowCounts我的方法似乎“关闭”。这是因为我的方法是对正在执行的操作的手动实现CROSS APPLY:它运行初始查询Production.Product并返回 161 行,然后将其用于针对Production.TransactionHistory. 因此,RowCount我的条目的值总是比其他条目多 161。在第三组测试(带缓存)中,所有方法的行数都相同。Name >= N'M' AND Name < N'S'我没有使用构造,而是选择使用Name LIKE N'[M-R]%',并且 SQL Server 将它们视为相同的。这本质上是开箱即用的 AdventureWorks2012。在所有情况下,我的方法明显优于其他一些方法,但永远不如前 1 或 2 种方法好。
测试 1

Aaron 的 CTE 显然是这里的赢家。
测试 2

Aaron 的 CTE(再次)和 Mikael 的第二种apply row_number()方法紧随其后。
测试 3

Aaron 的 CTE(再次)是赢家。
结论
在没有支持索引的情况下TransactionDate,我的方法比做标准要好CROSS APPLY,但是,使用 CTE 方法显然是要走的路。
对于这组测试,我添加了明显的索引,TransactionHistory.TransactionDate因为所有查询都在该字段上排序。我说“显而易见”,因为大多数其他答案也同意这一点。由于查询都需要最近的日期,所以TransactionDate应该对字段进行排序DESC,所以我只是抓住了CREATE INDEXMikael 答案底部的语句并添加了一个明确的FILLFACTOR:
CREATE INDEX [IX_TransactionHistoryX]
ON Production.TransactionHistory (ProductID ASC, TransactionDate DESC)
WITH (FILLFACTOR = 100);
Run Code Online (Sandbox Code Playgroud)
一旦这个索引到位,结果就会发生很大的变化。
测试 1

这次是我的方法领先,至少在逻辑读取方面。该CROSS APPLY方法以前在测试 1 中表现最差,在 Duration 上获胜,甚至在逻辑读取上击败了 CTE 方法。
测试 2

这次是 Mikael 的第apply row_number()一种方法,在查看 Reads 时是赢家,而之前它是表现最差的方法之一。现在,在查看 Reads 时,我的方法排在第二位。事实上,在 CTE 方法之外,其余的在 Reads 方面都相当接近。
测试 3

在这里,CTE 仍然是赢家,但现在与创建索引之前存在的巨大差异相比,其他方法之间的差异几乎不明显。
结论
我的方法的适用性现在更加明显,尽管它在没有适当索引的情况下弹性较差。
对于这组测试,我使用了缓存,因为,为什么不呢?我的方法允许使用其他方法无法访问的内存缓存。公平地说,我创建了以下临时表,用于代替Product.Product所有三个测试中其他方法中的所有引用。该DaysToManufacture字段仅在测试编号 2 中使用,但更容易在 SQL 脚本中保持一致以使用同一个表,并且在那里使用它也没有什么坏处。
CREATE TABLE #Products
(
ProductID INT NOT NULL PRIMARY KEY,
Name NVARCHAR(50) NOT NULL,
DaysToManufacture INT NOT NULL
);
INSERT INTO #Products (ProductID, Name, DaysToManufacture)
SELECT p.ProductID, p.Name, p.DaysToManufacture
FROM Production.Product p
WHERE p.Name >= N'M' AND p.Name < N'S'
AND EXISTS (
SELECT *
FROM Production.TransactionHistory th
WHERE th.ProductID = p.ProductID
);
ALTER TABLE #Products REBUILD WITH (FILLFACTOR = 100);
Run Code Online (Sandbox Code Playgroud)
测试 1

所有方法似乎都从缓存中受益,而我的方法仍然领先。
测试 2

在这里,我们现在看到了阵容中的差异,因为我的方法几乎没有领先,仅比 Mikael 的第apply row_number()一种方法好 2 次读取,而在没有缓存的情况下,我的方法落后 4 次读取。
测试 3

请参阅底部(线下方)的更新。在这里,我们再次看到了一些不同之处。与 Aaron 的 CROSS APPLY 方法相比,我的方法的“参数化”风格现在几乎领先 2 次读取(没有缓存,它们是相等的)。但真正奇怪的是,我们第一次看到了一种受到缓存负面影响的方法:Aaron 的 CTE 方法(以前是测试编号 3 的最佳方法)。但是,我不会在未到期的情况下获得功劳,而且由于没有缓存,Aaron 的 CTE 方法仍然比我在这里使用缓存的方法快,因此这种特殊情况的最佳方法似乎是 Aaron 的 CTE 方法。
结论 请参阅底部的更新(在该行下方)
重复使用辅助查询结果的情况通常(但不总是)从缓存这些结果中受益。但是当缓存是一个好处时,使用内存进行缓存比使用临时表有一些优势。
我将“标题”查询(即获取ProductIDs,在一种情况下还有DaysToManufacture, 基于Name以某些字母开头)与“详细信息”查询(即获取TransactionIDs 和TransactionDates)分开。这个概念是执行非常简单的查询,并且在加入它们时不允许优化器混淆。显然,这并不总是有利的,因为它也不允许优化器进行优化。但正如我们在结果中看到的,根据查询的类型,这种方法确实有其优点。
这种方法的各种口味之间的区别是:
常量:提交任何可替换的值作为内联常量而不是参数。这将ProductID在所有三个测试中以及在测试 2 中返回的行数都引用,因为它是“DaysToManufacture产品属性的五倍”的函数。这种子方法意味着每个方法都ProductID将获得自己的执行计划,如果 的数据分布差异很大,这将是有益的ProductID。但是,如果数据分布几乎没有变化,则生成额外计划的成本可能不值得。
参数化:至少提交ProductID为@ProductID,允许执行计划缓存和重用。还有一个额外的测试选项,也可以将要为测试 2 返回的可变行数视为参数。
优化未知:当引用ProductIDas 时@ProductID,如果数据分布变化很大,那么可以缓存对其他ProductID值有负面影响的计划,因此最好知道使用此查询提示是否有帮助。
缓存产品:而不是Production.Product每次都查询表,只是为了获得完全相同的列表,运行一次查询(当我们在它时,过滤掉任何ProductID不在TransactionHistory表中的 s,这样我们就不会浪费任何资源)并缓存该列表。该列表应包括该DaysToManufacture字段。使用此选项,第一次执行时逻辑读取的初始命中略高,但之后仅TransactionHistory查询表。
好的,但是,嗯,如何在不使用 CURSOR 并将每个结果集转储到临时表或表变量的情况下,将所有子查询作为单独的查询发出?显然,执行 CURSOR / Temp Table 方法会在 Reads 和 Writes 中很明显地反映出来。好吧,通过使用 SQLCLR :)。通过创建 SQLCLR 存储过程,我能够打开一个结果集,并将每个子查询的结果作为一个连续的结果集(而不是多个结果集)流式传输到它。在产品信息(即ProductID,Name和DaysToManufacture),任何子查询结果都不必存储在任何地方(内存或磁盘),而只是作为 SQLCLR 存储过程的主要结果集传递。这让我可以做一个简单的查询来获取产品信息,然后循环遍历它,针对TransactionHistory.
而且,这就是我必须使用 SQL Server Profiler 来捕获统计信息的原因。SQLCLR 存储过程未通过设置“包括实际执行计划”查询选项或通过发出SET STATISTICS XML ON;.
对于产品信息缓存,我使用了readonly static通用列表(即_GlobalProducts在下面的代码中)。看来,加入到集合不违反readonly选项,因此该代码的工作,当组件具有PERMISSON_SET的SAFE:),即使是反直觉的。
此 SQLCLR 存储过程产生的查询如下:
测试编号 1 和 3(无缓存)
CREATE INDEX [IX_TransactionHistoryX]
ON Production.TransactionHistory (ProductID ASC, TransactionDate DESC)
WITH (FILLFACTOR = 100);
Run Code Online (Sandbox Code Playgroud)
测试编号 2(无缓存)
CREATE TABLE #Products
(
ProductID INT NOT NULL PRIMARY KEY,
Name NVARCHAR(50) NOT NULL,
DaysToManufacture INT NOT NULL
);
INSERT INTO #Products (ProductID, Name, DaysToManufacture)
SELECT p.ProductID, p.Name, p.DaysToManufacture
FROM Production.Product p
WHERE p.Name >= N'M' AND p.Name < N'S'
AND EXISTS (
SELECT *
FROM Production.TransactionHistory th
WHERE th.ProductID = p.ProductID
);
ALTER TABLE #Products REBUILD WITH (FILLFACTOR = 100);
Run Code Online (Sandbox Code Playgroud)
测试编号 1、2 和 3(缓存)
SELECT prod1.ProductID, prod1.Name, 1 AS [DaysToManufacture]
FROM Production.Product prod1
WHERE prod1.Name LIKE N'[M-R]%';
Run Code Online (Sandbox Code Playgroud)
测试编号 1 和 2(常数)
;WITH cte AS
(
SELECT prod1.ProductID
FROM Production.Product prod1 WITH (INDEX(AK_Product_Name))
WHERE prod1.Name LIKE N'[M-R]%'
)
SELECT prod2.ProductID, prod2.Name, prod2.DaysToManufacture
FROM Production.Product prod2
INNER JOIN cte
ON cte.ProductID = prod2.ProductID;
Run Code Online (Sandbox Code Playgroud)
测试编号 1 和 2(参数化)
;WITH cte AS
(
SELECT prod1.ProductID
FROM Production.Product prod1 WITH (INDEX(AK_Product_Name))
WHERE prod1.Name LIKE N'[M-R]%'
AND EXISTS (
SELECT *
FROM Production.TransactionHistory th
WHERE th.ProductID = prod1.ProductID
)
)
SELECT prod2.ProductID, prod2.Name, prod2.DaysToManufacture
FROM Production.Product prod2
INNER JOIN cte
ON cte.ProductID = prod2.ProductID;
Run Code Online (Sandbox Code Playgroud)
测试编号 1 和 2(参数化 + 优化未知)
SELECT TOP (5) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = 977
ORDER BY th.TransactionDate DESC;
Run Code Online (Sandbox Code Playgroud)
测试编号 2(参数化两者)
SELECT TOP (5) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
;
Run Code Online (Sandbox Code Playgroud)
测试编号 2(参数化两者 + 优化未知)
SELECT TOP (5) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
OPTION (OPTIMIZE FOR (@ProductID UNKNOWN));
Run Code Online (Sandbox Code Playgroud)
测试编号 3(常数)
SELECT TOP (@RowsToReturn) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
;
Run Code Online (Sandbox Code Playgroud)
测试编号 3(参数化)
SELECT TOP (@RowsToReturn) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
OPTION (OPTIMIZE FOR (@ProductID UNKNOWN));
Run Code Online (Sandbox Code Playgroud)
测试编号 3(参数化 + 优化未知)
SELECT TOP (1) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = 977
ORDER BY th.TransactionDate DESC, th.TransactionID DESC;
Run Code Online (Sandbox Code Playgroud)
using System;
using System.Collections.Generic;
using System.Data;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
public class ObligatoryClassName
{
private class ProductInfo
{
public int ProductID;
public string Name;
public int DaysToManufacture;
public ProductInfo(int ProductID, string Name, int DaysToManufacture)
{
this.ProductID = ProductID;
this.Name = Name;
this.DaysToManufacture = DaysToManufacture;
return;
}
}
private static readonly List<ProductInfo> _GlobalProducts = new List<ProductInfo>();
private static void PopulateGlobalProducts(SqlBoolean PrintQuery)
{
if (_GlobalProducts.Count > 0)
{
if (PrintQuery.IsTrue)
{
SqlContext.Pipe.Send(String.Concat("I already haz ", _GlobalProducts.Count,
" entries :)"));
}
return;
}
SqlConnection _Connection = new SqlConnection("Context Connection = true;");
SqlCommand _Command = new SqlCommand();
_Command.CommandType = CommandType.Text;
_Command.Connection = _Connection;
_Command.CommandText = @"
;WITH cte AS
(
SELECT prod1.ProductID
FROM Production.Product prod1 WITH (INDEX(AK_Product_Name))
WHERE prod1.Name LIKE N'[M-R]%'
AND EXISTS (
SELECT *
FROM Production.TransactionHistory th
WHERE th.ProductID = prod1.ProductID
)
)
SELECT prod2.ProductID, prod2.Name, prod2.DaysToManufacture
FROM Production.Product prod2
INNER JOIN cte
ON cte.ProductID = prod2.ProductID;
";
SqlDataReader _Reader = null;
try
{
_Connection.Open();
_Reader = _Command.ExecuteReader();
while (_Reader.Read())
{
_GlobalProducts.Add(new ProductInfo(_Reader.GetInt32(0), _Reader.GetString(1),
_Reader.GetInt32(2)));
}
}
catch
{
throw;
}
finally
{
if (_Reader != null && !_Reader.IsClosed)
{
_Reader.Close();
}
if (_Connection != null && _Connection.State != ConnectionState.Closed)
{
_Connection.Close();
}
if (PrintQuery.IsTrue)
{
SqlContext.Pipe.Send(_Command.CommandText);
}
}
return;
}
[Microsoft.SqlServer.Server.SqlProcedure]
public static void GetTopRowsPerGroup(SqlByte TestNumber,
SqlByte ParameterizeProductID, SqlBoolean OptimizeForUnknown,
SqlBoolean UseSequentialAccess, SqlBoolean CacheProducts, SqlBoolean PrintQueries)
{
SqlConnection _Connection = new SqlConnection("Context Connection = true;");
SqlCommand _Command = new SqlCommand();
_Command.CommandType = CommandType.Text;
_Command.Connection = _Connection;
List<ProductInfo> _Products = null;
SqlDataReader _Reader = null;
int _RowsToGet = 5; // default value is for Test Number 1
string _OrderByTransactionID = "";
string _OptimizeForUnknown = "";
CommandBehavior _CmdBehavior = CommandBehavior.Default;
if (OptimizeForUnknown.IsTrue)
{
_OptimizeForUnknown = "OPTION (OPTIMIZE FOR (@ProductID UNKNOWN))";
}
if (UseSequentialAccess.IsTrue)
{
_CmdBehavior = CommandBehavior.SequentialAccess;
}
if (CacheProducts.IsTrue)
{
PopulateGlobalProducts(PrintQueries);
}
else
{
_Products = new List<ProductInfo>();
}
if (TestNumber.Value == 2)
{
_Command.CommandText = @"
;WITH cte AS
(
SELECT prod1.ProductID
FROM Production.Product prod1 WITH (INDEX(AK_Product_Name))
WHERE prod1.Name LIKE N'[M-R]%'
)
SELECT prod2.ProductID, prod2.Name, prod2.DaysToManufacture
FROM Production.Product prod2
INNER JOIN cte
ON cte.ProductID = prod2.ProductID;
";
}
else
{
_Command.CommandText = @"
SELECT prod1.ProductID, prod1.Name, 1 AS [DaysToManufacture]
FROM Production.Product prod1
WHERE prod1.Name LIKE N'[M-R]%';
";
if (TestNumber.Value == 3)
{
_RowsToGet = 1;
_OrderByTransactionID = ", th.TransactionID DESC";
}
}
try
{
_Connection.Open();
// Populate Product list for this run if not using the Product Cache
if (!CacheProducts.IsTrue)
{
_Reader = _Command.ExecuteReader(_CmdBehavior);
while (_Reader.Read())
{
_Products.Add(new ProductInfo(_Reader.GetInt32(0), _Reader.GetString(1),
_Reader.GetInt32(2)));
}
_Reader.Close();
if (PrintQueries.IsTrue)
{
SqlContext.Pipe.Send(_Command.CommandText);
}
}
else
{
_Products = _GlobalProducts;
}
SqlDataRecord _ResultRow = new SqlDataRecord(
new SqlMetaData[]{
new SqlMetaData("ProductID", SqlDbType.Int),
new SqlMetaData("Name", SqlDbType.NVarChar, 50),
new SqlMetaData("TransactionID", SqlDbType.Int),
new SqlMetaData("TransactionDate", SqlDbType.DateTime)
});
SqlParameter _ProductID = new SqlParameter("@ProductID", SqlDbType.Int);
_Command.Parameters.Add(_ProductID);
SqlParameter _RowsToReturn = new SqlParameter("@RowsToReturn", SqlDbType.Int);
_Command.Parameters.Add(_RowsToReturn);
SqlContext.Pipe.SendResultsStart(_ResultRow);
for (int _Row = 0; _Row < _Products.Count; _Row++)
{
// Tests 1 and 3 use previously set static values for _RowsToGet
if (TestNumber.Value == 2)
{
if (_Products[_Row].DaysToManufacture == 0)
{
continue; // no use in issuing SELECT TOP (0) query
}
_RowsToGet = (5 * _Products[_Row].DaysToManufacture);
}
_ResultRow.SetInt32(0, _Products[_Row].ProductID);
_ResultRow.SetString(1, _Products[_Row].Name);
switch (ParameterizeProductID.Value)
{
case 0x01:
_Command.CommandText = String.Format(@"
SELECT TOP ({0}) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC{2}
{1};
", _RowsToGet, _OptimizeForUnknown, _OrderByTransactionID);
_ProductID.Value = _Products[_Row].ProductID;
break;
case 0x02:
_Command.CommandText = String.Format(@"
SELECT TOP (@RowsToReturn) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
{0};
", _OptimizeForUnknown);
_ProductID.Value = _Products[_Row].ProductID;
_RowsToReturn.Value = _RowsToGet;
break;
default:
_Command.CommandText = String.Format(@"
SELECT TOP ({0}) th.TransactionID, th.TransactionDate
FROM Production.TransactionHistory th
WHERE th.ProductID = {1}
ORDER BY th.TransactionDate DESC{2};
", _RowsToGet, _Products[_Row].ProductID, _OrderByTransactionID);
break;
}
_Reader = _Command.ExecuteReader(_CmdBehavior);
while (_Reader.Read())
{
_ResultRow.SetInt32(2, _Reader.GetInt32(0));
_ResultRow.SetDateTime(3, _Reader.GetDateTime(1));
SqlContext.Pipe.SendResultsRow(_ResultRow);
}
_Reader.Close();
}
}
catch
{
throw;
}
finally
{
if (SqlContext.Pipe.IsSendingResults)
{
SqlContext.Pipe.SendResultsEnd();
}
if (_Reader != null && !_Reader.IsClosed)
{
_Reader.Close();
}
if (_Connection != null && _Connection.State != ConnectionState.Closed)
{
_Connection.Close();
}
if (PrintQueries.IsTrue)
{
SqlContext.Pipe.Send(_Command.CommandText);
}
}
}
}
Run Code Online (Sandbox Code Playgroud)
这里没有足够的空间来发布测试,所以我会找到另一个位置。
对于某些场景,SQLCLR 可用于操作在 T-SQL 中无法完成的查询的某些方面。并且可以使用内存代替临时表进行缓存,尽管这应该谨慎谨慎地进行,因为内存不会自动释放回系统。这种方法也不会有助于即席查询,尽管可以通过添加参数来定制正在执行的查询的更多方面,使其比我在这里展示的更灵活。
附加测试
我的原始测试包括支持索引TransactionHistory使用以下定义:
ProductID ASC, TransactionDate DESC
Run Code Online (Sandbox Code Playgroud)
我当时决定放弃包括TransactionId DESC在最后,认为
Mik*_*son 19
APPLY TOP或者ROW_NUMBER()?在这件事上还有什么可说的?
对差异的简短回顾并真正保持简短,我将仅显示选项 2 的计划,并且我已将索引添加到Production.TransactionHistory.
create index IX_TransactionHistoryX on
Production.TransactionHistory(ProductID, TransactionDate)
Run Code Online (Sandbox Code Playgroud)
该row_number()查询:。
with C as
(
select T.TransactionID,
T.TransactionDate,
P.DaysToManufacture,
row_number() over(partition by P.ProductID order by T.TransactionDate desc) as rn
from Production.Product as P
inner join Production.TransactionHistory as T
on P.ProductID = T.ProductID
where P.Name >= N'M' and
P.Name < N'S'
)
select C.TransactionID,
C.TransactionDate
from C
where C.rn <= 5 * C.DaysToManufacture;
Run Code Online (Sandbox Code Playgroud)

该apply top版本:
select T.TransactionID,
T.TransactionDate
from Production.Product as P
cross apply (
select top(cast(5 * P.DaysToManufacture as bigint))
T.TransactionID,
T.TransactionDate
from Production.TransactionHistory as T
where P.ProductID = T.ProductID
order by T.TransactionDate desc
) as T
where P.Name >= N'M' and
P.Name < N'S';
Run Code Online (Sandbox Code Playgroud)

它们之间的主要区别在于apply top嵌套循环下方的顶部表达式上的过滤器连接在row_number连接之后版本过滤器的位置。这意味着有Production.TransactionHistory比实际需要更多的读取。
如果只有一种方法可以在连接之前将负责枚举行的运算符推送到较低的分支,那么row_numberversion 可能会做得更好。
所以输入apply row_number()版本。
select T.TransactionID,
T.TransactionDate
from Production.Product as P
cross apply (
select T.TransactionID,
T.TransactionDate
from (
select T.TransactionID,
T.TransactionDate,
row_number() over(order by T.TransactionDate desc) as rn
from Production.TransactionHistory as T
where P.ProductID = T.ProductID
) as T
where T.rn <= cast(5 * P.DaysToManufacture as bigint)
) as T
where P.Name >= N'M' and
P.Name < N'S';
Run Code Online (Sandbox Code Playgroud)

如您所见apply row_number(),几乎相同,apply top只是稍微复杂一些。执行时间也大致相同或稍慢。
那么,我为什么要费心想出一个并不比我们已有的答案更好的答案呢?好吧,您在现实世界中还有一件事要尝试,实际上读取有所不同。一个我没有解释的*。
create index IX_TransactionHistoryX on
Production.TransactionHistory(ProductID, TransactionDate)
Run Code Online (Sandbox Code Playgroud)
当我在做的时候,我可能会抛出第二个row_number()版本,在某些情况下可能是要走的路。那些特定的情况是当您期望您实际上需要其中的大部分行时,Production.TransactionHistory因为在这里您会在Production.Product和 enumerated之间获得合并连接Production.TransactionHistory。
with C as
(
select T.TransactionID,
T.TransactionDate,
T.ProductID,
row_number() over(partition by T.ProductID order by T.TransactionDate desc) as rn
from Production.TransactionHistory as T
)
select C.TransactionID,
C.TransactionDate
from C
inner join Production.Product as P
on P.ProductID = C.ProductID
where P.Name >= N'M' and
P.Name < N'S' and
C.rn <= 5 * P.DaysToManufacture;
Run Code Online (Sandbox Code Playgroud)

要在没有排序运算符的情况下获得上述形状,您还必须将支持索引更改为按TransactionDate降序排序。
create index IX_TransactionHistoryX on
Production.TransactionHistory(ProductID, TransactionDate desc)
Run Code Online (Sandbox Code Playgroud)
*编辑:额外的逻辑读取是由于与 apply-top 一起使用的嵌套循环预取。您可以使用未记录的 TF 8744(和/或更高版本的 9115)禁用此功能以获得相同数量的逻辑读取。在适当的情况下,预取可能是 apply-top 替代方案的优势。- 保罗怀特
Kri*_*yer 12
我通常使用 CTE 和窗口函数的组合。您可以使用以下内容获得此答案:
;WITH GiveMeCounts
AS (
SELECT CustomerID
,OrderDate
,TotalAmt
,ROW_NUMBER() OVER (
PARTITION BY CustomerID ORDER BY
--You can change the following field or sort order to whatever you'd like to order by.
TotalAmt desc
) AS MySeqNum
)
SELECT CustomerID, OrderDate, TotalAmt
FROM GiveMeCounts
--Set n per group here
where MySeqNum <= 10
Run Code Online (Sandbox Code Playgroud)
对于额外的信用部分,不同的组可能想要返回不同的行数,您可以使用单独的表。假设使用地理标准,例如州:
+-------+-----------+
| State | MaxSeqnum |
+-------+-----------+
| AK | 10 |
| NY | 5 |
| NC | 23 |
+-------+-----------+
Run Code Online (Sandbox Code Playgroud)
为了在值可能不同的情况下实现这一点,您需要将您的 CTE 加入类似于此的 State 表:
SELECT [CustomerID]
,[OrderDate]
,[TotalAmt]
,[State]
FROM GiveMeCounts gmc
INNER JOIN StateTable st ON gmc.[State] = st.[State]
AND gmc.MySeqNum <= st.MaxSeqNum
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
65153 次 |
| 最近记录: |