为什么插入索引表时无法获得最少的日志记录


14

我正在测试在不同情况下的最小日志记录插入量,以及从我读过的使用TABLOCK和SQL Server 2016+将具有非聚集索引的INSERT INTO SELECT读取到堆中的最少记录,但是在我这样做的情况下,完整日志记录。我的数据库处于简单恢复模型中,并且在没有索引和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)

然后,我尝试将posts表复制到该表中。

INSERT INTO PostsDestination WITH(TABLOCK)
SELECT * FROM Posts ORDER BY Id 

通过查看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

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

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

我得到这些结果

在此处输入图片说明

日志文件的增长为0.0024mb,日志记录的大小非常小,并且其中很少有人感到高兴,因为这是使用最少的日志记录。

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

CREATE INDEX ndx_PostsDestination_Id ON PostsDestination(Id)

然后再次运行我的同一个插件...

在此处输入图片说明

我不仅在非聚集索引上没有获得最少的日志记录,而且在堆上也丢失了它。经过更多测试之后,看来我是否使ID集群化,它的日志记录最少,但是从我所读到的2016+开始,应该在使用tablock时将日志最小记录到具有非聚集索引的堆中。

最后编辑

我已在SQL Server UserVoice上向Microsoft报告了此行为,如果得到响应,它将进行更新。我还在https://gavindraper.com/2018/05/29/SQL-Server-Minimal-Logging-Inserts/上写了我无法使用的最小日志方案的完整详细信息


3
保罗·怀特(Paul White)在这里有一个有用的相关答案
埃里克·达林

Answers:


12

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

最少日志记录堆中使用时是不可用INSERT...SELECTTABLOCK与一个非聚集索引,这是一种堆意外。我的猜测是INSERT...SELECT不能同时使用(b-tree)支持RowsetBulk(heap)进行批量加载FastLoadContext。只有Microsoft才能确认这是错误还是设计使然。

使用以下警告至少记录了堆上的非聚集索引(假定TF610已打开,或使用SQL Server 2016+,启用了):FastLoadContext

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

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


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

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

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


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

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

您博客文章中的其他情况

评论已关闭,因此我将在这里简要介绍一下。

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

这是使用最小日志记录FastLoadContextTABLOCK。完全记录的唯一行是插入到第一页的行,因为在事务开始时聚簇索引为空。

具有数据和跟踪610或2016+的聚簇索引

这也是使用最少记录的FastLoadContext。添加到现有页面的行将被完整记录,其余部分将被最小记录。

具有非聚集索引和TABLOCK或Trace 610 / SQL 2016+的聚集索引

FastLoadContext只要非聚集索引由一个单独的运算符维护DMLRequestSort,并将其设置为true,并且满足我的帖子中列出的其他条件,则也可以使用来最少记录该日志。


2

以下文档虽然陈旧,但是仍然是一本好书。

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

数据加载性能指南

(3)根据优化程序选择的计划,表上的非聚集索引可能是完整记录或最小记录的。

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

更新2

您可能实际上正在获得最小化日志记录。启用TraceFlag 610时,日志的行为有所不同,如果出现问题,SQL将在日志中保留足够的空间以执行回滚,但实际上不会使用日志。

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

EXEC('DBCC SQLPERF(logspace)')

此代码从使用中拆分为保留

SELECT
    database_transaction_log_bytes_used
    ,database_transaction_log_bytes_reserved
    ,*
FROM sys.dm_tran_database_transactions 
WHERE database_id = DB_ID()

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

看一下这个链接

更新1

尝试使用TABLOCKX代替TABLOCK。使用Tablock,您仍然具有共享锁,因此,如果另一个进程启动,则SQL可能正在记录日志。

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

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

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.