每组检索n行


88

我经常需要从结果集中的每个组中选择一些行。

例如,我可能想列出每个客户的'n'个最高或最低最近订单值。

在更复杂的情况下,每个组要列出的行数可能有所不同(由分组/父记录的属性定义)。这部分绝对是可选的/为了获得额外的荣誉,并不旨在阻止人们回答。

解决SQL Server 2005及更高版本中此类问题的主要选项是什么?每种方法的主要优点和缺点是什么?

AdventureWorks示例(为清楚起见,可选)

  1. TransactionHistory对于每个以M到R(含)开头的字母的产品,从表中列出五个最近的交易日期和ID 。
  2. 再次相同,但是n每个产品都有历史记录行,其中nDaysToManufacture产品属性的五倍。
  3. 相同,在特殊情况下,每个产品只需要一个历史记录行(单个最近的条目,由TransactionDate,打破平局)TransactionID

Answers:


70

让我们从基本场景开始。

如果要从表中获取一些行,则有两个主要选项:排序函数;或TOP

首先,让我们考虑Production.TransactionHistory特定的整个集合ProductID

SELECT h.TransactionID, h.ProductID, h.TransactionDate
FROM Production.TransactionHistory h
WHERE h.ProductID = 800;

这将返回418行,并且该计划显示,它检查表中的每一行以查找此行-不受限制的聚集索引扫描,并带有谓词以提供过滤器。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;

两种方案-基本TOP \ RowNum

您会注意到,TOP在查询和计划中,第二个查询()比第一个查询简单得多。但非常重要的是,它们都TOP用来限制实际从索引中拉出的行数。成本只是估算值,值得忽略,但是您会发现这两个计划有很多相似之处,该ROW_NUMBER()版本做了少量的额外工作来分配数字并进行相应的过滤,并且两个查询最终只进行了两次读取即可他们的工作。查询优化器肯定认识到对ROW_NUMBER()字段进行过滤的想法,意识到它可以使用Top运算符来忽略不需要的行。这两个查询都足够好- TOP并没有很多值得改进的代码,但对于初学者来说更简单,也可能更清晰。

因此,这项工作适用于单个产品。但是,我们需要考虑如果需要跨多个产品执行此操作,会发生什么情况。

迭代的程序员将考虑遍历感兴趣的产品并多次调用此查询的想法,并且实际上可以避免编写这种形式的查询-不使用游标,而是使用APPLY。我正在使用OUTER APPLY,弄清楚如果没有针对它的交易,我们可能想返回NULL的Product。

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';

此计划是迭代程序员的方法-嵌套循环,对每个产品执行Top操作并进行Seek(之前有2次读取)。这将对Product进行4次读取,对TransactionHistory进行360次读取。

申请计划

使用ROW_NUMBER()的方法是PARTITION BYOVER子句中使用,以便我们为每个产品重新开始编号。然后可以像以前一样对其进行过滤。该计划最终大为不同。逻辑读取在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;

ROW_NUMBER个计划

值得注意的是,该计划具有昂贵的Sort运算符。合并联接似乎未维护TransactionHistory中的行顺序,必须使用数据才能找到行号。读取次数更少,但是这种阻塞的排序可能会让人感到痛苦。使用APPLY,嵌套循环将在几次读取后非常快速地返回第一行,但是使用Sort,ROW_NUMBER()将仅在大部分工作完成后才返回行。

有趣的是,如果ROW_NUMBER()查询使用INNER JOIN而不是LEFT JOIN,那么将提出不同的计划。

ROW_NUMBER(),INNER JOIN

与一样,该计划使用嵌套循环APPLY。但是没有Top运算符,因此它提取每种产品的所有交易记录,并使用比以前更多的读取-492对TransactionHistory进行读取。没有充分的理由不在此处选择“合并联接”选项,因此我认为该计划被认为是“足够好”。仍然-它不会阻止,这很好-只是不如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;

但是Join运算符说的是“ Left Outer Join”,而不是“ Inner Join”。相对于TransactionHistory表,读取次数仍不到500次读取。

在p.ProductID上代替PARTITION BY,而在h.ProductID上

无论如何-回到手头的问题...

我们已经回答了问题1,您可以选择两个选项。就个人而言,我喜欢这种APPLY选择。

