可怜的基数估计使INSERT无法进行最少的记录?


11

为什么第二个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)是执行此操作的一种好方法,但是现在,由于我们在实际的生产场景中观察到了上面演示的情况(尽管使用了更复杂的查询,而不是使用简化版)。

Answers:


7

为什么第二个查询不符合最少日志记录的条件?

最小日志记录用于第二个查询,但是引擎选择在运行时不使用它。

有一个最低阈值INSERT...SELECT在该最低阈值之下,它选择不使用批量负载优化。设置批量行集操作涉及成本,并且仅批量插入几行不会导致有效的空间利用。

可以采取什么措施来改善这种情况?

使用其他SELECT INTO没有此阈值的方法(例如)中的一种。另外,您也可以通过某种方式重写源查询,以使估计的行数/页数超过的阈值INSERT...SELECT

另请参阅Geoff的自我解答以获取更多有用的信息。


可能有趣的琐事: 仅在不使用批量加载优化时SET STATISTICS IO报告目标表的逻辑读取。


5

我能够使用自己的测试装置重新创建问题:

USE test;

CREATE TABLE dbo.SourceGood
(
    SourceGoodID INT NOT NULL
        CONSTRAINT PK_SourceGood
        PRIMARY KEY CLUSTERED
        IDENTITY(1,1)
    , SomeData VARCHAR(384) NOT NULL
);

CREATE TABLE dbo.SourceBad
(
    SourceBadID INT NOT NULL
        CONSTRAINT PK_SourceBad
        PRIMARY KEY CLUSTERED
        IDENTITY(-2147483647,1)
    , SomeData VARCHAR(384) NOT NULL
);

CREATE TABLE dbo.InsertTest
(
    SourceBadID INT NOT NULL
        CONSTRAINT PK_InsertTest
        PRIMARY KEY CLUSTERED
    , SomeData VARCHAR(384) NOT NULL
);
GO

INSERT INTO dbo.SourceGood WITH (TABLOCK) (SomeData) 
SELECT TOP(5000000) o.name + o1.name + o2.name
FROM syscolumns o
    , syscolumns o1
    , syscolumns o2;
GO

ALTER DATABASE test SET AUTO_UPDATE_STATISTICS OFF;
GO

INSERT INTO dbo.SourceBad WITH (TABLOCK) (SomeData)
SELECT TOP(5000000) o.name + o1.name + o2.name
FROM syscolumns o
    , syscolumns o1
    , syscolumns o2;
GO

ALTER DATABASE test SET AUTO_UPDATE_STATISTICS ON;
GO

BEGIN TRANSACTION;

INSERT INTO dbo.InsertTest WITH (TABLOCK)
SELECT *
FROM dbo.SourceGood;

SELECT * FROM sys.dm_tran_database_transactions;

/*
database_transaction_log_record_count
472 
database_transaction_log_bytes_used
692136
*/

COMMIT TRANSACTION;


BEGIN TRANSACTION;

INSERT INTO dbo.InsertTest WITH (TABLOCK)
SELECT *
FROM dbo.SourceBad;

SELECT * FROM sys.dm_tran_database_transactions;

/*
database_transaction_log_record_count   
5000003 
database_transaction_log_bytes_used
642699256
*/

COMMIT TRANSACTION;

这就引出了一个问题,为什么不通过在运行最少记录操作之前更新源表上的统计信息来“解决”该问题?

TRUNCATE TABLE dbo.InsertTest;
UPDATE STATISTICS dbo.SourceBad;

BEGIN TRANSACTION;

INSERT INTO dbo.InsertTest WITH (TABLOCK)
SELECT *
FROM dbo.SourceBad;

SELECT * FROM sys.dm_tran_database_transactions;

/*
database_transaction_log_record_count
472
database_transaction_log_bytes_used
692136
*/

COMMIT TRANSACTION;

2
在实际代码中,有一个SELECT包含大量联接的复杂语句,该语句为生成结果集INSERT。这些联接对最终表插入运算符的基数估计值不佳(我已经通过错误UPDATE STATISTICS调用在repro脚本中进行了模拟),因此,它并不比发出UPDATE STATISTICS命令来解决问题那么简单。我完全同意,简化查询以使基数估计器更容易理解可能是一个好方法,但是实现给定的复杂业务逻辑并不是一个简单的方法。
Geoff Patterson

我没有要测试的SQL Server 2014实例,但是,确定SQL Server 2014新基数估计器问题和Service Pack 1改进涉及启用跟踪标志4199等以启用新基数估计器。你有尝试过吗?
Max Vernon

好主意,但这没有帮助。我刚刚尝试了TF 4199,TF 610(失去了最小的日志记录条件),并且两者都一起使用(嘿,为什么不呢?),但是第二次测试查询没有任何变化。
Geoff Patterson

4

以某种方式重写源查询以提高估计的行数

扩展Paul的想法,如果您真的很绝望,一种解决方法是添加一个虚拟表,以确保插入的估计行数足够高,足以进行批量加载优化。我确认这将减少日志记录并提高查询性能。

-- Create a dummy table that SQL Server thinks has a million rows
CREATE TABLE dbo.emptyTableWithMillionRowEstimate (
    n INT PRIMARY KEY
)
GO
UPDATE STATISTICS dbo.emptyTableWithMillionRowEstimate
WITH ROWCOUNT = 1000000
GO

-- Concatenate this table into the final rowset:
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.fiveMillionNumbersBadEstimate
-- Add in dummy rowset to ensure row estimate is high enough for bulk load optimization
UNION ALL
SELECT NULL FROM dbo.emptyTableWithMillionRowEstimate
OPTION (MAXDOP 1)

最后要点

  1. 使用SELECT...INTO如果需要一次性插入操作最小记录。正如Paul所指出的,无论行估计如何,这将确保最少的日志记录
  2. 尽可能以一种简单的方式编写查询,以使查询优化器可以有效地进行推理。例如,可以将查询分为多个部分,以允许将统计信息构建在中间表上。
  3. 如果您有权访问SQL Server 2014,请在查询中尝试一下;在我的实际生产案例中,我只是尝试了一下,而新的Cardinality Estimator产生了更高(更好)的估算值;然后将查询记录为最少。但是,如果您需要支持SQL 2012及更早版本,这可能无济于事。
  4. 如果您不顾一切,可以使用像这样的骇人解决方案!

相关文章

保罗·怀特(Paul White)在2019年5月的博客文章《使用INSERT…SELECT最小记录日志到堆表》中更详细地介绍了其中一些信息。

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.