平行计划中不正确的“实际”行计数


17

这是一个纯粹的学术问题,在很大程度上并没有引起问题,我只是想听听对此行为的任何解释。

取得标准的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

发出查询,该查询将创建一百万个行号表:

SELECT
    COUNT(N)
FROM
    dbo.TallyTable(1000000) tt

看一下此查询的并行执行计划:

并行执行计划

请注意,“收集流”运算符之前的“实际”行计数为1,004,588。在集合流运算符之后,行数为预期的1,000,000。仍然是陌生人,该值不一致,并且每次运行都会有所不同。COUNT的结果始终正确。

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

SELECT
    COUNT(N)
FROM
    dbo.TallyTable(1000000) tt
OPTION (MAXDOP 1)

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

非并行执行计划

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

Answers:


12

行是在内部从生产者到消费者线程通过交换以数据包的形式传递的(因此称为CXPACKET-类交换数据包),而不是一次行。交换内部有一定数量的缓冲。同样,从Gather Streams的消费者方关闭管道的调用也必须以控制包的形式传递回生产者线程。计划和其他内部考虑因素意味着并行计划始终具有一定的“停止距离”。

因此,您经常会看到这种行数差异,其中实际需要的数量少于子树的整个潜在行集。在这种情况下,TOP将执行带到了“尽头”。

更多信息:


10

我想我对此可能有部分解释,但请随时将其记录下来或发布其他选择。通过强调执行计划中TOP的作用,@ MartinSmith肯定可以解决问题。

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

取自BOL

物理操作员初始化,收集数据并关闭。具体而言,物理操作员可以回答以下三个方法调用:

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

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

为了完整起见,对并行运算符有一些了解是有用的。工作由重分配流或分布式流运算符按并行计划分配到多个流。这些使用以下四种机制之一在线程之间分配行或页面:

  • 杂凑基于行中列的分布行
  • 循环通过循环遍历线程列表来分布行
  • 广播将所有页面或行分配到所有线程
  • 按需分区仅用于扫描。线程启动,向操作员请求一页数据,对其进行处理,并在完成后请求另一页数据。

第一个分布式流运算符(在计划中最右边)对源自恒定扫描的行使用需求分区。有三个线程分别调用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>

在下一个分配运算符处,我们再次具有三个线程,这次分别对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>

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

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

因此,我们现在有11个对GetNext()的调用,我们期望在那里看到10。

编辑:2011-11-13

停留在这一点上,我去聚集索引中的小伙子小贩讨价还价,@ MikeWalsh 在此友好地指示@SQLKiwi 。


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

计划

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

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

我的猜测只是,因为任务是并行处理的,所以一项任务是在飞行中的处理行中,而另一项则将第百万行发送给了集合流运算符,因此正在处理其他行。此外,本文还对行进行了缓冲,并分批将其传递给此迭代器,因此,无论如何,要处理的行数似乎很可能会超出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

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

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


1

我认为问题出在多个流可以处理同一行,这取决于流之间的行划分方式。

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.