为了将其扩展为使用可变数字(问题2),5正义需要相应地进行更改。哦,我添加了另一个索引,因此Production.Product.NameDaysToManufacture列上有一个索引。

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';

而且这两个计划几乎都和以前一样!

可变行

同样,忽略估算的成本-但我仍然喜欢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';

这里的计划看起来更简单-它不是阻塞性的,但是有一个潜在的危险。

加入外部CTE

注意,Compute Scalar正在从Product表中提取数据。这正在计算5 * p.DaysToManufacture价值。此值未传递到从TransactionHistory表中提取数据的分支中,而是在合并联接中使用。作为残留物。

偷偷摸摸的残留物!

因此,合并联接正在消耗所有行,不仅是第一行,而且是很多行,而是所有行,然后进行剩余检查。随着交易数量的增加,这很危险。我不喜欢这种情况-合并联接中的残留谓词可以快速升级。我更喜欢这种APPLY/TOP情况的另一个原因。

在特殊情况下,它恰好是一行,对于问题3,我们显然可以使用相同的查询,但用1代替5。但是,我们还有一个额外的选择,那就是使用常规聚合。

SELECT ProductID, MAX(TransactionDate)
FROM Production.TransactionHistory
GROUP BY ProductID;

像这样的查询将是一个有用的开始,我们可以轻松地对其进行修改,以拉出TransactionID,以达到平局的目的(使用随后将被分解的连接),但是我们要么查看整个索引,要么我们会逐个产品地研究产品,在这种情况下,我们之前的产品并没有真正的大进步。

但我应该指出,我们在这里正在研究一种特殊的情况。对于真实数据,以及可能不理想的索引策略,里程可能会有很大差异。尽管我们已经看到了这APPLY一点,但在某些情况下它可能会变慢。不过,它很少会阻塞,因为它倾向于使用嵌套循环,许多人(包括我自己在内)都觉得它很吸引人。

我没有在这里尝试探索并行性,也没有非常努力地研究问题3,由于连接和拆分的复杂性,我认为这是人们很少想要的特例。这里主要要考虑的是这两个选项都很强大。

我更喜欢APPLY。很明显,它很好地使用了Top运算符,并且很少引起阻塞。


44

在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;

您也可以使用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;

在指定了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;

再一次,使用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;

请注意,根据数据分布和支持索引的可用性,这些操作的性能会有所不同,因此优化性能和获得最佳计划的确取决于本地因素。

就我个人而言,我更喜欢CTE和窗口解决方案,而不是CROSS APPLY/,TOP因为它们更好地分离了逻辑,并且对我而言更直观。总的来说(在这种情况下和我的一般经验),CTE方法都会产生更有效的计划(下面的示例),但这不应被视为普遍真理-您应始终测试您的方案,尤其是在索引已更改或数据严重偏斜。


AdventureWorks示例-无需任何更改

  1. TransactionHistory对于每个以M到R(含)开头的字母的产品,从表中列出五个最近的交易日期和ID 。
-- 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';

这两个运行时指标的比较:

在此处输入图片说明

CTE / OVER()方案:

在此处输入图片说明

CROSS APPLY 计划:

在此处输入图片说明

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

在此处输入图片说明

在此处输入图片说明

  1. 再次相同,但是n每个产品都有历史记录行,其中nDaysToManufacture产品属性的五倍。

这里需要很小的改动。对于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';

运行时结果:

在此处输入图片说明

并行CTE / OVER()计划:

在此处输入图片说明

单线程CTE / OVER()计划:

在此处输入图片说明

CROSS APPLY 计划:

在此处输入图片说明

  1. 相同,在特殊情况下,每个产品只需要一个历史记录行(单个最近的条目,由TransactionDate,打破平局)TransactionID

同样,这里有微小的变化。在CTE解决方案中,我们添加TransactionIDOVER()子句,并将外部过滤器更改为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';

运行时结果:

在此处输入图片说明

并行CTE / OVER()计划:

在此处输入图片说明

单线程CTE / OVER()计划:

在此处输入图片说明

CROSS APPLY 计划:

在此处输入图片说明

