Pourquoi ALTER COLUMN to NOT NULL entraîne-t-il une croissance massive des fichiers journaux?

56

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

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

J'ai ajouté une colonne NULLABLE avec 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 plus d'entrées NULL, j'ai ensuite 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'à épuisement de l'espace!

Quelqu'un a-t-il une idée de ce que fait SQL Server 2008 R2 pour que cette commande simple génère une telle croissance?

PapillonUK
la source
7
SQL Server 2012 Enterprise ajoute la possibilité d'ajouter une NOT NULLcolonne avec une valeur par défaut en tant qu'opération de métadonnées. Voir également "Ajout de colonnes NOT NULL en tant qu'opération en ligne" dans la documentation .
Paul White

Réponses:

48

Lorsque vous modifiez une colonne en NOT NULL, SQL Server doit toucher chaque page, même s'il n'y a pas de valeur NULL. En fonction de votre facteur de remplissage, cela pourrait entraîner beaucoup de fractionnements de pages. Bien sûr, chaque page touchée doit être consignée et je soupçonne, à cause des scissions, que deux modifications doivent être consignées pour plusieurs pages. Comme tout est fait en un seul passage, cependant, le journal doit prendre en compte tous les changements. Par conséquent, si vous appuyez sur Cancel, il sait exactement ce qu'il faut annuler.


Un exemple. Tableau 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 à 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 s'est avéré être 5.

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

La sortie a indiqué que la page 159 m'intéressait (la seule ligne en DBCC INDsortie avec PageType = 1).

Maintenant, regardons quelques détails de pages sélectionnées au fur et à mesure que nous explorons le scénario du PO.

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

entrez la description de l'image ici

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

entrez la description de l'image ici

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

entrez la description de l'image ici

Maintenant, je n'ai pas toutes les réponses à cette question, car je ne suis pas un type interne profond. Mais il est clair que, même si 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 totalement différente. Cela semble en fait changer la structure de l'enregistrement, plutôt que de simplement manipuler des bits, en permutant la colonne nullable par une colonne non nullable. Je ne sais pas trop pourquoi , mais je suppose que c'est une bonne question pour l'équipe du moteur de stockage . Je pense que SQL Server 2012 gère beaucoup mieux certains de ces scénarios, FWIW - mais je n'ai pas encore procédé à des tests exhaustifs.

Aaron Bertrand
la source
4
Ce comportement a considérablement changé dans les versions ultérieures de SQL Server. J'ai vérifié 2016 RC2 et découvert que pour ce scénario exact et 1 million de lignes dans la table, seuls 29 enregistrements de journal sont générés lors du passage de NULL à NOT NULL si toutes les valeurs étaient déjà spécifiées pour la colonne.
Endrju
32

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 ou de suppression de colonne.

  • Une nouvelle ligne est insérée dans sys.sysrscolspour représenter une nouvelle colonne. Le statusbit pour 128se mettre en indiquant la colonne ne permet pas de NULLs
  • Une mise à jour est effectuée sur chaque ligne de la table en définissant la nouvelle valeur columnn sur celle de l'ancienne valeur de colonne. Si les versions "avant" et "après" de la ligne sont exactement les mêmes, cela ne provoque 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 sys.sysrscols. rscolidMis à jour en un grand entier et le statusbit 2 activé).
  • L'entrée dans sys.sysrscolspour la nouvelle colonne est modifiée pour lui donner le rscolidde l'ancienne colonne.

L'opération susceptible de générer beaucoup de journalisation est la UPDATEde toutes les lignes de la table, mais cela ne signifie pas que cela se produira toujours . Si les images "avant" et "après" de la ligne sont identiques, cela sera traité comme une mise à jour sans mise à jour et ne sera pas enregistré depuis mes tests.

L'explication de la raison pour laquelle vous obtenez beaucoup de journalisation dépendra donc du fait que les versions "avant" et "après" de la ligne ne sont pas exactement les mêmes.

Pour les colonnes de longueur variable stockées au FixedVarformat, j'ai constaté que ce paramètre NOT NULLentraînait toujours une modification de la ligne à consigner. 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 dupliquant les données.

datetimeoffset(0)Toutefois, sa longueur est fixe et pour les colonnes de longueur fixe stockées au FixedVarformat, les anciennes et les nouvelles colonnes semblent se voir attribuer le même créneau dans la partie de données de longueur fixe de la ligne et, comme elles ont la même longueur et la même valeur, les "avant" et les versions "après" de la ligne sont les mêmes . Ceci 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

Ceci n'est pas connecté.

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

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

  • Si la colonne était initialement déclarée comme telle, SPARSEla nouvelle colonne serait stockée dans une autre partie de la ligne que la colonne d'origine, ce qui aurait pour effet de rendre différentes les images de lignes 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 des colonnes du tableau de CD est incrémentée.
  • Sur les bases de données pour lesquelles l'une des options d'isolation de capture instantanée est activée, les informations de version de chaque ligne sont mises à jour (@SQL Kiwi indique que cela peut également se produire dans des bases de données sans SI activé, comme décrit ici ).
  • Il est possible qu'une ALTER TABLEopération antérieure mise en œuvre sous la forme d'une modification de métadonnées uniquement et n'ayant pas encore été appliquée à la ligne. Par exemple, si une nouvelle colonne de longueur variable nullable a été ajoutée, elle est appliquée à l'origine en tant que métadonnées uniquement. Elle est en fait uniquement écrite dans les lignes lors de leur prochaine mise à jour (l'écriture qui se produit réellement dans cette dernière instance n'est que des mises à jour). la section de comptage de colonne et l' NULL_BITMAPune NULL varcharcolonne à l'extrémité de la rangée ne prend pas de place)
Martin Smith
la source
5

J'ai rencontré le même problème concernant une table ayant 200.000.000 lignes. Initialement, j'ai ajouté la colonne nullable, puis mis à jour toutes les lignes et enfin modifié la colonne en NOT NULLvia une ALTER TABLE ALTER COLUMNinstruction. Cela a entraîné deux transactions énormes faisant exploser le fichier journal incroyablement (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 encore été nommée:

    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 de SQL Server 2008 (SP2).

Fritz
la source
2

J'ai couru 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 pense que cela a à voir avec l'espace réservé que le journal conserve, au cas où vous annuleriez la transaction. Recherchez dans la fonction fn_dblog la colonne "Log Reserve" pour la ligne LOP_BEGIN_XACT et voyez combien d'espace elle tente de réserver.

Keith Tate
la source
Si vous essayez, select * FROM fn_dblog(null, null) where AllocUnitName='dbo.tblCheckResult' AND Operation = 'LOP_MODIFY_ROW'vous pouvez voir les mises à jour 10000 lignes.
Martin Smith
-2

Le comportement de cette opération est différent dans SQL Server 2012. Voir http://rusanu.com/2011/07/13/online-non-null-with-values-column-add-in-sql-server-11/

Le nombre d'enregistrements de journal générés pour les versions de SQL Server 2008 R2 et inférieures sera considérablement plus élevé que le nombre d'enregistrements de journal pour SQL Server 2012.

DépannageSQL
la source
2
La question est de savoir pourquoi la modification d’une colonne existante NOT NULLentraîne la journalisation. La modification de 2012 concerne l'ajout d'une nouvelle NOT NULLcolonne avec une valeur par défaut.
Martin Smith