并行计划中的“实际”行数不准确

Mar*_*ith 17 sql-server

这是一个纯粹的学术问题,以至于它不会引起问题,我只是想听听对这种行为的任何解释。

以标准问题 Itzik Ben-Gan 交叉连接 CTE 计数表为例:

USE [master]
GO

SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO

CREATE FUNCTION [dbo].[TallyTable] 
(   
    @N INT
)
RETURNS TABLE WITH SCHEMABINDING AS
RETURN 
(
    WITH 
    E1(N) AS 
    (
        SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL 
        SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL 
        SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
    )                                       -- 1*10^1 or 10 rows
    , E2(N) AS (SELECT 1 FROM E1 a, E1 b)   -- 1*10^2 or 100 rows
    , E4(N) AS (SELECT 1 FROM E2 a, E2 b)   -- 1*10^4 or 10,000 rows
    , E8(N) AS (SELECT 1 FROM E4 a, E4 b)   -- 1*10^8 or 100,000,000 rows

    SELECT TOP (@N) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) AS N FROM E8 
)
GO
Run Code Online (Sandbox Code Playgroud)

发出将创建 100 万行号表的查询:

SELECT
    COUNT(N)
FROM
    dbo.TallyTable(1000000) tt
Run Code Online (Sandbox Code Playgroud)

看看这个查询的并行执行计划:

并行执行计划

请注意,收集流运算符之前的“实际”行数为 1,004,588。在收集流操作符之后,行数是预期的 1,000,000。更奇怪的是,该值并不一致,并且会因运行而异。COUNT 的结果总是正确的。

再次发出查询,强制非并行计划:

SELECT
    COUNT(N)
FROM
    dbo.TallyTable(1000000) tt
OPTION (MAXDOP 1)
Run Code Online (Sandbox Code Playgroud)

这次所有运算符都显示正确的“实际”行数。

非并行执行计划

到目前为止,我已经在 2005SP3 和 2008R2 上尝试过这个,两者的结果相同。关于可能导致这种情况的任何想法?

Pau*_*ite 12

行在内部从生产者线程以数据包的形式传递到消费者线程(因此是 CXPACKET - 类交换数据包),而不是一次一行。交易所内部有一定的缓冲。此外,从 Gather Streams 的消费者端关闭管道的调用必须在控制数据包中传递回生产者线程。调度和其他内部考虑意味着并行计划总是有一定的“停止距离”。

因此,您经常会看到这种行数差异,其中实际需要的行数少于子树的整个潜在行集。在这种情况下,TOP 使执行“提前结束”。

更多信息:


Mar*_*ith 10

我想我可能对此有部分解释,但请随时将其驳回或发布任何替代方案。@MartinSmith 通过在执行计划中突出显示 TOP 的效果,肯定会有所作为。

简而言之,“实际行数”不是运算符处理的行数,而是运算符的 GetNext() 方法被调用的次数。

取自BOL

物理操作符初始化、收集数据和关闭。具体来说,物理操作员可以回答以下三个方法调用:

  • Init():Init() 方法使物理运算符初始化自身并设置任何所需的数据结构。物理操作员可能会收到许多 Init() 调用,但通常物理操作员只会收到一个。
  • GetNext():GetNext() 方法使物理运算符获取第一行或后续数据行。物理操作员可能会收到零个或多个 GetNext() 调用。
  • Close():Close() 方法使物理操作符执行一些清理操作并自行关闭。物理操作员只收到一个 Close() 调用。

GetNext() 方法返回一行数据,它被调用的次数在使用 SET STATISTICS PROFILE ON 或 SET STATISTICS XML ON 生成的 Showplan 输出中显示为 ActualRows。

为了完整起见,了解一些并行运算符的背景知识很有用。工作通过重新分区流或分发流操作符以并行计划分发到多个流。它们使用以下四种机制之一在线程之间分配行或页面:

  • Hash根据行中列的散列分布行
  • 循环通过循环遍历线程列表来分配行
  • 广播将所有页面或行分发给所有线程
  • 需求分区仅用于扫描。线程启动,向操作员请求一页数据,处理它并在完成后请求另一页。

第一个分布式流操作符(在计划中的最右边)对源自恒定扫描的行使用需求分区。有三个线程调用 GetNext() 6、4 和 0 次,总共 10 个“实际行”:

<RunTimeInformation>
       <RunTimeCountersPerThread Thread="2" ActualRows="6" ActualEndOfScans="1" ActualExecutions="1" />
       <RunTimeCountersPerThread Thread="1" ActualRows="4" ActualEndOfScans="1" ActualExecutions="1" />
       <RunTimeCountersPerThread Thread="0" ActualRows="0" ActualEndOfScans="0" ActualExecutions="0" />
 </RunTimeInformation>
Run Code Online (Sandbox Code Playgroud)

在下一个分布操作符中,我们再次拥有三个线程,这次分别对 GetNext() 进行了 50、50 和 0 次调用,总共 100 次:

<RunTimeInformation>
    <RunTimeCountersPerThread Thread="2" ActualRows="50" ActualEndOfScans="1" ActualExecutions="1" />
    <RunTimeCountersPerThread Thread="1" ActualRows="50" ActualEndOfScans="1" ActualExecutions="1" />
    <RunTimeCountersPerThread Thread="0" ActualRows="0" ActualEndOfScans="0" ActualExecutions="0" />
</RunTimeInformation>
Run Code Online (Sandbox Code Playgroud)

原因和解释可能出现在下一个并行运算符处。

<RunTimeInformation>
    <RunTimeCountersPerThread Thread="2" ActualRows="1" ActualEndOfScans="0" ActualExecutions="1" />
    <RunTimeCountersPerThread Thread="1" ActualRows="10" ActualEndOfScans="0" ActualExecutions="1" />
    <RunTimeCountersPerThread Thread="0" ActualRows="0" ActualEndOfScans="0" ActualExecutions="0" />
</RunTimeInformation>
Run Code Online (Sandbox Code Playgroud)

所以我们现在有 11 次对 GetNext() 的调用,我们期望看到 10 次。

编辑:2011-11-13

在这一点上卡住了,我和聚集索引中的小伙子一起兜售答案,@MikeWalsh在这里指导@SQLKiwi 。


Mar*_*ith 7

1,004,588 这个数字在我的测试中也经常出现。

我也看到了下面稍微简单的计划。

WITH 
E1(N) AS 
(
    SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL 
    SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL 
    SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
)                                       -- 1*10^1 or 10 rows
, E2(N) AS (SELECT 1 FROM E1 a, E1 b)   -- 1*10^2 or 100 rows
, E4(N) AS (SELECT 1 FROM E2 a, E2 b)   -- 1*10^4 or 10,000 rows
SELECT * INTO #E4 FROM E4;

WITH E8(N) AS (SELECT 1 FROM #E4 a, #E4 b),
Nums(N) AS (SELECT  TOP (1000000) ROW_NUMBER() OVER (ORDER BY (SELECT 0)) FROM E8 )
SELECT COUNT(N) FROM Nums

DROP TABLE #E4
Run Code Online (Sandbox Code Playgroud)

计划

执行计划中其他感兴趣的数字是

+----------------------------------+--------------+--------------+-----------------+
|                                  | Table Scan A | Table Scan B | Row Count Spool |
+----------------------------------+--------------+--------------+-----------------+
| Number Of Executions             | 2            |            2 |             101 |
| Actual Number Of Rows - Total    | 101          |        20000 |         1004588 |
| Actual Number Of Rows - Thread 0 | -            |              |                 |
| Actual Number Of Rows - Thread 1 | 95           |        10000 |          945253 |
| Actual Number Of Rows - Thread 2 | 6            |        10000 |           59335 |
| Actual Rebinds                   | 0            |            0 |               2 |
| Actual Rewinds                   | 0            |            0 |              99 |
+----------------------------------+--------------+--------------+-----------------+
Run Code Online (Sandbox Code Playgroud)

我的猜测只是因为任务是并行处理的,一个任务在飞行中处理行,而另一个任务将第百万行传递给收集流操作符,因此正在处理额外的行。此外,从本文中,行被缓冲并分批传送到此迭代器,因此TOP在任何情况下,正在处理的行数似乎很可能会超过而不是完全达到规范。

编辑

只是更详细地看一下这个。我注意到我得到的变化不仅仅是1,004,588上面引用的行数,所以在循环中运行上面的查询 1,000 次迭代并捕获实际的执行计划。丢弃并行度为零的 81 个结果,得出以下数字。

count       Table Scan A: Total Actual Row Spool - Total Actual Rows
----------- ------------------------------ ------------------------------
352         101                            1004588
323         102                            1004588
72          101                            1003565
37          101                            1002542
35          102                            1003565
29          101                            1001519
18          101                            1000496
13          102                            1002542
5           9964                           99634323
5           102                            1001519
4           9963                           99628185
3           10000                          100000000
3           9965                           99642507
2           9964                           99633300
2           9966                           99658875
2           9965                           99641484
1           9984                           99837989
1           102                            1000496
1           9964                           99637392
1           9968                           99671151
1           9966                           99656829
1           9972                           99714117
1           9963                           99629208
1           9985                           99847196
1           9967                           99665013
1           9965                           99644553
1           9963                           99623626
1           9965                           99647622
1           9966                           99654783
1           9963                           99625116
Run Code Online (Sandbox Code Playgroud)

可以看出,1,004,588 是迄今为止最常见的结果,但有 3 次发生了最坏的情况,处理了 100,000,000 行。观察到的最佳情况是 1,000,496 行计数,发生了 19 次。

要重现的完整脚本位于此答案修订版 2 的底部(如果在具有 2 个以上处理器的系统上运行,则需要进行调整)。