为具有 300 个分区的分区表重建索引

Rso*_*ola 6 sql-server partitioning sql-server-2016 index-maintenance cardinality-estimates

设想

分区表为空,我正在加载 1 个具有 180k 行的分区的数据。我禁用索引并加载数据并在加载数据后重建索引。

问题

在检查重建索引的查询计划时,我可以看到“估计行数”为 180k,但“实际行数”为 300 个分区 * 180,000 行 = 5400 万行,即使我只加载了一个分区的数据。

在此处输入图片说明 在此处输入图片说明

你能解释一下这种行为以及如何克服这个问题吗?

Microsoft SQL Server 2016 (SP2) (KB4052908) - 13.0.5026.0 (X64) Mar 18 2018 09:11:49 版权所有 (c) Windows Server 2012 R2 Standard 6.3(内部版本 9600)上的 Microsoft Corporation Enterprise Edition(64 位):

Pau*_*ite 14

在检查重建索引的查询计划时,我可以看到“估计行数”为 180k,但“实际行数”为 300 个分区 * 180,000 行 = 5400 万行,即使我只加载了一个分区的数据。

你的数学有点差。提供的图像显示了 364 次迭代(364 * 1,853,250 = 674,582,272)中估计的 1,853,250 行(不是 180k)和总共 674,582,272 行(不是 5400 万)。

尽管如此,问题仍然存在:当整个表仅包含 1,853,250 行时,为什么 SQL Server 会读取 6.74 亿行来重建非聚集索引?

显示的执行计划是并置连接。它执行以下操作:

  1. 常量扫描保存表的每个分区的分区号。
  2. 对于每个分区:
    1. 全表扫描(Clustered Index Scan)
    2. 将行排序为非聚集索引顺序(排序)
    3. 过滤当前分区的行(过滤器)
    4. 将行插入非聚集索引(Index Insert)

这显然是一种非常低效的处理方式。但优化器并不疯狂:总体思路是合理的,问题是过滤器应用得太晚了。通常,“当前分区”谓词会被下推到聚集索引扫描中,在那里它会显示为选择当前分区的搜索。

它应该如何工作

生成的通常并行计划如下所示:

正常平行计划

请注意缺少 Filter 运算符。它的谓词已被推入扫描:

用搜索扫描

是的,这是一个带有搜索的扫描。这个想法是在循环的每次迭代中仅从当前分区获取行,因此每次迭代都会构建非聚集索引的一个分区。

你的情况

我设法通过使用floatorbigint数据类型作为分区列来重现问题中显示的执行计划。过滤器的基数估计的某些方面(可能是猜测)阻止优化器将“当前分区”谓词推送到排序之外,并进入聚集索引扫描。

以下演示创建一个包含 300 个分区的表,并禁用非聚集索引,将一百万行加载到单个分区中,然后重建非聚集索引。它使用bigint数据类型:

DROP TABLE IF EXISTS dbo.Test;

IF EXISTS (SELECT * FROM sys.partition_schemes AS PS WHERE PS.[name] = N'PS')
    DROP PARTITION SCHEME PS;

IF EXISTS (SELECT * FROM sys.partition_functions AS PF WHERE PF.[name] = N'PF')
DROP PARTITION FUNCTION PF;
GO
CREATE PARTITION FUNCTION PF (bigint)
AS RANGE RIGHT 
FOR VALUES 
(
    0,
    1000000,2000000,3000000,4000000,5000000,6000000,7000000,8000000,9000000,10000000,
    11000000,12000000,13000000,14000000,15000000,16000000,17000000,18000000,19000000,
    20000000,21000000,22000000,23000000,24000000,25000000,26000000,27000000,28000000,
    29000000,30000000,31000000,32000000,33000000,34000000,35000000,36000000,37000000,
    38000000,39000000,40000000,41000000,42000000,43000000,44000000,45000000,46000000,
    47000000,48000000,49000000,50000000,51000000,52000000,53000000,54000000,55000000,
    56000000,57000000,58000000,59000000,60000000,61000000,62000000,63000000,64000000,
    65000000,66000000,67000000,68000000,69000000,70000000,71000000,72000000,73000000,
    74000000,75000000,76000000,77000000,78000000,79000000,80000000,81000000,82000000,
    83000000,84000000,85000000,86000000,87000000,88000000,89000000,90000000,91000000,
    92000000,93000000,94000000,95000000,96000000,97000000,98000000,99000000,100000000,
    101000000,102000000,103000000,104000000,105000000,106000000,107000000,108000000,109000000,
    110000000,111000000,112000000,113000000,114000000,115000000,116000000,117000000,118000000,119000000,
    120000000,121000000,122000000,123000000,124000000,125000000,126000000,127000000,128000000,129000000,
    130000000,131000000,132000000,133000000,134000000,135000000,136000000,137000000,138000000,139000000,
    140000000,141000000,142000000,143000000,144000000,145000000,146000000,147000000,148000000,149000000,
    150000000,151000000,152000000,153000000,154000000,155000000,156000000,157000000,158000000,159000000,
    160000000,161000000,162000000,163000000,164000000,165000000,166000000,167000000,168000000,169000000,
    170000000,171000000,172000000,173000000,174000000,175000000,176000000,177000000,178000000,179000000,
    180000000,181000000,182000000,183000000,184000000,185000000,186000000,187000000,188000000,189000000,
    190000000,191000000,192000000,193000000,194000000,195000000,196000000,197000000,198000000,199000000,
    200000000,201000000,202000000,203000000,204000000,205000000,206000000,207000000,208000000,209000000,
    210000000,211000000,212000000,213000000,214000000,215000000,216000000,217000000,218000000,219000000,
    220000000,221000000,222000000,223000000,224000000,225000000,226000000,227000000,228000000,229000000,
    230000000,231000000,232000000,233000000,234000000,235000000,236000000,237000000,238000000,239000000,
    240000000,241000000,242000000,243000000,244000000,245000000,246000000,247000000,248000000,249000000,
    250000000,251000000,252000000,253000000,254000000,255000000,256000000,257000000,258000000,259000000,
    260000000,261000000,262000000,263000000,264000000,265000000,266000000,267000000,268000000,269000000,
    270000000,271000000,272000000,273000000,274000000,275000000,276000000,277000000,278000000,279000000,
    280000000,281000000,282000000,283000000,284000000,285000000,286000000,287000000,288000000,289000000,
    290000000,291000000,292000000,293000000,294000000,295000000,296000000,297000000,298000000,299000000,
    300000000
);
CREATE PARTITION SCHEME PS
AS PARTITION PF
ALL TO ([PRIMARY]);
GO
CREATE TABLE dbo.Test
(
    c1 bigint NOT NULL,
    c2 bigint NOT NULL,

    CONSTRAINT [PK dbo.Test (c1)]
    PRIMARY KEY CLUSTERED (c1)
    ON PS (c1),

    INDEX [IX dbo.Test (c2)]
    NONCLUSTERED (c2)
    ON PS (c1)
)
ON PS (c1);
GO
ALTER INDEX [IX dbo.Test (c2)]
ON dbo.Test 
DISABLE;
GO
INSERT dbo.Test WITH (TABLOCKX)
(
    c1,
    c2
)
SELECT
    CONVERT(bigint, SV1.number * 1000 + SV2.number),
    CONVERT(bigint, (SV1.number * 1000 + SV2.number) * 2)