窗口函数并不总是最好的选择(请参见参考资料COUNT(*) OVER()),它们也不是解决每组n行问题的仅有两种方法,但是在这种特定情况下-给定架构,现有索引和数据分布-通过所有有意义的说明,CTE的表现都更好。


AdventureWorks示例-灵活添加索引

但是,如果添加支持索引,则类似于保罗在评论中提到的索引,但第2列和第3列排序DESC

CREATE UNIQUE NONCLUSTERED INDEX UQ3 ON Production.TransactionHistory 
  (ProductID, TransactionDate DESC, TransactionID DESC);

实际上,您会得到更有利的计划,并且CROSS APPLY在所有三种情况下,指标都会翻转以支持该方法:

在此处输入图片说明

如果这是我的生产环境,那么我可能会对这种情况下的持续时间感到满意,并且不会为进一步优化而费心。


在不支持APPLYOVER()子句的SQL Server 2000中,这一切都非常难看。


24

在像MySQL这样的DBMS中,它没有窗口功能或CROSS APPLY,执行此操作的方法是使用标准SQL(89)。较慢的方法是使用聚合的三角形交叉连接。更快的方法(但仍然可能不及使用交叉应用或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 ;

对于每个组的自定义顶部行的额外问题:

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 ;

注意:在MySQL中,AND o.OrderID IN (SELECT TOP(@top) oi.OrderID ...)将使用代替一个AND o.OrderDate >= (SELECT oi.OrderDate ... LIMIT 1 OFFSET (@top - 1))。SQL Server FETCH / OFFSET在2012版本中添加了语法。此处的查询已调整为IN (TOP...)可与早期版本一起使用。


21

我采用了一种略有不同的方法,主要是看该技术与其他技术的比较,因为选择不错,对吗?

测试

我们为什么不首先看各种方法如何相互对立。我做了三组测试:

  1. 第一组运行时没有数据库修改
  2. 第二组是在创建索引以TransactionDate针对的基于支持的查询之后运行的Production.TransactionHistory
  3. 第三组假设略有不同。由于所有三个测试都针对相同的产品列表运行,因此如果我们缓存该列表怎么办?我的方法使用内存缓存,而其他方法使用等效的临时表。为第二组测试创建的支持索引对于该组测试仍然存在。

其他测试详细信息:

  • 这些测试是AdventureWorks2012在SQL Server 2012 SP2(开发人员版)上运行的。
  • 对于每个测试,我都标记了我从谁那里得到的答案以及该查询是哪个特定查询。
  • 我使用了“查询选项” |“执行后丢弃结果”选项 结果。
  • 请注意,对于前两组测试,RowCounts我的方法显示为“关闭”。这是由于我的方法是手动执行操作CROSS APPLY:它针对进行初始查询Production.Product并返回161行,然后将其用于进行查询Production.TransactionHistory。因此,RowCount我的条目的值始终比其他条目多161。在第三组测试(带缓存)中,所有方法的行数均相同。
  • 我使用SQL Server Profiler来捕获统计信息,而不是依赖执行计划。亚伦(Aaron)和米凯尔(Mikael)在展示他们的查询计划方面已经做得非常出色,并且无需复制该信息。我的方法的目的是将查询简化为一个简单的形式,以至于实际上并不重要。使用Profiler的另一个原因是,稍后将提到。
  • Name >= N'M' AND Name < N'S'我选择使用Name LIKE N'[M-R]%',而不是使用构造,而SQL Server对待它们的方式相同。

结果

没有支持指数

这本质上是开箱即用的AdventureWorks2012。在所有情况下,我的方法显然都比其他方法更好,但从来没有比前一种或前两种方法更好。

测试1 测试1结果-无索引
亚伦的CTE显然是赢家。

测试2 测试2结果-无索引
Aaron的CTE(再次)和Mikael的第二种apply row_number()方法紧随其后。

测试3 测试3结果-无索引
亚伦的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);

一旦建立了该索引,结果就会发生很大变化。

测试1 测试1结果-带有支持指数
这次至少在逻辑读取方面是我的方法。该CROSS APPLY方法以前在测试1中表现最差,在持续时间方面胜出,甚至在逻辑读取方面胜过CTE方法。

