表扫描中读取的实际行数乘以用于扫描的线程数

Rad*_*hiu 6 sql-server execution-plan sql-server-2008-r2 sql-server-2017

我遇到了一个很奇怪的问题。我正在运行相同的脚本来生成数据并稍后在较旧的 2008R2 实例上进行一些匹配。最后一个查询(一个UPDATE ) 执行单个表扫描并返回所有 250.000 行,而在较新的 2017 实例上,该表是并行扫描的,4 个线程中的每一个都读取 250.000 行,并返回 100 万“实际读取行”。

我在 2017 年的实例中将兼容模式更改为 2008 年,实际值保持不变,为 1.000.000。

为什么会发生这种情况是否有任何正当理由,或者这似乎应该是一个 Connect 项目?

计划包含相同的运算符,但其中一个执行并行扫描,而不是将 250.000 行拆分为 4 个线程中的每一个(并且每个线程仅读取 62.500 行)所有线程读取 250k,每个 *4 = 1.000.000

两个执行计划都可以在 pastetheplan 找到:

此外,我正在运行的完整脚本可以在下面找到:

create table #targets (id int identity(1,1), start_point int, end_point int, refference_type_id int, bla1 int, bla2 int, bla3 int, bla4 int, bla5 int, bla6 int, bla7 int, bla8 int, bla9 bit, assignedTouch varchar(10));

;with cte as (
    select
        1 sp
        , abs(checksum(newid())) % 11 + 2 ep
        , 1 rn
        , abs(checksum(newid())) % 3 b
        , abs(checksum(newid())) % 3 c
        , abs(checksum(newid())) % 3 d
        , abs(checksum(newid())) % 3 e
        , abs(checksum(newid())) % 3 f
        , abs(checksum(newid())) % 3 g
        , abs(checksum(newid())) % 3 g2
        , abs(checksum(newid())) % 3 h
        , abs(checksum(newid())) % 3 x3
    union all
    select
        sp
        , abs(checksum(newid())) % 11 + 2 ep
        , rn + 1
        , abs(checksum(newid())) % 8
        , abs(checksum(newid())) % 8
        , abs(checksum(newid())) % 8
        , abs(checksum(newid())) % 8
        , abs(checksum(newid())) % 8
        , abs(checksum(newid())) % 8
        , abs(checksum(newid())) % 8
        , abs(checksum(newid())) % 8
        , abs(checksum(newid())) % 8
    from cte
    where rn < 250000)
insert into #targets
select *
    , 'Unassigned' [Default State]
from cte
option (maxrecursion 0)

select top 250 *
    , char(abs(checksum(newid())) % 85 + 65) [class]
into #matching
from #targets
where end_point in ( 11, 14, 22, 33 )
order by abs(checksum(newid())) % 13

update #matching
set bla8 = case 
            when bla1 - bla2 > 0
                then NULL
            else bla8
        end
    , bla7 = case 
            when bla2 - bla3 > 0
                then NULL
            else bla7
        end
    , bla6 = case 
            when bla4 - bla5 > 0
                then NULL
            else bla6
        end

create nonclustered index nc_assignedTouch on #targets (assignedTouch);

update t
set assignedTouch = m.class
from #targets t
    inner join #matching m
        on t.start_point = isnull(m.start_point, t.start_point)
            and t.bla1 = isnull(m.bla1, t.bla1)
            and t.bla2 = isnull(m.bla2, t.bla2)
            and t.bla3 = isnull(m.bla3, t.bla3)
            and t.bla4 = isnull(m.bla4, t.bla4)
            and t.bla5 = isnull(m.bla5, t.bla5)
            and t.bla6 = isnull(m.bla6, t.bla6)
            and t.bla7 = isnull(m.bla7, t.bla7)
            and t.bla8 = isnull(m.bla8, t.bla8)
            and t.bla9 = isnull(m.bla9, t.bla9)
where t.assignedTouch = 'Unassigned';
Run Code Online (Sandbox Code Playgroud)

sp_configure 信息:

在此处输入图片说明

Pau*_*ite 12

并行堆扫描

您可能希望在并行线程之间进行分配,如下面的玩具示例所示:

SELECT TOP (5 * 1000 * 1000)
    n = ROW_NUMBER() OVER (ORDER BY @@SPID)
INTO #n
FROM sys.columns AS C
CROSS JOIN sys.columns AS C2
CROSS JOIN sys.columns AS C3;

SELECT COUNT_BIG(*) 
FROM #n AS N
GROUP BY N.n % 10
OPTION (USE HINT ('ENABLE_PARALLEL_PLAN_PREFERENCE'));
Run Code Online (Sandbox Code Playgroud)

在此处输入图片说明

在那个计划中,堆表确实是并行扫描的,所有线程协调共享读取整个表的工作:

在此处输入图片说明

或在 SSMS 视图中:

在此处输入图片说明

你的情况

但这不是您上传的计划中的安排:

在此处输入图片说明

堆扫描位于嵌套循环 join*内侧,因此每个线程都运行内侧的串行副本,这意味着存在 Table Spool 和 Table Scan 的 DOP(并行度)独立副本。

在 DOP 4 时,这意味着有四个线轴四个扫描,如Number of Executions = 4桌面扫描所证明的那样。实际上,堆表被完全扫描了四次(每个线程一次),得到 250,000 * 4 = 1,000,000 行。延迟假脱机缓存每个线程的扫描结果。

所以不同之处在于您的并行扫描是并行的四次串行扫描,而不是四个线程合作并行扫描一次堆(如上面的玩具示例)。

将差异概念化可能具有挑战性,但它至关重要。一旦您将两个交易所之间的分支视为 DOP 单独的串行计划,解码就会变得更容易。

当然,该计划效率极低,线轴几乎没有增加任何价值。请注意,连接谓词卡在嵌套循环连接处,而不是被推到内侧(使连接成为应用)。这是由于涉及ISNULL.

你可能会得到一个稍微好一点的计划 nc_assignedTouch索引聚集而不是非聚集,,但大部分工作仍会在连接处发生,而且改进几乎肯定是最小的。这里可能需要重写查询。如果您需要帮助以更易于执行的方式表达查询,请提出后续问题。

有关并行性方面的更多背景信息,请参阅我的文章了解和使用 SQL Server 中的并行性

脚注

*对此有一个普遍的例外,在嵌套循环连接的内侧可以看到真正的协作并行扫描(和交换):必须保证外部输入最多产生一行,并且循环join 不能有任何相关参数(外部引用)。在这些条件下,执行引擎将允许内部并行,因为它总是会产生正确的结果。

您可能还会注意到,并行嵌套循环连接内侧的 Eager Spool 下方的运算符也只执行一次。这些运算符仍然有 DOP 副本,但运行时行为是只有一个线程构建一个共享索引结构,然后由 Eager Spool 的所有实例使用。我很抱歉这一切太复杂了。