为什么临时表比急切线轴更有效地解决万圣节问题?

Joe*_*ish 14 sql-server database-internals sql-server-2017

考虑以下查询,该查询仅在源表中不存在时才插入源表中的行:

INSERT INTO dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR WITH (TABLOCK)
SELECT maybe_new_rows.ID
FROM dbo.A_HEAP_OF_MOSTLY_NEW_ROWS maybe_new_rows
WHERE NOT EXISTS (
    SELECT 1
    FROM dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR halloween
    WHERE maybe_new_rows.ID = halloween.ID
)
OPTION (MAXDOP 1, QUERYTRACEON 7470);
Run Code Online (Sandbox Code Playgroud)

一种可能的计划形状包括合并连接和急切线轴。Eager spool 运算符用于解决万圣节问题

第一个计划

在我的机器上,上面的代码在大约 6900 毫秒内执行。用于创建表格的重现代码包含在问题的底部。如果我对性能不满意,我可能会尝试加载要插入到临时表中的行,而不是依赖于 Eager spool。这是一种可能的实现:

DROP TABLE IF EXISTS #CONSULTANT_RECOMMENDED_TEMP_TABLE;
CREATE TABLE #CONSULTANT_RECOMMENDED_TEMP_TABLE (
    ID BIGINT,
    PRIMARY KEY (ID)
);

INSERT INTO #CONSULTANT_RECOMMENDED_TEMP_TABLE WITH (TABLOCK)
SELECT maybe_new_rows.ID
FROM dbo.A_HEAP_OF_MOSTLY_NEW_ROWS maybe_new_rows
WHERE NOT EXISTS (
    SELECT 1
    FROM dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR halloween
    WHERE maybe_new_rows.ID = halloween.ID
)
OPTION (MAXDOP 1, QUERYTRACEON 7470);

INSERT INTO dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR WITH (TABLOCK)
SELECT new_rows.ID
FROM #CONSULTANT_RECOMMENDED_TEMP_TABLE new_rows
OPTION (MAXDOP 1);
Run Code Online (Sandbox Code Playgroud)

新代码的执行时间约为 4400 毫秒。我可以获得实际计划并使用 Actual Time Statistics™ 来检查操作员级别的时间花费在哪里。请注意,要求实际计划会为这些查询增加大量开销,因此总数将与之前的结果不匹配。

????????????????????????????????????????????
?  operator   ? first query ? second query ?
????????????????????????????????????????????
? big scan    ? 1771        ? 1744         ?
? little scan ? 163         ? 166          ?
? sort        ? 531         ? 530          ?
? merge join  ? 709         ? 669          ?
? spool       ? 3202        ? N/A          ?
? temp insert ? N/A         ? 422          ?
? temp scan   ? N/A         ? 187          ?
? insert      ? 3122        ? 1545         ?
????????????????????????????????????????????
Run Code Online (Sandbox Code Playgroud)

与使用临时表的计划相比,使用 Eager spool 的查询计划似乎在插入和假脱机操作符上花费了更多的时间。

为什么带有临时表的计划效率更高?无论如何,一个急切的线轴不是主要只是一个内部临时表吗?我相信我正在寻找专注于内部的答案。我能够看到调用堆栈的不同之处,但无法弄清楚大局。

如果有人想知道,我在 SQL Server 2017 CU 11 上。以下是填充上述查询中使用的表的代码:

DROP TABLE IF EXISTS dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR;

CREATE TABLE dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR (
ID BIGINT NOT NULL,
PRIMARY KEY (ID)
);

INSERT INTO dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR WITH (TABLOCK)
SELECT TOP (20000000) ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM master..spt_values t1
CROSS JOIN master..spt_values t2
CROSS JOIN master..spt_values t3
OPTION (MAXDOP 1);


DROP TABLE IF EXISTS dbo.A_HEAP_OF_MOSTLY_NEW_ROWS;

CREATE TABLE dbo.A_HEAP_OF_MOSTLY_NEW_ROWS (
ID BIGINT NOT NULL
);

INSERT INTO dbo.A_HEAP_OF_MOSTLY_NEW_ROWS WITH (TABLOCK)
SELECT TOP (1900000) 19999999 + ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM master..spt_values t1
CROSS JOIN master..spt_values t2;
Run Code Online (Sandbox Code Playgroud)