FROM master.dbo.spt_values AS SV1
CROSS JOIN master.dbo.spt_values AS SV2
WHERE
    SV1.[type] = N'P'
    AND SV2.[type] = N'P'
    AND SV1.number >= 0
    AND SV1.number < 1000
    AND SV2.number >= 0
    AND SV2.number < 1000;
GO
ALTER INDEX [IX dbo.Test (c2)]
ON dbo.Test
REBUILD;
Run Code Online (Sandbox Code Playgroud)

索引重建计划大约需要90 秒才能运行:

bigint 数据类型计划

相比之下,当分区列是一种integer数据类型时,重建需要两秒钟(使用前面显示的高效扫描和搜索计划)。

这可能是因为分区 id 本身就是一个integer(因此过滤谓词中不需要转换),但此时细节尚不清楚。

解决方法

如果我使用默认(现代)基数估计模型,以上内容只会为我重现。无法直接在索引重建语句上指定提示,因此以下使用记录的跟踪标志 9481 来临时使用遗留(原始)基数估计模型:

ALTER INDEX [IX dbo.Test (c2)]
ON dbo.Test 
DISABLE;

DBCC TRACEON (9481);

ALTER INDEX [IX dbo.Test (c2)]
ON dbo.Test
REBUILD;

DBCC TRACEOFF (9481);
Run Code Online (Sandbox Code Playgroud)

这将生成最佳执行计划并在大约两秒钟内完成。

真正的解决方案

有一种更好的方法可以将数据添加或加载到表的单个分区中。这个想法是SWITCH将现有分区放入一个独立的表中,将数据加载到其中,然后SWITCH将分区放回:

DROP TABLE IF EXISTS dbo.TestSwitchP2;
GO
-- Table with constraints limiting values to the target partition
CREATE TABLE dbo.TestSwitchP2
(
    c1 float NOT NULL PRIMARY KEY,
    c2 float NOT NULL,

    CHECK (c1 >= 0 AND c1 < 1000000)
)
ON [PRIMARY];
GO
-- Switch existing rows into the working table
ALTER TABLE dbo.Test
SWITCH PARTITION 2
TO dbo.TestSwitchP2;
GO
-- Add new rows
INSERT dbo.TestSwitchP2 WITH (TABLOCKX)
(
    c1,
    c2
)
SELECT
    CONVERT(float, SV1.number * 1000 + SV2.number),
    CONVERT(float, (SV1.number * 1000 + SV2.number) * 2)
FROM master.dbo.spt_values AS SV1
CROSS JOIN master.dbo.spt_values AS SV2
WHERE
    SV1.[type] = N'P'
    AND SV2.[type] = N'P'
    AND SV1.number >= 0
    AND SV1.number < 1000
    AND SV2.number >= 0
    AND SV2.number < 1000;
GO
-- Create a compatible nonclustered index
CREATE INDEX i ON dbo.TestSwitchP2 (c2);
GO
-- Switch the partition back in to the main table
ALTER TABLE dbo.TestSwitchP2
SWITCH TO dbo.Test 
PARTITION 2;
Run Code Online (Sandbox Code Playgroud)

分区切换是仅限元数据的操作,通常会立即完成。

上面的实现在不到两秒的时间内加载了一百万个新行。请注意,无需禁用主表上的非聚集索引。