为什么第二个INSERT
语句比第一个慢 ~5 倍?
从生成的日志数据量来看,我认为第二个不符合最小日志记录的条件。但是,数据加载性能指南中的文档指出这两个插入应该能够被最少地记录。因此,如果最小日志记录是关键的性能差异,为什么第二个查询不符合最小日志记录的条件?可以做些什么来改善这种情况?
查询 #1:使用 INSERT...WITH (TABLOCK) 插入 5MM 行
考虑以下查询,它将 5MM 行插入到堆中。此查询在 中执行1 second
并生成64MB
所报告的事务日志数据sys.dm_tran_database_transactions
。
CREATE TABLE dbo.minimalLoggingTest (n INT NOT NULL)
GO
INSERT INTO dbo.minimalLoggingTest WITH (TABLOCK) (n)
SELECT n
-- Any table/view/sub-query that correctly estimates that it will generate 5MM rows
FROM dbo.fiveMillionNumbers
-- Provides greater consistency on my laptop, where other processes are running
OPTION (MAXDOP 1)
GO
查询 #2:插入相同的数据,但 SQL 低估了行数
现在考虑这个非常相似的查询,它对完全相同的数据进行操作,但恰好是从SELECT
基数估计值太低的表(或在我的实际生产案例中具有许多连接的复杂语句)中提取的。此查询在事务日志数据中执行5.5 seconds
并生成461MB
。
CREATE TABLE dbo.minimalLoggingTest (n INT NOT NULL)
GO
INSERT INTO dbo.minimalLoggingTest WITH (TABLOCK) (n)
SELECT n
-- Any table/view/sub-query that produces 5MM rows but SQL estimates just 1000 rows
FROM dbo.fiveMillionNumbersBadEstimate
-- Provides greater consistency on my laptop, where other processes are running
OPTION (MAXDOP 1)
GO
完整脚本
请参阅此 Pastebin以获取用于生成测试数据并执行其中任一场景的全套脚本。请注意,您必须使用SIMPLE
恢复模型中的数据库。
商业背景
我们半频繁地移动数百万行数据,让这些操作尽可能高效非常重要,无论是在执行时间还是磁盘 I/O 负载方面。我们最初的印象是创建堆表并使用INSERT...WITH (TABLOCK)
它是一个很好的方法,但现在我们变得不那么自信了,因为我们在实际生产场景中观察到了上面展示的情况(尽管有更复杂的查询,而不是此处为简化版)。
为什么第二个查询不符合最少日志记录的条件?
最小日志记录可用于第二个查询,但引擎选择在运行时不使用它。
有一个最小阈值,
INSERT...SELECT
低于该阈值它选择不使用批量加载优化。设置批量行集操作会产生成本,并且仅批量插入几行不会导致有效的空间利用。可以做些什么来改善这种情况?
SELECT INTO
使用没有此阈值的许多其他方法之一(例如)。或者,您可能能够以某种方式重写源查询,以提高估计的行/页数超过INSERT...SELECT
.另请参阅Geoff 的自我回答以获取更多有用信息。
可能有趣的琐事: 仅在不使用批量加载优化时
SET STATISTICS IO
报告目标表的逻辑读取。我能够用自己的测试装置重现问题:
这就引出了一个问题,为什么不在运行最少日志记录操作之前通过更新源表上的统计信息来“解决”问题?
扩展 Paul 的想法,如果您真的绝望,一种解决方法是添加一个虚拟表,以确保插入的估计行数足够高以达到批量加载优化的质量。我确认这会获得最少的日志记录并提高查询性能。
最后的收获
SELECT...INTO
一次性插入操作。正如保罗指出的那样,无论行估计如何,这都将确保最少的日志记录相关文章
Paul White 的 2019 年 5 月博客文章使用 INSERT…SELECT 进入堆表进行最小日志记录更详细地介绍了其中的一些信息。