测试2 测试2结果-带有支持指数
这次,这是Mikael的第apply row_number()一种方法,它是看Reads的赢家,而以前它是表现最差的一种。现在,在查看Reads时,我的方法排在第二位。实际上,在CTE方法之外,其余所有在读取方面都相当接近。

测试3 测试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);

测试1 测试1结果-支持索引和缓存
所有方法似乎都从缓存中同样受益,而我的方法仍然遥遥领先。

测试2 测试2结果-支持索引和缓存
在这里,我们现在看到了阵容上的差异,因为我的方法勉强领先,仅比Mikael的第apply row_number()一种方法好2读,而没有缓存,我的方法落后4读。

测试3 测试3结果-支持索引和缓存
请参阅底部的更新(在行下方)。在这里,我们再次看到了一些区别。与Aaron的CROSS APPLY方法相比,我的方法的“参数化”风格现在几乎没有2次读取领先(没有缓存,它们是相等的)。但真正奇怪的是,我们第一次看到一种受缓存负面影响的方法:Aaron的CTE方法(以前是测试3的最佳方法)。但是,我不会在不适当的地方使用它,并且由于没有缓存,Aaron的CTE方法仍然比我的缓存方法要快,因此针对这种特殊情况的最佳方法似乎是Aaron的CTE方法。

结论 请参见底部的更新(在此行下方)
,重复使用辅助查询结果的情况通常(但不总是)受益于缓存这些结果。但是当缓存是一个好处时,使用内存进行缓存比使用临时表具有一些优势。

方法

通常

我从“详细”查询(即获取s和s)中分离出“标头”查询(即,获取ProductIDs,在某些情况下还DaysToManufacture基于Name以某些字母开头的)。其概念是执行非常简单的查询,并且不允许优化器在加入查询时感到困惑。显然,这并不总是有利的,因为它也不允许优化器进行优化。但是正如我们在结果中看到的那样,根据查询的类型,此方法确实有其优点。TransactionIDTransactionDate

此方法的各种风味之间的区别是:

  • 常量:提交任何可替换的值作为内联常量而不是参数。这将ProductID在所有三个测试中以及在测试2中返回的行数中进行引用,因为这是“乘以DaysToManufacture乘积属性的五倍”的函数。此子方法意味着每个人都有ProductID自己的执行计划,如果的数据分布差异很大,这将是有益的ProductID。但是,如果数据分布几乎没有变化,那么生成额外计划的成本可能就不值得了。

  • 参数化:至少提交ProductID@ProductID,以允许执行计划缓存和重用。还有一个额外的测试选项,也可以将要返回测试2的可变行数作为参数。

  • 优化未知:当引用ProductID为时@ProductID,如果数据分布差异很大,则可以缓存对其他ProductID值有负面影响的计划,因此最好知道使用此查询提示是否有帮助。

  • 缓存产品:与其Production.Product每次都查询表,不如要获取完全相同的列表,而是运行一次查询(在我们查询时,过滤掉ProductID甚至不在TransactionHistory表中的所有内容,因此我们不会浪费任何内容)资源)并缓存该列表。该列表应包含该DaysToManufacture字段。使用此选项,第一次执行时,逻辑读取的初始命中率会略高,但此后仅TransactionHistory查询表。

特别

好的,但是,嗯,怎么可能不使用CURSOR并将每个结果集转储到临时表或表变量中而将所有子查询作为单独的查询发布?显然,执行CURSOR / Temp Table方法将在读取和写入中反映出非常明显的效果。好吧,通过使用SQLCLR :)。通过创建SQLCLR存储过程,我能够打开一个结果集,并从本质上将每个子查询的结果作为连续结果集(而不是多个结果集)流式传输到该结果集。的产品信息外(即ProductIDNameDaysToManufacture),则无需将子查询结果存储在任何位置(内存或磁盘),只需将其作为SQLCLR存储过程的主要结果集传递即可。这使我可以执行一个简单的查询来获取产品信息,然后循环浏览它,针对发出非常简单的查询TransactionHistory

并且,这就是为什么我必须使用SQL Server Profiler来捕获统计信息的原因。通过设置“包括实际执行计划”查询选项,或通过发出,SQLCLR存储过程未返回执行计划SET STATISTICS XML ON;