Pau*_*ite 14

这就是我所说的手动万圣节保护

您可以在我的文章优化更新查询 中找到它与更新语句一起使用的示例。必须小心保留相同的语义,例如在单独的查询执行时锁定目标表以防止所有并发修改,如果这与您的场景相关。

为什么带有临时表的计划效率更高?无论如何,一个急切的线轴不是主要只是一个内部临时表吗?

线轴具有临时表的一些特征,但两者并不完全等效。特别是,线轴本质上是对 b 树结构的逐行无序插入。它确实受益于锁定和日志记录优化,但不支持批量加载优化

因此,通常可以通过以自然方式拆分查询来获得更好的性能:将新行批量加载到临时表或变量中,然后从临时对象执行优化插入(没有明确的万圣节保护)。

进行这种分离还允许您额外自由地分别调整原始语句的读取和写入部分。

作为旁注,考虑如何使用行版本解决万圣节问题是很有趣的。也许 SQL Server 的未来版本将在合适的情况下提供该功能。


正如 Michael Kutz 在评论中提到的那样,您还可以探索利用填孔优化来避免显式 HP的可能性。实现这一目标的演示一种方法是在创建一个唯一索引(clustered如果你喜欢)ID的列A_HEAP_OF_MOSTLY_NEW_ROWS

CREATE UNIQUE INDEX i ON dbo.A_HEAP_OF_MOSTLY_NEW_ROWS (ID);
Run Code Online (Sandbox Code Playgroud)

有了这个保证,优化器可以使用空洞填充和行集共享:

MERGE dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR WITH (SERIALIZABLE) AS HICETY
USING dbo.A_HEAP_OF_MOSTLY_NEW_ROWS AS AHOMNR
    ON AHOMNR.ID = HICETY.ID
WHEN NOT MATCHED BY TARGET
THEN INSERT (ID) VALUES (AHOMNR.ID);
Run Code Online (Sandbox Code Playgroud)

合并计划

虽然很有趣,但在许多情况下,您仍然可以通过采用精心实施的手动万圣节保护来获得更好的性能。


Joe*_*ish 5

为了稍微扩展 Paul 的回答,线轴和临时表方法之间经过时间的部分差异似乎归结为缺乏对DML Request Sort线轴计划中的选项的支持。使用未记录的跟踪标志 8795,临时表方法的运行时间从 4400 毫秒跳到 5600 毫秒。

INSERT INTO dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR WITH (TABLOCK)
SELECT new_rows.ID
FROM #CONSULTANT_RECOMMENDED_TEMP_TABLE new_rows
OPTION (MAXDOP 1, QUERYTRACEON 8795);
Run Code Online (Sandbox Code Playgroud)

请注意,这并不完全等同于假脱机计划执行的插入。此查询将大量数据写入事务日志。

同样的效果可以通过一些技巧反过来看到。可以鼓励 SQL Server 使用排序而不是假脱机进行万圣节保护。一种实现:

INSERT INTO dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR WITH (TABLOCK)
SELECT TOP (987654321) 
maybe_new_rows.ID
FROM dbo.A_HEAP_OF_MOSTLY_NEW_ROWS maybe_new_rows
WHERE NOT EXISTS (
    SELECT 1
    FROM dbo.HALLOWEEN_IS_COMING_EARLY_THIS_YEAR halloween
    WHERE maybe_new_rows.ID = halloween.ID
)
ORDER BY maybe_new_rows.ID, maybe_new_rows.ID + 1
OPTION (MAXDOP 1, QUERYTRACEON 7470, MERGE JOIN);
Run Code Online (Sandbox Code Playgroud)

现在该计划有一个 TOP N Sort 运算符来代替线轴。sort 是一个阻塞运算符,因此不再需要 spool:

在此处输入图片说明

更重要的是,我们现在支持该DML Request Sort选项。再次查看实际时间统计信息,插入操作符现在只需要 1623 毫秒。整个计划大约需要 5400 毫秒才能执行,而无需请求实际计划。

正如 Hugo解释的那样,Eager Spool 运算符确实保留了顺序。通过TOP PERCENT计划最容易看出这一点。不幸的是,带有假脱机的原始查询无法更好地利用假脱机中数据的排序特性。