web-dev-qa-db-fra.com

Pourquoi ALTER COLUMN to NOT NULL provoque une croissance massive des fichiers journaux?

J'ai une table avec 64m de lignes prenant 4,3 Go sur le disque pour ses données.

Chaque ligne représente environ 30 octets de colonnes entières, plus une variable NVARCHAR(255) colonne pour le texte.

J'ai ajouté une colonne NULLABLE avec le type de données Datetimeoffset(0).

J'ai ensuite mis à jour cette colonne pour chaque ligne et je me suis assuré que toutes les nouvelles insertions placent une valeur dans cette colonne.

Une fois qu'il n'y avait aucune entrée NULL, j'ai alors exécuté cette commande pour rendre mon nouveau champ obligatoire:

ALTER TABLE tblCheckResult 
ALTER COLUMN [dtoDateTime] [datetimeoffset](0) NOT NULL

Le résultat a été une ÉNORME croissance de la taille du journal des transactions - de 6 Go à plus de 36 Go jusqu'à ce qu'il manque d'espace!

Quelqu'un a-t-il une idée de ce que fait SQL Server 2008 R2 pour que cette simple commande entraîne une croissance énorme?

56
PapillonUK

Lorsque vous modifiez une colonne en NOT NULL, SQL Server doit toucher chaque page unique, même s'il n'y a pas de valeurs NULL. En fonction de votre facteur de remplissage, cela pourrait entraîner de nombreux fractionnements de page. Chaque page qui est touchée, bien sûr, doit être enregistrée, et je soupçonne en raison des divisions que deux modifications peuvent devoir être enregistrées pour de nombreuses pages. Comme tout est fait en un seul passage, le journal doit tenir compte de toutes les modifications afin que, si vous appuyez sur Annuler, il sache exactement quoi annuler.


Un exemple. Table simple:

DROP TABLE dbo.floob;
GO

CREATE TABLE dbo.floob
(
  id INT IDENTITY(1,1) NOT NULL PRIMARY KEY CLUSTERED, 
  bar INT NULL
);

INSERT dbo.floob(bar) SELECT NULL UNION ALL SELECT 4 UNION ALL SELECT NULL;

ALTER TABLE dbo.floob ADD CONSTRAINT df DEFAULT(0) FOR bar

Maintenant, regardons les détails de la page. Nous devons d'abord savoir avec quelle page et DB_ID nous avons affaire. Dans mon cas, j'ai créé une base de données appelée foo, et le DB_ID se trouvait être 5.

DBCC TRACEON(3604, -1);
DBCC IND('foo', 'dbo.floob', 1);
SELECT DB_ID();

La sortie a indiqué que j'étais intéressé par la page 159 (la seule ligne dans DBCC IND sortie avec PageType = 1).

Maintenant, regardons certains détails de la page que nous parcourons le scénario de l'OP.

DBCC PAGE(5, 1, 159, 3);

enter image description here

UPDATE dbo.floob SET bar = 0 WHERE bar IS NULL;    
DBCC PAGE(5, 1, 159, 3);

enter image description here

ALTER TABLE dbo.floob ALTER COLUMN bar INT NOT NULL;
DBCC PAGE(5, 1, 159, 3);

enter image description here

Maintenant, je n'ai pas toutes les réponses à cela, car je ne suis pas un gars profondément interne. Mais il est clair que - alors que l'opération de mise à jour et l'ajout de la contrainte NOT NULL écrivent indéniablement sur la page - cette dernière le fait d'une manière entièrement différente. Il semble en fait changer la structure de l'enregistrement, plutôt que de simplement jouer avec des bits, en remplaçant la colonne nullable par une colonne non nullable. Pourquoi cela doit-il le faire, je ne suis pas sûr - une bonne question pour l'équipe du moteur de stockage , je suppose. Je crois que SQL Server 2012 gère beaucoup mieux certains de ces scénarios, FWIW - mais je n'ai pas encore fait de test exhaustif.

48
Aaron Bertrand

Lors de l'exécution de la commande

ALTER COLUMN ... NOT NULL

Cela semble être implémenté comme une opération d'ajout de colonne, de mise à jour et de suppression de colonne.

  • Une nouvelle ligne est insérée dans sys.sysrscols Pour représenter une nouvelle colonne. Le bit status pour 128 Est défini, indiquant que la colonne n'autorise pas NULLs
  • Une mise à jour est effectuée sur chaque ligne du tableau en définissant la nouvelle valeur de colonne sur celle de l'ancienne valeur de colonne. Si les versions "avant" et "après" de la ligne sont exactement les mêmes, cela n'entraîne aucune écriture dans le journal des transactions, sinon la mise à jour est enregistrée.
  • La colonne d'origine est marquée comme supprimée (il s'agit d'un changement de métadonnées uniquement dans sys.sysrscols. rscolid mis à jour en un grand entier et status bit 2 réglé sur indiqué supprimé)
  • L'entrée dans sys.sysrscols Pour la nouvelle colonne est modifiée pour lui donner le rscolid de l'ancienne colonne.