对于产品信息缓存,我使用了readonly static通用列表(即_GlobalProducts下面的代码)。似乎添加到集合中不会违反该readonly选项,因此,即使程序集具有反常性,此代码也可以在程序集具有PERMISSON_SETof SAFE:) 时起作用。

生成的查询

此SQLCLR存储过程产生的查询如下:

产品资讯

测试编号1和3(无缓存)

SELECT prod1.ProductID, prod1.Name, 1 AS [DaysToManufacture]
FROM   Production.Product prod1
WHERE  prod1.Name LIKE N'[M-R]%';

测试编号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;

测试编号1、2和3(缓存)

;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;

交易信息

测试编号1和2(常数)

SELECT TOP (5) th.TransactionID, th.TransactionDate
FROM   Production.TransactionHistory th
WHERE  th.ProductID = 977
ORDER BY th.TransactionDate DESC;

测试编号1和2(参数化)

SELECT TOP (5) th.TransactionID, th.TransactionDate
FROM   Production.TransactionHistory th
WHERE  th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
;

测试编号1和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));

测试编号2(均已参数化)

SELECT TOP (@RowsToReturn) th.TransactionID, th.TransactionDate
FROM   Production.TransactionHistory th
WHERE  th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC
;

测试编号2(同时参数化+未知优化)

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));

测试编号3(常数)

SELECT TOP (1) th.TransactionID, th.TransactionDate
FROM   Production.TransactionHistory th
WHERE  th.ProductID = 977
ORDER BY th.TransactionDate DESC, th.TransactionID DESC;

测试编号3(参数化)

SELECT TOP (1) th.TransactionID, th.TransactionDate
FROM   Production.TransactionHistory th
WHERE  th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC, th.TransactionID DESC
;

测试编号3(参数化+优化未知)

SELECT TOP (1) th.TransactionID, th.TransactionDate
FROM   Production.TransactionHistory th
WHERE  th.ProductID = @ProductID
ORDER BY th.TransactionDate DESC, th.TransactionID DESC
OPTION (OPTIMIZE FOR (@ProductID UNKNOWN));

编码

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);
            }
        }


    }
}

测试查询

没有足够的空间在这里发布测试,因此我将找到另一个位置。

结论

对于某些情况,SQLCLR可用于处理T-SQL中无法完成的查询的某些方面。并且可以使用内存而不是临时表来进行缓存,但是应该谨慎谨慎地进行,因为内存不会自动释放回系统。尽管有可能通过添加参数来定制正在执行的查询的更多方面,使此方法比我在此显示的更加灵活,但它也不会对临时查询有所帮助。


更新

其他测试
我的原始测试(包括支持索引)TransactionHistory使用以下定义:

ProductID ASC, TransactionDate DESC

我当时决定放弃,包括TransactionId DESC最后,认为这可能对测试3有所帮助(它指定了最近的抢七游戏- TransactionId嗯,假设“最新”是因为没有明确说明,但每个人似乎同意这个假设),可能没有足够的联系来发挥作用。

但是,随后亚伦用确实包含的支持指数进行了重新测试,TransactionId DESC发现该CROSS APPLY方法在所有三个测试中均胜出。这与我的测试不同,后者表明CTE方法最适合测试编号3(不使用缓存时,反映了Aaron的测试)。显然,还有其他变化需要测试。

我删除了当前的支持索引,使用创建了一个新索引,TransactionId并清除了计划缓存(请确保):

DROP INDEX [IX_TransactionHistoryX] ON Production.TransactionHistory;

CREATE UNIQUE INDEX [UIX_TransactionHistoryX]
    ON Production.TransactionHistory (ProductID ASC, TransactionDate DESC, TransactionID DESC)
    WITH (FILLFACTOR = 100);

DBCC FREEPROCCACHE WITH NO_INFOMSGS;

我重新运行了1号测试,结果与预期的一样。然后,我重新运行了测试3,结果确实发生了变化:

测试3个结果-带有支持索引(带有TransactionId DESC)
以上结果适用于标准的非缓存测试。这次,不仅CROSS APPLY击败了CTE(正如Aaron的测试所示),而且SQLCLR proc领先30 Reads(woo hoo)。

