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


14

Я тестую мінімальні вставки для реєстрації в різних сценаріях, і з того, що я прочитав INSERT INTO SELECT в купу з некластеризованим індексом за допомогою TABLOCK та SQL Server 2016+, слід мінімально увійти, однак у моєму випадку при цьому я отримую повна лісозаготівля. Моя база даних є в простій моделі відновлення, і я успішно отримую мінімально зареєстровані вставки на купі без індексів і TABLOCK.

Я використовую стару резервну копію бази даних переповнення стека, щоб перевірити і створив репліку таблиці "Повідомлення" з наступною схемою ...

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 для мінімального входу до індексованих таблиць, я також намагався налаштувати це, але все ще не радість.

Я здогадуюсь, що мені щось тут не вистачає?

EDIT - Детальніше

Щоб додати більше інформації, я використовую наступну процедуру, яку я написав, щоб спробувати виявити мінімальний журнал, можливо, тут щось не так ...

/*
    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+, слід мінімально увійти до купи з некластеризованим індексом, коли використовується табло.

Заключні EDIT :

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


3
Пол Білий має тут корисну відповідь .
Ерік Дарлінг

Відповіді:


12

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

Мінімальна протоколювання в купі недоступна при використанні INSERT...SELECTз TABLOCKкупи з некластерного індексом, який є несподіваним . Моя здогадка, не INSERT...SELECTможна підтримувати об'ємні навантаження за допомогою RowsetBulk(купи) одночасно з FastLoadContext(b-tree). Тільки 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 може бути проблемою, оскільки у вас є ТОП і ЗАМОВЛЕННЯ ПО. Ви вставляєте дані в таблицю в іншому порядку до індексу, тому SQL, можливо, проводить багато сортування у фоновому режимі.

ОНОВЛЕННЯ 2

Можливо, ви отримуєте мінімальний журнал. З TraceFlag 610 ON Журнал веде себе по-різному, 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()

Я припускаю, що Мінімальний журнал (що стосується Майкрософт) - це насправді про виконання найменшої кількості вводу-виводу в журналі, а не про те, скільки журналу зарезервовано.

Подивіться за цим посиланням .

ОНОВЛЕННЯ 1

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

TABLOCK, можливо, потрібно використовувати разом із HOLDLOCK. Це застосовує функцію Tablock до кінця транзакції.

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

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.