前一篇文章提到了在2014下分区表引起的奇葩问题,最近也在苦苦研究问题的原因。这篇文章主要讲述在分区表下,查询是怎么样的一个情况。由于2014的新功能“参数估计”下是如何运转的目前无从得知,所以只能分享下2008下的原理了...
在 SQL Server 2008 中,已分区表的内部表示形式已发生变化,即已分区表将作为一个多列索引呈现给查询处理器,其中 PartitionID是第一列。PartitionID 是一个隐藏的计算列,用于在内部表示包含特定行的分区的 ID。例如,假设一个定义为 T(a, b, c) 的表 T 在 a列进行了分区,并在 b 列的聚集索引。在 SQL Server 2008 中,此分区表在内部被视为一个具有架构 T(PartitionID, a, b, c) 的未分区表,并具有组合键 (PartitionID, b) 的聚集索引。这样查询优化器便可以基于 PartitionID 对任何已分区表或索引执行查找操作。
此外,查询优化器的功能也得以扩展,可以针对 PartitionID(作为逻辑首列)以及其他可能的索引键列执行某一条件下的查找或扫描操作,然后,对于符合第一级查找操作的条件的每个不同值,再针对一个或多个其他列执行不同条件下的二级查找。也就是说,这种称为“skip-scan”的操作允许查询优化器基于某一条件来执行查找或扫描操作以确定要访问的分区,然后在该运算符内执行一个二级索引查找操作以返回这些分区中符合另一个不同条件的行。例如,请考虑以下查询。
SELECT * FROM T WHERE a < 10 and b = 2;
对于本示例,假设定义为 T(a, b, c) 的表 T 对 a 列进行了分区,并具有 b 的聚集索引。表 T 的分区边界由以下分区函数定义:
CREATE PARTITION FUNCTION myRangePF1 (int) AS RANGE LEFT FOR VALUES (3, 7, 10);
为求解该查询,查询处理器将执行第一级查找操作以查找包含符合条件 T.a < 10 的行的每个分区。这将标识要访问的分区。然后,在所标识的每个分区内,处理器将针对 b 列的聚集索引执行一个二级查找以查找符合条件 T.b = 2 和 T.a < 10 的行。
下图所示为跳跃扫描操作的逻辑表示形式,其中显示了在 a 列和 b 列中包含数据的表 T。分区编号为 1 到 4,分区边界由垂直虚线表示。对分区执行的第一级查找操作(图中未显示)已确定分区 1、2 和 3 符合查找条件(由为该表定义的分区和 a 列的谓词指示),即T.a < 10。曲线指示了跳跃扫描操作的二级查找部分所遍历的路径。实际上,跳跃扫描操作将在这些分区的每个分区中查找符合条件 b = 2 的行。跳跃扫描操作的总开销等于三个单独索引查找之和。
-------已分区堆的执行计划------------------------------------------------------
已分区堆被视为分区 ID 的逻辑索引。已分区堆的分区排除在执行计划中表示为一个 Table Scan 运算符,其中对分区 ID 使用了 SEEK 谓词。下面的示例显示了所提供的显示计划信息:
|-- Table Scan (OBJECT: ([db].[dbo].[T]), SEEK: ([PtnId1001]=[Expr1011]) ORDERED FORWARD)
------归置联接的执行计划--------------------------------------------------------
使用相同或等效的分区函数对两个表进行分区并且在查询的联接条件中指定了来自联接两侧的分区依据列时就会发生联接归置。查询优化器可以生成一个计划,其中具有相等分区 ID 的每个表的分区将分别联接在一起。归置联接可能比非归置联接的执行速度快,因为前者可以只需较少的内存和处理时间。优化器会基于成本估计来选择非归置计划或归置计划。
在归置计划中,Nested Loops 联接从内侧读取一个或多个联接表或索引分区。Constant Scan 运算符内的数字表示分区号。
为已分区表或索引生成归置联接的并行计划时,在 Constant Scan 和 Nested Loops 联接运算符之间会出现一个 Parallelism 运算符。在此情况下,在联接外侧的多个线程会各自在不同的分区上进行读取和操作。
下图显示了一个归置联接的并行查询计划。
------并行查询执行策略--------------------------------------------------------
查询处理器对从已分区对象选择的查询使用查询执行策略。作为执行策略的一部分,查询处理器会确定查询所需的表分区,以及要分配给每个分区的线程比例。在大多数情况下,查询处理器会为每个分区分配数量相等或几乎相等的线程,然后在这些分区中并行地执行查询。以下几段更详细地介绍了线程分配情况。
如果线程数小于分区数,则查询处理器会将每个线程分配给一个不同的分区,最初会有一个或多个分区没有获得分配的线程。当线程完成在一个分区上的执行时,查询处理器会将它分配给下一个分区,直到每个分区都分配有一个线程。这是查询处理器将线程重新分配给其他分区的唯一情况。
如果线程数与分区数相等,则查询处理器会为每个分区分配一个线程。当线程完成时,不会重新分配给另一个分区。
如果线程数大于分区数,则查询处理器会为每个分区分配相等数量的线程。如果线程数并非恰好是分区数的倍数,则查询处理器会为某些分区额外分配一个线程,以使用所有可用线程。请注意,如果只有一个分区,则会将所有线程都分配给该分区。在下图中,有四个分区和 14 个线程。每个分区都分配有 3 个线程,两个分区具有一个额外的线程,总共分配了 14 个线程。当线程完成时,不会重新分配给另一个分区。
尽管以上示例指出了一种分配线程的简单方式,但实际策略要复杂一些,并需要考虑在查询执行过程中出现的其他变化因素。例如,如果表已分区,并在 A 列上有一个聚集索引,并且查询有谓词子句 WHERE A IN (13, 17, 25),则查询处理器将为这三个查找值(A=13、A=17 和 A=25))各分配一个或多个线程,而不是为每个表分区分配一个或多个线程。只需在包含这些值的分区中执行查询,并且如果所有这些查找谓词都恰好在同一个表分区中,则所有线程都将分配给同一个表分区。
为了举出另一个示例,假定表在 A 列上有四个分区(边界点为 (10, 20, 30)),在 B 列上有一个索引,并且查询有一个谓词子句WHERE B IN (50, 100, 150)。因为表分区是基于值 A,所以值 B 可以出现在任何表分区中。这样,查询处理器将分别在四个表分区中查找三个 B 值 (50, 100, 150) 中的每一个值。查询处理器将按比例分配线程,以便它可以并行执行 12 个查询扫描中的每一个扫描。
基于 A 列的表分区 |
在每个表分区中查找 B 列 |
---|---|
表分区 1:A < 10 |
B=50, B=100, B=150 |
表分区 2:A >= 10 AND A < 20 |
B=50, B=100, B=150 |
表分区 3:A >= 20 AND A < 30 |
B=50, B=100, B=150 |
表分区 4:A >= 30 |
B=50, B=100, B=150 |