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


14

考虑以下查询,该查询仅在源表中的行尚未插入目标表中时才插入它们:

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);

一种可能的计划形状包括合并联接和渴望的线轴。热心的线轴操作员出席以解决万圣节问题

第一个计划

在我的计算机上,以上代码在大约6900毫秒内执行。问题的底部包括创建表的Repro代码。如果我对性能不满意,则可以尝试加载要插入到临时表中的行,而不是依赖急切的假脱机。这是一种可能的实现:

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);

新代码将在大约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         
╚═════════════╩═════════════╩══════════════╝

与使用临时表的计划相比,具有急切假脱机的查询计划似乎在插入和假脱机运算符上花费了更多的时间。

为什么使用临时表的计划效率更高?急切的假脱机是否还只是内部临时表?我相信我正在寻找针对内部的答案。我能够看到调用堆栈的不同之处,但无法弄清楚全局。

我正在使用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;

Answers:


14

这就是所谓的“ 手动万圣节防护”

您可以在我的文章“ 优化更新查询”中找到与更新语句一起使用的示例。必须保留一些相同的语义,例如,如果在您的方案中很重要,则可以在执行单独的查询时锁定目标表以防止所有并发的修改。

为什么使用临时表的计划效率更高?急切的假脱机是否还只是内部临时表?

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

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

进行这种分离还使您具有额外的自由度,可以分别调整原始语句的读取和写入部分。

附带说明一下,考虑如何使用行版本解决万圣节问题很有趣。也许将来的SQL Server版本将在适当的情况下提供该功能。


正如迈克尔·库兹(Michael Kutz)在评论中提到的那样,您还可以探索利用孔填充优化来避免显式HP 的可能性。实现此演示的一种方法是在的ID列上创建一个唯一索引(如果需要,可以聚集)A_HEAP_OF_MOSTLY_NEW_ROWS

CREATE UNIQUE INDEX i ON dbo.A_HEAP_OF_MOSTLY_NEW_ROWS (ID);

有了这一保证,优化器就可以使用孔填充和行集共享:

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);

合并计划

有趣的是,在许多情况下,仍可以通过采用精心实现的“手动万圣节防护”来获得更好的性能。


5

为了进一步解释Paul的答案,假脱机和临时表方法之间经过时间的部分差异似乎可以归结为DML Request Sort对假脱机计划中的选项缺乏支持。使用未记录的跟踪标志8795,临时表方法的已用时间从4400 ms跳到5600 ms。

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);

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

反过来可以看到一些欺骗的效果。可以鼓励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);

现在,该计划将TOP N Sort运算符替换为线轴。sort是一个阻塞运算符,因此不再需要假脱机:

在此处输入图片说明

更重要的是,我们现在对 DML Request Sort选项。再次查看“实际时间统计信息”,插入操作符现在仅花费1623 ms。整个计划大约需要5400毫秒才能执行,而无需实际计划。

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

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.