Почему я не получаю минимальное ведение журнала при вставке в индексированные таблицы

14

Я тестирую минимальное количество вставок в журналы в разных сценариях, и из того, что я прочитал, 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)

Затем я пытаюсь скопировать таблицу сообщений в эту таблицу ...

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,0024 МБ, очень небольших размерах записи журнала и очень немногих из них я рад, что это использует минимальное ведение журнала.

Если я тогда создаю некластеризованный индекс по id ...

CREATE INDEX ndx_PostsDestination_Id ON PostsDestination(Id)

Затем запустите мою же вставку снова ...

введите описание изображения здесь

Мало того, что я не получаю минимальное ведение журнала для некластеризованного индекса, но я также потерял его в куче. После еще нескольких тестов кажется, что если я сделаю ID кластеризованным, он будет вести минимальный журнал, но из того, что я прочитал в 2016+, следует минимально регистрироваться в куче с некластеризованным индексом при использовании таблока.

ЗАКЛЮЧИТЕЛЬНОЕ РЕДАКТИРОВАНИЕ :

Я сообщил Microsoft о поведении на SQL Server UserVoice и обновлю , если получу ответ. Я также написал полную информацию о сценариях минимального журнала, которые я не смог заставить работать по адресу https://gavindraper.com/2018/05/29/SQL-Server-Minimal-Logging-Inserts/

Gavin
источник
3
У Пола Уайта есть полезный связанный ответ здесь .
Эрик Дарлинг

Ответы:

12

Я могу воспроизвести ваши результаты на SQL Server 2017, используя базу данных Stack Overflow 2010, но не (все) ваши выводы.

Минимальное протоколирование в куче недоступна при использовании INSERT...SELECTс TABLOCKкучи с некластерного индексом, который является неожиданным . Я предполагаю, что не INSERT...SELECTможет поддерживать массовые загрузки, используя RowsetBulk(куча) в то же время, как FastLoadContext(b-дерево). Только 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используются разные пути кода, поэтому факт поведения отличается не совсем неожиданно.


Для получения полной информации о минимальном ведении журнала с использованием RowsetBulkи FastLoadContextс INSERT...SELECTмоими сериями из трех частей на SQLPerformance.com:

  1. Минимальное ведение журнала с помощью INSERT… SELECT в таблицах кучи
  2. Минимальное ведение журнала с помощью INSERT… SELECT в пустых кластеризованных таблицах
  3. Минимальная регистрация с INSERT… SELECT и контекстом быстрой загрузки

Другие сценарии из вашего блога

Комментарии закрыты, поэтому я кратко остановлюсь здесь.

Пустой кластерный индекс с трассировкой 610 или 2016+

Это минимально зарегистрировано, используя FastLoadContextбез TABLOCK. Единственными полностью зарегистрированными строками являются те, которые вставлены на первую страницу, потому что кластерный индекс был пуст в начале транзакции.

Кластерный индекс с данными и трассировкой 610 ИЛИ 2016+

Это также минимально зарегистрировано, используя FastLoadContext. Строки, добавленные на существующую страницу, полностью регистрируются, остальные - минимально.

Кластерный индекс с некластерными индексами и TABLOCK или Trace 610 / SQL 2016+

Это также может быть минимально зарегистрировано, используя, FastLoadContextпока некластеризованный индекс поддерживается отдельным оператором, DMLRequestSortустановлен в true, и другие условия, изложенные в моих сообщениях , выполнены.

Пол Уайт 9
источник
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) на самом деле связано с наименьшим количеством операций ввода-вывода в журнале, а не с тем, какая часть журнала зарезервирована.

Посмотрите на эту ссылку .

ОБНОВЛЕНИЕ 1

Попробуйте использовать TABLOCKX вместо TABLOCK. С Tablock у вас все еще есть общая блокировка, поэтому SQL может регистрироваться в случае запуска другого процесса.

TABLOCK может потребоваться использовать вместе с HOLDLOCK. Это применяет Таблок до конца вашей транзакции.

Также установите блокировку на исходную таблицу [Posts], регистрация может происходить, потому что исходная таблица может измениться во время вашей транзакции. Пол Уайт достиг минимальной регистрации, когда источником не была таблица SQL.

pacreely
источник