基数估计不佳会使 INSERT 不符合最少日志记录吗?

Geo*_*son 11 performance sql-server insert transaction-log sql-server-2014

为什么第二个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
Run Code Online (Sandbox Code Playgroud)


查询 #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
Run Code Online (Sandbox Code Playgroud)


完整脚本

请参阅此 Pastebin以获取用于生成测试数据并执行这些场景中的任何一个的完整脚本集。请注意,您必须使用处于SIMPLE 恢复模型中的数据库。


业务背景

我们半频繁地移动数百万行数据,重要的是让这些操作尽可能高效,无论是在执行时间还是磁盘 I/O 负载方面。我们最初的印象是创建一个堆表并使用INSERT...WITH (TABLOCK)是一个很好的方法来做到这一点,但现在我们变得不那么自信了,因为我们在实际生产场景中观察到了上面展示的情况(尽管有更复杂的查询,而不是简化版在这里)。

Pau*_*ite 7

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

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

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

可以做些什么来改善这种情况?

使用SELECT INTO没有此阈值的许多其他方法(例如)中的一种。或者,您可能能够以某种方式重写源查询,以将估计的行/页数提高到阈值INSERT...SELECT

有关更多有用信息,另请参阅Geoff 的自我回答


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


Han*_*non 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;
Run Code Online (Sandbox Code Playgroud)

这就引出了一个问题,为什么不在运行最小日志操作之前通过更新源表的统计信息来“修复”问题?

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;
Run Code Online (Sandbox Code Playgroud)

  • 在实际代码中,有一个复杂的“SELECT”语句,其中包含许多连接,用于生成“INSERT”的结果集。这些连接对最终表插入操作符(我通过错误的“UPDATE STATISTICS”调用在重现脚本中模拟)产生了较差的基数估计,因此它不像发出“UPDATE STATISTICS”命令来解决问题那么简单. 我完全同意简化查询以便基数估计器更容易理解可能是一种很好的方法,但实现给定的复杂业务逻辑并非易事。 (2认同)