【问题标题】:Why are logical reads for windowed aggregate functions so high?为什么窗口聚合函数的逻辑读取如此之高?
【发布时间】:2011-05-12 23:08:02
【问题描述】:

我发现在使用公共子表达式假脱机的执行计划中,报告的逻辑读取对于大型表来说非常高。

经过反复试验,我找到了一个似乎适用于下面的测试脚本和执行计划的公式。 Worktable logical reads = 1 + NumberOfRows * 2 + NumberOfGroups * 4

我不明白为什么这个公式成立。这比我认为看计划的必要性要多。任何人都可以详细说明这是怎么回事吗?

或者如果失败了,是否有任何方法可以跟踪每次逻辑读取中读取的页面,以便我自己解决?

SET STATISTICS IO OFF; SET NOCOUNT ON;

IF Object_id('tempdb..#Orders') IS NOT NULL
  DROP TABLE #Orders;

CREATE TABLE #Orders
  (
     OrderID    INT IDENTITY(1, 1) NOT NULL PRIMARY KEY CLUSTERED,
     CustomerID NCHAR(5) NULL,
     Freight    MONEY NULL,
  );

CREATE NONCLUSTERED INDEX ix
  ON #Orders (CustomerID)
  INCLUDE (Freight);

INSERT INTO #Orders
VALUES (N'ALFKI', 29.46), 
       (N'ALFKI', 61.02), 
       (N'ALFKI', 23.94), 
       (N'ANATR', 39.92), 
       (N'ANTON', 22.00);

SELECT PredictedWorktableLogicalReads = 
        1 + 2 * Count(*) + 4 * Count(DISTINCT CustomerID)
FROM   #Orders;

SET STATISTICS IO ON;

SELECT OrderID,
       Freight,
       Avg(Freight) OVER (PARTITION BY CustomerID) AS Avg_Freight
FROM   #Orders; 

输出

PredictedWorktableLogicalReads
------------------------------
23

Table 'Worktable'. Scan count 3, logical reads 23, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.
Table '#Orders___________000000000002'. Scan count 1, logical reads 2, physical reads 0, read-ahead reads 0, lob logical reads 0, lob physical reads 0, lob read-ahead reads 0.

附加信息:

Query Tuning and Optimization Book 和 this blog post by Paul White 的第 3 章对这些线轴有很好的解释。

总之,计划顶部的段迭代器向它发送的行添加一个标志,指示它何时是新分区的开始。主段假脱机一次从段迭代器获取一行并将其插入到 tempdb 中的工作表中。一旦它得到表明一个新组已经开始的标志,它就会向嵌套循环运算符的顶部输入返回一行。这会导致在工作表中的行上调用流聚合,计算平均值,然后在截断工作表之前将该值与工作表中的行连接起来,以便为新组做好准备。段假脱机发出一个虚拟行,以便处理最后一组。

据我了解,工作表是一个堆(或者在计划中将其表示为索引假脱机)。但是,当我尝试复制相同的过程时,它只需要 11 次逻辑读取。

CREATE TABLE #WorkTable
  (
     OrderID    INT,
     CustomerID NCHAR(5) NULL,
     Freight    MONEY NULL,
  )

DECLARE @Average MONEY

PRINT 'Insert 3 Rows'

INSERT INTO #WorkTable
VALUES      (1, N'ALFKI', 29.46) /*Scan count 0, logical reads 1*/

INSERT INTO #WorkTable
VALUES      (2, N'ALFKI', 61.02) /*Scan count 0, logical reads 1*/

INSERT INTO #WorkTable
VALUES      (3, N'ALFKI', 23.94) /*Scan count 0, logical reads 1*/
PRINT 'Calculate AVG'

SELECT @Average = Avg(Freight)
FROM   #WorkTable /*Scan count 1, logical reads 1*/
PRINT 'Return Rows - With the average column included'

/*This convoluted query is just to force a nested loops plan*/
SELECT *
FROM   (SELECT @Average AS Avg_Freight) T /*Scan count 1, logical reads 1*/
       OUTER APPLY #WorkTable
