代码改变世界

【SQLServer】并行执行计划中的分支和线程

2022-09-18 08:41  abce  阅读(237)  评论(0编辑  收藏  举报

SQL Server 2012的执行计划增加了保留线程和并行执行计划的使用信息。

例如下面的查询:

SELECT
    BP.ProductID,
    cnt = COUNT_BIG(*)
FROM dbo.bigProduct AS BP
JOIN dbo.bigTransactionHistory AS BTH
    ON BTH.ProductID = BP.ProductID
GROUP BY BP.ProductID
ORDER BY BP.ProductID;

执行优化器选择了并行执行:

计划资源管理器在根节点提示中显示并行线程使用详细信息。要在SSMS中查看相同的信息,请单击计划根节点,打开“属性”窗口,然后展开ThreadStat节点。 使用一台有8个逻辑处理器可供SQL Server使用的机器,此查询的典型运行中的线程使用信息如下所示,左侧是计划资源管理器,右侧是SSMS视图:

截图显示执行引擎为此查询保留了24个线程,并最终使用了其中的16个。它还显示查询计划具有三个分支,尽管它没有准确说明分支是什么。分支是并行查询计划的一部分,由交换运算符限定。下图绘制了边界,并对分支进行了编号:

 

我们来深入分析一下分支2(橙色部分)

在并行度(DOP)为8时,有8个线程运行查询计划的这个分支。这8个线程只是针对这个分支,并不是整个执行计划。在串行执行计划中,单个线程从数据源读取数据,通过多个计划运算符处理行,并将结果返回到目标(例如,可能是SSMS查询结果窗口或数据库表)。在并行执行计划的分支中,情况非常相似:每个线程从源读取数据,通过多个计划运算符处理行,并将结果返回到目标。区别在于目的地是交换(并行)操作,数据源也可以是交换操作。

在橙色分支中,数据源是聚集索引扫描,目标是重新分区流交换(Repartition Streams exchange)。橙色分支中的八个线程合作扫描表并将行添加到交换中。交换将行组装成页面大小的数据包。一旦数据包已满,它就会通过交换被推送到另一端。如果交换有另一个空包可填充,则该过程将继续,直到所有数据源行都已处理(或交换用完空包)。

我们可以使用计划资源管理器中的计划树视图查看每个线程上处理的行数:

计划资源管理器可以轻松查看计划中所有物理操作的行是如何跨线程分布的。在SSMS中,只能查看单个计划运算符的行分布。为此,请单击运算符图标,打开"属性"窗口,然后展开"实际行数"节点。
下图显示了橙色和紫色分支边界处的Repartition Streams节点的SSMS信息:

 

再来看看分支三(绿色部分)

分支3与分支2类似,但它包含一个额外的Stream Aggregate运算符。分支3也有8个线程,到目前为止一共看到了16个。分支3的线程从非聚集索引扫描中读取数据,执行某种聚合,并将结果传递给另一个Repartition Streams。

Stream Aggregate的Plan Explorer工具提示显示它正在按产品ID分组并计算标记为partialagg1005的表达式:

Expressions标签显示表达式是计算每组中的行数的结果:

Stream Aggregate正在计算部分(也称为"本地")聚合。部分(或本地)限定符只是意味着每个线程计算它看到的行上的聚合。索引扫描中的行在线程之间分布:没有提前固定的行分布;线程在请求​​时从扫描中接收一系列行。哪些行最终在哪些线程上基本上是随机的,因为它取决于时间问题和其他因素。

每个线程会从扫描中看到不同的行,但具有相同产品ID的行可能会被多个线程看到。聚合是"部分"的,因为特定产品ID组的小计可以出现在多个线程上;它是"本地的",因为每个线程仅根据它碰巧接收到的行来计算其结果。例如,假设表中有1,000 行产品ID #1。一个线程可能碰巧看到其中的432行,而另一个线程可能会看到568行。对于产品ID #1,两个线程都有部分行数(一个线程中的432行,另一个线程中的568行)。

部分聚合是一种性能优化,因为它比其他方法更早地减少了行数。在绿色分支中,早期聚合导致更少的行被组装成数据包并通过Repartition Stream交换推送。

 

分支1(紫色部分)

分支1多了8个线程,到现在已经24个线程了。此分支中的每个线程从两个Repartition Streams交换读取行,并将行写入一个Gather Streams交换。这个分支可能看起来很复杂和陌生,但它只是从数据源读取行并将结果发送到目标,就像任何其他查询计划一样。

该计划的右侧显示了从橙色和绿色分支中看到的两个Repartition Streams交换的另一侧读取的数据。交换的这个(左手边)被称为消费者端,因为这里附加的线程正在读取(消费)行。八个紫色分支线程是两个 Repartition Streams 交换处的数据消费者。

分支1中的每个线程运行分支中的每个运算符,就像单个线程执行串行执行计划中的每个操作一样。主要区别在于有8个线程同时运行,每个线程在任何给定时间都在不同的行上工作,使用查询计划运算符的不同实例。

此分支中的Stream Aggregate是一个全局聚合。它结合了在绿色分支中计算的部分(本地)聚合(记住一个线程中的432计数和另一个线程中的568的示例),以生成每个产品ID的组合总数。Plan Explorer工具提示显示全局结果表达式,标记为Expr1004:

每个产品ID的正确全局结果是通过对部分聚合求和来计算的,如"Expressions"选项所示:

继续我们的示例,产品ID #1 的1,000 行的正确结果是通过将432和568这两个小计相加获得的。

分支1中线程的每一个都从两个Gather Streams交换的消费者端读取数据,计算全局聚合,对产品ID执行Merge Join,
并将行添加到分支1中最左侧的Gather Streams交换。核心流程与普通串行计划没有太大区别;区别在于从哪里读取行,将它们发送到哪里,以及行在线程之间的分布方式……

 

交换行分布(Exchange Row Distribution)
警惕的读者会想知道一些细节。紫色分支如何设法计算每个产品ID的正确结果,而绿色分支却不能(相同产品ID的结果分布在许多线程中)?
此外,如果有8个单独的merge joins(每个线程一个),SQL Server如何保证将连接的行最终位于同一连接实例?

这两个问题都可以通过查看两个Repartition Streams从生产者端(在绿色和橙色分支中)到消费者端(在紫色分支中)交换路由行的方式来回答。
我们将首先查看与橙色和紫色分支接壤的Repartition Streams交换:

此交换使用应用于产品ID列的哈希函数路由传入行(来自橙色分支)。其效果是保证特定产品ID的所有行都被路由到同一个紫色分支线程。橙色和紫色的线程对这个路由一无所知;所有这些都由交换内部处理。

所有橙色线程都知道它们正在将行返回给请求它们的父迭代器(交换的生产者端)。同样,所有紫色线程都“知道”它们正在从数据源读取行。交换决定传入的橙色线程行将进入哪个数据包,它可以是八个候选数据包中的任何一个。类似地,交换器确定从哪个数据包中读取一行以满足来自紫色线程的读取请求。

注意不要幻想直接链接到特定紫色(消费者)线程的特定橙色(生产者)线程的心理图像。这不是这个查询计划的工作方式。橙色生产者最终可能会向所有紫色消费者发送行——路由完全取决于它处理的每一行中产品ID列的值。

另请注意,交换中的行数据包仅在已满时(或生产者端数据用完时)才会传输。想象一下交换器一次填充一行数据包,其中特定数据包的行可能来自任何生产者端(橙色)线程。一旦一个数据包满了,它就会被传递到消费者端,一个特定的消费者(紫色)线程可以开始读取它。

与绿色和紫色分支接壤的Repartition Streams交换以非常相似的方式工作:

在此交换中,行被路由到数据包,使用与之前看到的橙色-紫色交换相同的分区列上的相同哈希函数。这意味着两个Repartition Streams将具有相同产品ID的路由行交换到相同的紫色分支线程。

这解释了紫色分支中的流聚合如何能够计算全局聚合——如果在特定紫色分支线程上看到具有特定产品ID的一行,则保证该线程可以看到该产品ID的所有行(并且没有其他线程会)。

公共交换分区列也是合并连接的连接键,因此所有可能连接的行都保证由同一个(紫色)线程处理。

最后要注意的是,两个交换都是保留订单(也称为”合并”)的交换,如工具提示中的Order By属性所示。这满足了输入行按连接键排序的合并连接要求。请注意,交换本身不会对行进行排序,它们只能被配置为保留现有顺序。

 

线程0

执行计划的最后一部分位于Gather Streams交换的左侧。它总是在单个线程上运行。这个线程在执行计划中总是被标记为"线程0",有时也被称为"协调器"线程。

线程0读取行并将它们返回给客户端。请注意,Gather Streams也是一个合并交换(它具有 Order By属性):

 

保留的和已使用的线程

我们已经看到这个并行计划包含三个分支。这解释了为什么SQL Server保留24个线程(在DOP 8处有三个分支)。问题是为什么在上面的屏幕截图中只有16个线程被报告为"已使用"。​

答案分为两部分。第一部分不适用于该计划,但无论如何了解这一点很重要。报告的分支数是可以同时执行的最大数量。

你可能知道,某些计划运算符是"阻塞的"——这意味着它们必须先消耗所有输入行,然后才能生成第一个输出行。阻塞(也称为stop-and-go)运算符最明显的例子是Sort。在看到每个输入行之前,排序不能返回排序序列中的第一行,因为最后一个输入行可能首先排序。

具有多个输入的运算符(例如join和连接)可以对一个输入进行阻塞,但对另一个输入是非阻塞("流水线")。这方面的一个例子是哈希连接——构建输入是阻塞的,但探测输入是流水线的。构建输入是阻塞的,因为它创建了用于测试探测行的哈希表。

阻塞运算符的存在意味着可以保证一个或多个并行分支在其他分支开始之前完成。发生这种情况时,SQL Server可以将用于处理已完成分支的线程重用于序列中的后续分支。SQL Server对线程保留非常保守,因此只有保证在另一个开始之前完成的分支才会使用这种线程保留优化。我们的查询计划不包含任何阻塞运算符,因此报告的分支数只是分支的总数。

答案的第二部分是,如果线程碰巧在另一个分支中的线程启动之前完成,它们仍然可能被重用。在这种情况下仍然保留全部线程数,但实际使用量可能会更低。并行计划实际使用多少线程取决于时间问题等,并且在执行之间可能会有所不同。

并行线程并非全部同时开始执行,但其细节再次需要等待另一个场合。让我们再次查看查询计划,看看线程如何被重用,尽管缺少阻塞运算符:

很明显,分支一中的线程在分支二或三中的线程启动之前无法完成,因此那里没有线程重用的机会。分支3也不太可能在分支1或分支2启动之前完成,因为它有很多工作要做(大约3200万行要聚合)。

分支二是另一回事。产品表相对较小的大小意味着该分支很有可能在分支3启动之前完成其工作。如果读取产品表不会导致任何物理I/O,那么8个线程读取25,200行并将它们提交到橙紫色边界的Repartition Streams交换不会花费很长时间。

这正是到目前为止在本文中看到的截图所用的测试运行中发生的情况:八个橙色分支线程完成得足够快,可以将它们重用于绿色分支。总共使用了16个不同的线程,这就是执行计划报告的内容。

如果使用冷缓存重新运行查询,则物理I/O引入的延迟足以确保绿色分支线程在任何橙色分支线程完成之前启动。没有线程被重用,因此执行计划报告所有24个保留线程实际上都被使用了:

更一般地说,两个极端之间的任意数量的"已使用线程"(此查询计划为16和24)都是可能的:

最后,请注意,在最终Gather Streams左侧运行计划的串行部分的线程不计入并行线程总数。它不是为适应并行执行而添加的额外线程。

 

https://sqlperformance.com/2013/10/sql-plan/parallel-plans-branches-threads

https://www.cnblogs.com/abclife/p/16687789.html