为什么在插入索引表时没有获得最少的日志记录

Gav*_*vin 14 sql-server transaction-log

我正在测试不同场景中的最少日志记录插入,并且从我读到的 INSERT INTO SELECT 到带有非聚集索引的堆中,使用 TABLOCK 和 SQL Server 2016+ 应该最少记录,但是在我的情况下,这样做时我得到完整记录。我的数据库处于简单恢复模型中,我成功地在没有索引和 TABLOCK 的堆上获得了最少记录的插入。

我正在使用 Stack Overflow 数据库的旧备份进行测试,并使用以下架构创建了 Posts 表的副本...

CREATE TABLE [dbo].[PostsDestination](
    [Id] [int] NOT NULL,
    [AcceptedAnswerId] [int] NULL,
    [AnswerCount] [int] NULL,
    [Body] [nvarchar](max) NOT NULL,
    [ClosedDate] [datetime] NULL,
    [CommentCount] [int] NULL,
    [CommunityOwnedDate] [datetime] NULL,
    [CreationDate] [datetime] NOT NULL,
    [FavoriteCount] [int] NULL,
    [LastActivityDate] [datetime] NOT NULL,
    [LastEditDate] [datetime] NULL,
    [LastEditorDisplayName] [nvarchar](40) NULL,
    [LastEditorUserId] [int] NULL,
    [OwnerUserId] [int] NULL,
    [ParentId] [int] NULL,
    [PostTypeId] [int] NOT NULL,
    [Score] [int] NOT NULL,
    [Tags] [nvarchar](150) NULL,
    [Title] [nvarchar](250) NULL,
    [ViewCount] [int] NOT NULL
)
CREATE NONCLUSTERED INDEX ndx_PostsDestination_Id ON PostsDestination(Id)
Run Code Online (Sandbox Code Playgroud)

然后我尝试将帖子表复制到该表中...

INSERT INTO PostsDestination WITH(TABLOCK)
SELECT * FROM Posts ORDER BY Id 
Run Code Online (Sandbox Code Playgroud)

通过查看 fn_dblog 和日志文件使用情况,我可以看到我没有从中获得最少的日志记录。我读过 2016 年之前的版本需要跟踪标志 610 以最低限度地记录到索引表,我也尝试过设置它,但仍然没有乐趣。

我猜我在这里遗漏了什么?

编辑 - 更多信息

要添加更多信息,我正在使用我编写的以下程序来尝试检测最少的日志记录,也许我在这里出了点问题...

/*
    Example Usage...

    EXEC sp_GetLogUseStats
   @Sql = '
      INSERT INTO PostsDestination
      SELECT TOP 500000 * FROM Posts ORDER BY Id ',
   @Schema = 'dbo',
   @Table = 'PostsDestination',
   @ClearData = 1

*/

CREATE PROCEDURE [dbo].[sp_GetLogUseStats]
(   
   @Sql NVARCHAR(400),
   @Schema NVARCHAR(20),
   @Table NVARCHAR(200),
   @ClearData BIT = 0
)
AS

IF @ClearData = 1
   BEGIN
   TRUNCATE TABLE PostsDestination
   END

/*Checkpoint to clear log (Assuming Simple/Bulk Recovery Model*/
CHECKPOINT  

/*Snapshot of logsize before query*/
CREATE TABLE #BeforeLogUsed(
   [Db] NVARCHAR(100),
   LogSize NVARCHAR(30),
   Used NVARCHAR(50),
   Status INT
)
INSERT INTO #BeforeLogUsed
EXEC('DBCC SQLPERF(logspace)')

/*Run Query*/
EXECUTE sp_executesql @SQL

/*Snapshot of logsize after query*/
CREATE TABLE #AfterLLogUsed(    
   [Db] NVARCHAR(100),
   LogSize NVARCHAR(30),
   Used NVARCHAR(50),
   Status INT
)
INSERT INTO #AfterLLogUsed
EXEC('DBCC SQLPERF(logspace)')