L'opération qui a le potentiel pour provoquer beaucoup de journalisation est le UPDATE de toutes les lignes du tableau mais cela ne signifie pas que cela va se produisent toujours . Si les images "avant" et "après" de la ligne sont identiques, cela sera traité comme un mise à jour non mise à jour et ne sera pas enregistré à partir de mes tests jusqu'à présent.

Ainsi, l'explication de la raison pour laquelle vous obtenez beaucoup de journalisation dépendra de la raison pour laquelle les versions "avant" et "après" de la ligne ne sont pas identiques.

Pour les colonnes de longueur variable stockées au format FixedVar, j'ai constaté que la définition de NOT NULL Entraîne toujours un changement dans la ligne qui doit être enregistrée. Le nombre de colonnes et le nombre de colonnes de longueur variable sont tous deux incrémentés et la nouvelle colonne est ajoutée à la fin de la section de longueur variable qui duplique les données.

datetimeoffset(0) est de longueur fixe cependant et pour les colonnes de longueur fixe stockées au format FixedVar, les anciennes et les nouvelles colonnes semblent toutes deux avoir le même emplacement dans la partie de données de longueur fixe de la ligne et comme ils ont tous deux la même longueur et la même valeur les versions "avant" et "après" de la ligne sont les mêmes . Cela peut être vu dans la réponse de @ Aaron. Les deux versions de la ligne avant et après le ALTER TABLE dbo.floob ALTER COLUMN bar INT NOT NULL; Sont

0x10000c00 01000000 00000000 020000

Ce n'est pas enregistré.

Logiquement, d'après ma description des événements, la ligne devrait en fait être différente ici car le nombre de colonnes 02 Devrait être augmenté à 03, Mais aucun changement de ce genre ne se produit réellement dans la pratique.

Certaines raisons possibles expliquant pourquoi cela peut se produire dans une colonne de longueur fixe sont

  • Si la colonne a été déclarée à l'origine comme SPARSE, la nouvelle colonne sera stockée dans une partie différente de la ligne par rapport à l'original, ce qui entraînera la différence entre les images de la ligne avant et après.
  • Si vous utilisez l'une des options de compression, les versions avant et après de la ligne seront différentes car la section de comptage de colonnes dans la matrice de CD est incrémentée.
  • Sur les bases de données avec l'une des options d'isolement de cliché activée, les informations de version de chaque ligne sont mises à jour (@SQL Kiwi souligne que cela peut également se produire dans les bases de données sans SI activé comme décrit ici ).
  • Il se peut qu'une opération ALTER TABLE Précédente ait été implémentée en tant que modification de métadonnées uniquement et n'a pas encore été appliquée à la ligne. Par exemple, si une nouvelle colonne de longueur variable nullable a été ajoutée, elle est initialement appliquée en tant que modification des métadonnées uniquement et elle n'est réellement écrite dans les lignes que lors de leur prochaine mise à jour (l'écriture qui se produit réellement dans cette dernière instance est simplement une mise à jour de la section de comptage de colonnes et le NULL_BITMAP en tant que colonne NULLvarchar à la fin de la ligne ne prend pas de place)
32
Martin Smith

J'ai rencontré le même problème concernant une table ayant 200 000 000 lignes. Au départ, j'ai ajouté la colonne nullable, puis mis à jour toutes les lignes et finalement modifié la colonne en NOT NULL via un ALTER TABLE ALTER COLUMN déclaration. Cela a entraîné deux énormes transactions faisant exploser le fichier journal de manière incroyable (170 Go de croissance).

Le moyen le plus rapide que j'ai trouvé était le suivant:

  1. Ajouter la colonne en utilisant une valeur par défaut

    ALTER TABLE table1 ADD column1 INT NOT NULL DEFAULT (1)
    
  2. Supprimez la contrainte par défaut à l'aide de SQL dynamique car la contrainte n'a pas été nommée auparavant:

    DECLARE 
        @constraint_name SYSNAME,
        @stmt NVARCHAR(510);
    
    SELECT @CONSTRAINT_NAME = DC.NAME
    FROM SYS.DEFAULT_CONSTRAINTS DC
    INNER JOIN SYS.COLUMNS C
        ON DC.PARENT_OBJECT_ID = C.OBJECT_ID
        AND DC.PARENT_COLUMN_ID = C.COLUMN_ID
    WHERE
        PARENT_OBJECT_ID = OBJECT_ID('table1')
        AND C.NAME = 'column1';
    

Le temps d'exécution est passé de> 30 minutes à 10 minutes, y compris la réplication des modifications via la réplication transactionnelle. J'exécute une installation SQL Server 2008 (SP2).

5
Fritz

J'ai exécuté le test suivant:

create table tblCheckResult(
        ColID   int identity
    ,   dtoDateTime Datetimeoffset(0) null
    )

 go

insert into tblCheckResult (dtoDateTime)
select getdate()
go 10000

checkpoint 

ALTER TABLE tblCheckResult 
ALTER COLUMN [dtoDateTime] [datetimeoffset](0) NOT NULL

select * from fn_dblog(null,null)

Je crois que cela a à voir avec l'espace réservé que contient le journal au cas où vous annuleriez la transaction. Regardez dans la fonction fn_dblog dans la colonne 'Log Reserve' pour la ligne LOP_BEGIN_XACT et voyez combien d'espace il essaie de réserver.

2
Keith Tate