Pourquoi ne reçois-je pas une journalisation minimale lors de l'insertion dans des tables indexées


14

Je teste des insertions de journalisation minimales dans différents scénarios et à partir de ce que j'ai lu INSERT INTO SELECT dans un tas avec un index non clusterisé à l'aide de TABLOCK et SQL Server 2016+ devrait se connecter de manière minimale, mais dans mon cas, lorsque je fais cela, je reçois journalisation complète. Ma base de données est dans le modèle de récupération simple et j'ai réussi à obtenir des insertions enregistrées de manière minimale sur un tas sans index et TABLOCK.

J'utilise une ancienne sauvegarde de la base de données Stack Overflow pour tester et j'ai créé une réplique de la table Posts avec le schéma suivant ...

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)

J'essaie alors de copier le tableau des posts dans ce tableau ...

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

En regardant fn_dblog et l'utilisation du fichier journal, je peux voir que je n'obtiens pas de journalisation minimale à partir de cela. J'ai lu que les versions avant 2016 nécessitent l'indicateur de trace 610 pour se connecter de manière minimale aux tables indexées, j'ai également essayé de définir cela, mais toujours pas de joie.

Je suppose que je manque quelque chose ici?

EDIT - Plus d'informations

Pour ajouter plus d'informations, j'utilise la procédure suivante que j'ai écrite pour essayer de détecter une journalisation minimale, peut-être que j'ai quelque chose de mal ici ...