WHERE  COALESCE(Freight, OrderID) IS NOT NULL
       AND @Average IS NOT NULL

PRINT 'Clear out work table'

TRUNCATE TABLE #WorkTable

PRINT 'Insert 1 Row'

INSERT INTO #WorkTable
VALUES      (4, N'ANATR', 39.92) /*Scan count 0, logical reads 1*/
PRINT 'Calculate AVG'

SELECT @Average = Avg(Freight)
FROM   #WorkTable /*Scan count 1, logical reads 1*/
PRINT 'Return Rows - With the average column included'

SELECT *
FROM   (SELECT @Average AS Avg_Freight) T /*Scan count 1, logical reads 1*/
       OUTER APPLY #WorkTable
WHERE  COALESCE(Freight, OrderID) IS NOT NULL
       AND @Average IS NOT NULL

PRINT 'Clear out work table'

TRUNCATE TABLE #WorkTable

PRINT 'Insert 1 Row'

INSERT INTO #WorkTable
VALUES      (5, N'ANTON', 22.00) /*Scan count 0, logical reads 1*/
PRINT 'Calculate AVG'

SELECT @Average = Avg(Freight)
FROM   #WorkTable /*Scan count 1, logical reads 1*/
PRINT 'Return Rows - With the average column included'

SELECT *
FROM   (SELECT @Average AS Avg_Freight) T /*Scan count 1, logical reads 1*/
       OUTER APPLY #WorkTable
WHERE  COALESCE(Freight, OrderID) IS NOT NULL
       AND @Average IS NOT NULL

PRINT 'Clear out work table'

TRUNCATE TABLE #WorkTable

PRINT 'Calculate AVG'

SELECT @Average = Avg(Freight)
FROM   #WorkTable /*Scan count 1, logical reads 0*/
PRINT 'Return Rows - With the average column included'

SELECT *
FROM   (SELECT @Average AS Avg_Freight) T
       OUTER APPLY #WorkTable
WHERE  COALESCE(Freight, OrderID) IS NOT NULL
       AND @Average IS NOT NULL

DROP TABLE #WorkTable 

【问题讨论】:

  • 我们为临时表创建索引时有什么性能差异吗?

标签: sql sql-server sql-server-2008


【解决方案1】:

工作表的逻辑读取计数不同:每个 读取有一个“逻辑读取”。这并不意味着工作台比“真正的”线轴工作台效率低(恰恰相反);逻辑读取只是在不同的单元中。

我相信我的想法是计算工作表逻辑读取的散列页面不会很有用,因为这些结构是服务器内部的。报告逻辑读取计数器中假脱机的行使该数字对于分析目的更有意义。

这种洞察力应该使您的公式起作用的原因变得清晰。两个辅助线轴被完全读取两次 (2 * COUNT(*)),主线轴发出 (组值数 + 1) 行,如我的博客条目中所述,给出 (COUNT(DISTINCT CustomerID) + 1) 组件.加一用于主线轴发出的额外行,以指示最后一组已结束。

保罗

【讨论】:

  • 啊,这确实解释了一些事情。非常感谢您花时间回答这个问题,因为这让我困惑了几个月!
  • @MartinSmith & SQLkiwi - 这里有足够的衍生问题吗(如果你认为有足够的肉,我会发布一个) - 你如何比较两个等效查询以提高效率,其中一个使用窗口函数一个没有?探查器和执行计划中的读取/写入/cpu 是我的目标,但这会使事情复杂化。
【解决方案2】:

在公式中,您给出的 NumberOfRows * 2 将成立,因为执行图中的 Sort 函数和 Stream Aggregate 显示都需要所有行来完成处理。您能否确认添加“where”子句时逻辑读取的减少:

  1. 运费价值
  2. 客户 ID

【讨论】:

  • 您是否看过此查询的任何物理读数或在您开发公式的过程中?您是否能够使用 DBCC DROPCLEANBUFFERS 并再次捕获物理和逻辑读取?您可以在查询中添加 WHERE 子句并重新运行吗?谢谢
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多