/*Return before and after log size*/
SELECT 
   CAST(#AfterLLogUsed.Used AS DECIMAL(12,4)) - CAST(#BeforeLogUsed.Used AS DECIMAL(12,4)) AS LogSpaceUsersByInsert
FROM 
   #BeforeLogUsed 
   LEFT JOIN #AfterLLogUsed ON #AfterLLogUsed.Db = #BeforeLogUsed.Db
WHERE 
   #BeforeLogUsed.Db = DB_NAME()

/*Get list of affected indexes from insert query*/
SELECT 
   @Schema + '.' + so.name + '.' +  si.name AS IndexName
INTO 
   #IndexNames
FROM 
   sys.indexes si 
   JOIN sys.objects so ON si.[object_id] = so.[object_id]
WHERE 
   si.name IS NOT NULL
   AND so.name = @Table
/*Insert Record For Heap*/
INSERT INTO #IndexNames VALUES(@Schema + '.' + @Table)

/*Get log recrod sizes for heap and/or any indexes*/
SELECT 
   AllocUnitName,
   [operation], 
   AVG([log record length]) AvgLogLength,
   SUM([log record length]) TotalLogLength,
   COUNT(*) Count
INTO #LogBreakdown
FROM 
   fn_dblog(null, null) fn
   INNER JOIN #IndexNames ON #IndexNames.IndexName = allocunitname
GROUP BY 
   [Operation], AllocUnitName
ORDER BY AllocUnitName, operation

SELECT * FROM #LogBreakdown
SELECT AllocUnitName, SUM(TotalLogLength)  TotalLogRecordLength 
FROM #LogBreakdown
GROUP BY AllocUnitName
Run Code Online (Sandbox Code Playgroud)

使用以下代码插入没有索引和 TABLOCK 的堆...

EXEC sp_GetLogUseStats
   @Sql = '
      INSERT INTO PostsDestination
      SELECT * FROM Posts ORDER BY Id ',
   @Schema = 'dbo',
   @Table = 'PostsDestination',
   @ClearData = 1
Run Code Online (Sandbox Code Playgroud)

我得到这些结果

在此处输入图片说明

在 0.0024mb 日志文件增长时,日志记录大小非常小,而且很少,我很高兴这是使用最少的日志记录。

如果我然后在 id 上创建一个非聚集索引...

CREATE INDEX ndx_PostsDestination_Id ON PostsDestination(Id)
Run Code Online (Sandbox Code Playgroud)

然后再次运行我的相同插入...

在此处输入图片说明

我不仅没有在非聚集索引上获得最少的日志记录,而且我还在堆上丢失了它。在做了一些更多的测试之后,似乎如果我让 ID 聚集它会最少记录但从我读过的 2016+ 应该最少记录到使用 Tablock 时的非聚集索引的堆。

最终编辑

我已在SQL Server UserVoice上向 Microsoft 报告了该行为,如果收到回复,我将进行更新。我还写了我无法在https://gavindraper.com/2018/05/29/SQL-Server-Minimal-Logging-Inserts/上工作的最小日志场景的完整细节

Pau*_*ite 12

我可以使用 Stack Overflow 2010 数据库在 SQL Server 2017 上重现您的结果,但不能(所有)您的结论。

对具有非聚集索引的堆使用with时,对堆的最小日志记录不可用,这是出乎意料的。我的猜测是不能同时使用(堆)和(b-tree)支持批量加载。只有 Microsoft 能够确认这是一个错误还是设计使然。INSERT...SELECTTABLOCKINSERT...SELECTRowsetBulkFastLoadContext

堆上的非聚集索引最少记录(假设 TF610 已打开,或使用 SQL Server 2016+,启用FastLoadContext),但有以下注意事项:

  • 只有插入到新分配页面的行才会被最低限度地记录。
  • 如果索引在操作开始时为空,则不会最少记录添加到第一个索引页的行。

为非LOP_INSERT_ROWS聚集索引显示的 497个条目对应于索引的第一页。由于索引事先是空的,这些行被完全记录。其余的行都是最少记录的。如果已记录的跟踪标志 692 已启用 (2016+) 禁用FastLoadContext,则所有非聚集索引行都将被最低限度地记录。


我发现,最小记录被施加两个使用堆和当批量加载相同的表(具有索引)非聚集索引BULK INSERT从一个文件:

BULK INSERT dbo.PostsDestination
FROM 'D:\SQL Server\Posts.bcp'
WITH (TABLOCK, DATAFILETYPE = 'native');
Run Code Online (Sandbox Code Playgroud)

我注意到这一点是为了完整性。批量加载使用INSERT...SELECT不同的代码路径,因此行为不同的事实并非完全出乎意料。


有关使用和使用最小日志记录的完整详细信息,请参阅我在 SQLPerformance.com 上的三部分系列:RowsetBulkFastLoadContextINSERT...SELECT

  1. 使用 INSERT…SELECT 最小化日志记录到堆表
  2. 使用 INSERT…SELECT 最小化日志记录到空聚簇表
  3. 使用 INSERT…SELECT 和快速加载上下文的最小日志记录

您博客文章中的其他场景

评论已关闭,所以我将在这里简要介绍这些。

带有跟踪 610 或 2016+ 的空聚集索引

使用FastLoadContext不带TABLOCK. 唯一完全记录的行是那些插入到第一页的行,因为在事务开始时聚集索引是空的。

带有数据和跟踪的聚集索引 610 或 2016+

这也是使用FastLoadContext. 添加到现有页面的行完全记录,其余的记录最少。

带有非聚集索引和 TABLOCK 的聚集索引或跟踪 610/SQL 2016+

FastLoadContext只要非聚集索引由单独的操作员维护,DMLRequestSort设置为 true,并且满足我的帖子中列出的其他条件,也可以使用最少的日志记录。


pac*_*ely 3

下面的文档很旧,但仍然值得一读。

在 SQL 2016 中,跟踪标志 610 和 ALLOW_PAGE_LOCKS 默认情况下处于打开状态,但有人可能已禁用它们。

数据加载性能指南

(3) 根据优化器选择的计划,表上的非聚集索引可以是完全记录的,也可以是最小记录的。

SELECT 语句可能是问题所在,因为您有 TOP 和 ORDER BY。您以与索引不同的顺序将数据插入表中,因此 SQL 可能会在后台执行大量排序。

更新2

您实际上可能会得到最少的日志记录。当 TraceFlag 610 ON 时,日志的行为有所不同,SQL 将在日志中保留足够的空间,以便在出现问题时执行回滚,但实际上不会使用日志。

这可能正在计算保留(未使用)的空间

EXEC('DBCC SQLPERF(logspace)')
Run Code Online (Sandbox Code Playgroud)

此代码将“保留”与“使用”分开

SELECT
    database_transaction_log_bytes_used
    ,database_transaction_log_bytes_reserved
    ,*
FROM sys.dm_tran_database_transactions 
WHERE database_id = DB_ID()
Run Code Online (Sandbox Code Playgroud)

我认为最小日志记录(就 Microsoft 而言)实际上是在日志上执行最少的 IO,而不是保留多少日志。

看看这个链接

更新1

尝试使用 TABLOCKX 而不是 TABLOCK。使用 Tablock,您仍然拥有共享锁,因此 SQL 可能会记录日志,以防另一个进程启动。

TABLOCK 可能需要与 HOLDLOCK 结合使用。这会强制执行 Tablock,直到您的交易结束。

还要在源表 [Posts] 上加锁,可能会发生日志记录,因为在事务发生时源表可能会发生更改。当源不是 SQL 表时,Paul White 实现了最少的日志记录。