测试3个结果-带有支持索引(带有TransactionId DESC)和缓存
以上结果用于启用缓存的测试。这次CTE的性能没有下降,尽管CROSS APPLY仍然胜过它。但是,现在SQLCLR进程以23次读取领先(再次呼呼)。

拿走

  1. 有多种选择。最好尝试几种,因为它们各有所长。此处进行的测试显示,在所有测试中,表现最佳和表现最差的阅读器和持续时间之间的差异都很小(带有支持指数);读取的变化约为350,持续时间为55 ms。尽管SQLCLR proc确实在1次测试中均获胜(就读取而言),但仅保存少量读取通常不值得使用SQLCLR路由的维护成本。但是在AdventureWorks2012中,该Product表只有504行,TransactionHistory只有113,443行。随着行数的增加,这些方法之间的性能差异可能会变得更加明显。

  2. 尽管此问题特定于获取一组特定的行,但不应忽视的是,性能中的最大因素是索引而不是特定的SQL。在确定哪种方法真正最佳之前,需要有一个好的索引。

  3. 在这里找到的最重要的一课不是关于CROSS vs CTE和SQLCLR,而是关于TESTING。不要假设 从几个人那里获得想法,并尽可能多地测试场景。


2
请参阅我对Mikael答案的编辑,以了解与apply相关的额外逻辑读取的原因。
保罗·怀特

18

APPLY TOP还是ROW_NUMBER()?关于此事,可能还有什么要说的?

简要回顾一下差异,并实际上保持简短,我只显示选项2的计划,并在上添加了索引Production.TransactionHistory

create index IX_TransactionHistoryX on 
  Production.TransactionHistory(ProductID, TransactionDate)

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;

在此处输入图片说明

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';

在此处输入图片说明

它们之间的主要区别在于apply top,嵌套循环下面的顶部表达式上的row_number过滤器在连接之后的版本过滤器中连接。这意味着从中读取的内容Production.TransactionHistory比实际需要的更多。

如果只有一种方法可以在联接之前将负责枚举行的运算符向下推到较低的分支,则row_number版本可能会更好。

因此输入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';

在此处输入图片说明

如您所见,apply row_number()它几乎apply top只是稍微复杂一些而已。执行时间大约相同或稍慢。

那么,为什么我要花一个比我们现有的答案更好的答案呢?好吧,您还可以在现实世界中尝试一件事,但读取结果实际上有所不同。我没有*的解释。

APPLY - ROW_NUMBER
(961 row(s) affected)
Table 'TransactionHistory'. Scan count 115, logical reads 230, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Product'. Scan count 1, logical reads 15, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

APPLY - TOP
(961 row(s) affected)
Table 'TransactionHistory'. Scan count 115, logical reads 268, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table 'Product'. Scan count 1, logical reads 15, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

当我在处理它时,我不妨抛出第二种row_number()版本,在某些情况下可能是要走的路。在某些情况下,可能是您期望实际需要其中的大多数行的Production.TransactionHistory原因,因为在这里您得到了Production.Product和枚举之间的合并联接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;

在此处输入图片说明

要获得没有排序运算符的上述形状,您还必须将支持索引更改为TransactionDate降序排列。

create index IX_TransactionHistoryX on 
  Production.TransactionHistory(ProductID, TransactionDate desc)

*编辑:额外的逻辑读取是由于apply-top所使用的嵌套循环预取。您可以使用未记录的TF 8744(和/或更高版本的9115)禁用此功能,以获取相同数量的逻辑读取。在正确的情况下,预取可能是应用顶部替代方法的一个优点。-保罗·怀特


11

我通常结合使用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

对于额外的贷项部分,其中不同的组可能希望返回不同的行数,则可以使用单独的表。可以说使用地理标准(例如州):

+-------+-----------+
| State | MaxSeqnum |
+-------+-----------+
| AK    |        10 |
| NY    |         5 |
| NC    |        23 |
+-------+-----------+

为了在值可能不同的情况下实现此目的,您需要将CTE加入到State表,如下所示:

SELECT [CustomerID]
    ,[OrderDate]
    ,[TotalAmt]
    ,[State]
FROM GiveMeCounts gmc
INNER JOIN StateTable st ON gmc.[State] = st.[State]
    AND gmc.MySeqNum <= st.MaxSeqNum
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.