/*
    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

Insertion dans un tas sans index et TABLOCK à l'aide du code suivant ...

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

J'obtiens ces résultats

entrez la description de l'image ici

À une croissance de fichier journal de 0,0024 Mo, de très petites tailles d'enregistrement de journal et très peu d'entre eux, je suis heureux que cela utilise une journalisation minimale.

Si je crée ensuite un index non clusterisé sur id ...

CREATE INDEX ndx_PostsDestination_Id ON PostsDestination(Id)

Ensuite, exécutez à nouveau mon même insert ...

entrez la description de l'image ici

Non seulement je ne reçois pas de journalisation minimale sur l'index non cluster, mais je l'ai également perdu sur le tas. Après avoir fait plus de tests, il semble que si je fais un ID en cluster, il se connecte de manière minimale, mais d'après ce que j'ai lu 2016+, il devrait se connecter de manière minimale à un tas avec un index non cluster lorsque le tablock est utilisé.

MODIFICATION FINALE :

J'ai signalé le comportement à Microsoft sur UserVoice de SQL Server et je mettrai à jour si j'obtiens une réponse. J'ai également écrit tous les détails des scénarios de journalisation minimaux que je n'ai pas pu travailler sur https://gavindraper.com/2018/05/29/SQL-Server-Minimal-Logging-Inserts/


Réponses:


12

Je peux reproduire vos résultats sur SQL Server 2017 à l'aide de la base de données Stack Overflow 2010, mais pas (toutes) vos conclusions.

La journalisation minimale vers le segment de mémoire n'est pas disponible lors de l'utilisation INSERT...SELECTavec TABLOCKavec un segment de mémoire avec un index non cluster, ce qui est inattendu . Je suppose que INSERT...SELECTne peut pas prendre en charge les chargements en masse en utilisant RowsetBulk(tas) en même temps que FastLoadContext(b-tree). Seul Microsoft serait en mesure de confirmer s'il s'agit d'un bogue ou par conception.

L' index non cluster sur le tas est enregistré de manière minimale (en supposant que TF610 est activé ou que SQL Server 2016+ est utilisé, l'activation FastLoadContext) avec les mises en garde suivantes:

  • Seules les lignes insérées dans les pages nouvellement allouées sont enregistrées de façon minimale.
  • Les lignes ajoutées à la première page d'index ne sont pas enregistrées de façon minimale, si l'index était vide au début de l'opération.

Les 497 LOP_INSERT_ROWSentrées affichées pour l'index non cluster correspondent à la première page de l'index. Étant donné que l'index était vide au préalable, ces lignes sont entièrement enregistrées. Les lignes restantes sont toutes enregistrées de façon minimale . Si l'indicateur de trace documenté 692 est activé (2016+) pour désactiver FastLoadContext, toutes les lignes d'index non cluster sont enregistrées de manière minimale.


J'ai constaté qu'une journalisation minimale est appliquée à la fois au tas et à l'index non cluster lors du chargement en bloc de la même table (avec index) à l'aide BULK INSERTd'un fichier:

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

Je note cela pour être complet. Le chargement en bloc à l'aide de INSERT...SELECTchemins de code différents, le fait que les comportements diffèrent n'est pas entièrement inattendu.


Pour plus de détails sur la journalisation minimale en utilisant RowsetBulket FastLoadContextavec, INSERT...SELECTconsultez mes trois séries sur SQLPerformance.com:

  1. Journalisation minimale avec INSERT… SELECT dans les tables de tas
  2. Journalisation minimale avec INSERT… SELECT dans des tables en cluster vides
  3. Journalisation minimale avec INSERT… SELECT et contexte de chargement rapide

Autres scénarios de votre article de blog

Les commentaires sont fermés, je vais donc en parler brièvement ici.

Index cluster vide avec trace 610 ou 2016+

Ceci est minimalement connecté en utilisant FastLoadContextsans TABLOCK. Les seules lignes entièrement enregistrées sont celles insérées dans la première page car l'index cluster était vide au début de la transaction.

Index en cluster avec données et trace 610 OR 2016+

Ceci est également enregistré de façon minimale en utilisant FastLoadContext. Les lignes ajoutées à la page existante sont entièrement enregistrées, les autres sont enregistrées de manière minimale.

Index cluster avec index non cluster et TABLOCK ou Trace 610 / SQL 2016+

Cela peut également être journalisé de manière minimale en utilisant FastLoadContexttant que l'index non cluster est maintenu par un opérateur distinct, DMLRequestSortest défini sur true et que les autres conditions énoncées dans mes messages sont remplies.


2

Le document ci-dessous est ancien mais reste une excellente lecture.

Dans SQL 2016, l'indicateur de trace 610 et ALLOW_PAGE_LOCKS sont activés par défaut, mais quelqu'un peut les avoir désactivés.

Guide des performances de chargement des données

(3) Selon le plan choisi par l'optimiseur, l'index non cluster de la table peut être entièrement ou minimalement journalisé.

L'instruction SELECT peut être le problème car vous avez un TOP et un ORDER BY. Vous insérez des données dans la table dans un ordre différent de celui de l'index, donc SQL peut faire beaucoup de tri en arrière-plan.

MISE À JOUR 2

Vous obtenez peut-être une journalisation minimale. Lorsque TraceFlag 610 est activé, le journal se comporte différemment, SQL réserve suffisamment d'espace dans le journal pour effectuer une restauration en cas de problème, mais n'utilise pas réellement le journal.

Cela compte probablement l'espace réservé (inutilisé)

EXEC('DBCC SQLPERF(logspace)')

Ce code se divise Réservé de Utilisé

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

Je suppose que la journalisation minimale (en ce qui concerne Microsoft) consiste en réalité à effectuer le moins d'E / S sur le journal, et non pas la quantité de journal réservée.

Jetez un œil à ce lien .

MISE À JOUR 1

Essayez d'utiliser TABLOCKX au lieu de TABLOCK. Avec Tablock, vous avez toujours un verrou partagé, donc SQL peut se connecter au cas où un autre processus démarre.

TABLOCK peut devoir être utilisé conjointement avec HOLDLOCK. Cela applique le Tablock jusqu'à la fin de votre transaction.

Mettez également un verrou sur la table source [Posts], la journalisation peut avoir lieu car la table source peut changer pendant que votre transaction est en cours. Paul White a réalisé une journalisation minimale lorsque la source n'était pas une table SQL.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.