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


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/


Ответы:


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, и другие условия, изложенные в моих сообщениях , выполнены.


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.

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.