web-dev-qa-db-fra.com

ALTER TABLE ... DROP COLUMN est-il vraiment une opération de métadonnées uniquement?

J'ai trouvé plusieurs sources qui indiquent ALTER TABLE ... DROP COLUMN est une opération de métadonnées uniquement.

Source

Comment se peut-il? Les données pendant une DROP COLUMN n'ont-elles pas besoin d'être purgées des index non groupés sous-jacents et des index/tas cluster?

En outre, pourquoi le Microsoft Docs implique-t-il qu'il s'agit d'une opération entièrement journalisée?

Les modifications apportées à la table sont enregistrées et entièrement récupérables. Modifications qui affectent toutes les lignes des grandes tables, telles que la suppression d'une colonne ou, sur certaines éditions de SQL Server, l'ajout d'une colonne NOT NULL avec une valeur par défaut peut prendre beaucoup de temps pour terminer et générer de nombreux enregistrements de journal . Exécutez ces instructions ALTER TABLE avec le même soin que toute instruction INSERT, UPDATE ou DELETE qui affecte de nombreuses lignes.

Comme question secondaire: comment le moteur garde-t-il les colonnes supprimées si les données ne sont pas supprimées des pages sous-jacentes?

11
George.Palacios

Dans certaines circonstances, la suppression d'une colonne peut être une opération de métadonnées uniquement. Les définitions de colonne pour une table donnée ne sont pas incluses dans chaque page où les lignes sont stockées, les définitions de colonne ne sont stockées que dans les métadonnées de la base de données, y compris sys.sysrowsets, sys.sysrscols, etc.

Lors de la suppression d'une colonne qui n'est référencée par aucun autre objet, le moteur de stockage marque simplement la définition de la colonne comme n'étant plus présente en supprimant les détails pertinents de diverses tables système. L'action de supprimer les métadonnées invalide le cache de procédure, nécessitant une recompilation chaque fois qu'une requête référence ultérieurement cette table. Étant donné que la recompilation renvoie uniquement les colonnes qui existent actuellement dans la table, les détails de la colonne supprimée ne sont même jamais demandés; le moteur de stockage ignore les octets stockés dans chaque page pour cette colonne, comme si la colonne n'existe plus.

Lorsqu'une opération DML suivante se produit sur la table, les pages concernées sont réécrites sans les données de la colonne supprimée. Si vous reconstruisez un index cluster ou un segment de mémoire, tous les octets de la colonne supprimée ne sont naturellement pas réécrits sur la page sur le disque. Cela répartit efficacement la charge de chute de la colonne au fil du temps, ce qui la rend moins perceptible.

Dans certaines circonstances, vous ne pouvez pas supprimer une colonne, par exemple lorsque la colonne est incluse dans un index ou lorsque vous avez créé manuellement un objet de statistiques pour la colonne. J'ai écrit un article de blog montrant l'erreur qui est présentée lors de la tentative de modification d'une colonne avec un objet de statistiques créé manuellement. La même sémantique s'applique lors de la suppression d'une colonne - si la colonne est référencée par n'importe quel autre objet, elle ne peut pas simplement être supprimée. L'objet de référence doit d'abord être modifié, puis la colonne peut être supprimée.

C'est assez facile à montrer en regardant le contenu du journal des transactions après avoir supprimé une colonne. Le code ci-dessous crée une table avec une seule colonne de 8 000 caractères longs. Il ajoute une ligne, puis la supprime et affiche le contenu du journal des transactions applicable à l'opération de suppression. Les enregistrements du journal indiquent les modifications apportées à diverses tables système où les définitions de table et de colonne sont stockées. Si les données de colonne étaient réellement supprimées des pages allouées à la table, vous verriez des enregistrements de journal enregistrant les données de page réelles; il n'y a pas de tels enregistrements.

DROP TABLE IF EXISTS dbo.DropColumnTest;
GO
CREATE TABLE dbo.DropColumnTest
(
    rid int NOT NULL
        CONSTRAINT DropColumnTest_pkc
        PRIMARY KEY CLUSTERED
    , someCol varchar(8000) NOT NULL
);

INSERT INTO dbo.DropColumnTest (rid, someCol)
SELECT 1, REPLICATE('Z', 8000);
GO

DECLARE @startLSN nvarchar(25);

SELECT TOP(1) @startLSN = dl.[Current LSN]
FROM sys.fn_dblog(NULL, NULL) dl
ORDER BY dl.[Current LSN] DESC;

DECLARE @a int = CONVERT(varbinary(8), '0x' + CONVERT(varchar(10),      LEFT(@startLSN, 8), 0), 1)
      , @b int = CONVERT(varbinary(8), '0x' + CONVERT(varchar(10), SUBSTRING(@startLSN, 10, 8), 0), 1)
      , @c int = CONVERT(varbinary(8), '0x' + CONVERT(varchar(10),     RIGHT(@startLSN, 4), 0), 1);

SELECT @startLSN = CONVERT(varchar(8), @a, 1) 
    + ':' + CONVERT(varchar(8), @b, 1) 
    + ':' + CONVERT(varchar(8), @c, 1)

ALTER TABLE dbo.DropColumnTest DROP COLUMN someCol;

SELECT *
FROM sys.fn_dblog(@startLSN, NULL)


--modify an existing data row 
SELECT TOP(1) @startLSN = dl.[Current LSN]
FROM sys.fn_dblog(NULL, NULL) dl
ORDER BY dl.[Current LSN] DESC;

SET @a = CONVERT(varbinary(8), '0x' + CONVERT(varchar(10),      LEFT(@startLSN, 8), 0), 1);
SET @b = CONVERT(varbinary(8), '0x' + CONVERT(varchar(10), SUBSTRING(@startLSN, 10, 8), 0), 1);
SET @c = CONVERT(varbinary(8), '0x' + CONVERT(varchar(10),     RIGHT(@startLSN, 4), 0), 1);

SELECT @startLSN = CONVERT(varchar(8), @a, 1) 
    + ':' + CONVERT(varchar(8), @b, 1) 
    + ':' + CONVERT(varchar(8), @c, 1)

UPDATE dbo.DropColumnTest SET rid = 2;

SELECT *
FROM sys.fn_dblog(@startLSN, NULL)

(La sortie est trop grande pour être affichée ici, et dbfiddle.uk ne me permettra pas d'accéder à fn_dblog)

Le premier ensemble de résultats affiche le journal suite à la suppression de la colonne par l'instruction DDL. Le deuxième ensemble de sorties affiche le journal après l'exécution de l'instruction DML où nous mettons à jour la colonne rid. Dans le deuxième jeu de résultats, nous voyons des enregistrements de journal indiquant une suppression par rapport à dbo.DropColumnTest, suivie d'une insertion dans dbo.DropColumnTest. Chaque longueur d'enregistrement de journal est 8116, indiquant que la page réelle a été mise à jour.

Comme vous pouvez le voir sur la sortie de la commande fn_dblog Dans le test ci-dessus, toute l'opération est entièrement enregistrée. Cela vaut pour une récupération simple, ainsi qu'une récupération complète. La terminologie "entièrement enregistré" peut être mal interprétée car la modification des données n'est pas enregistrée. Ce n'est pas ce qui se passe - la modification est enregistrée et peut être entièrement annulée. Le journal est simplement uniquement enregistrant les pages qui ont été touchées, et comme aucune des pages de données de la table n'a été enregistrée par l'opération DDL, les deux DROP COLUMN, Et toute restauration qui pourrait se produire se produira extrêmement rapidement, quelle que soit la taille de la table.

Pour la science , le code suivant videra les pages de données du tableau inclus dans le code ci-dessus, en utilisant DBCC PAGE, Style "3" . Le style "3" indique que nous voulons le en-tête de page plus interprétation détaillée par ligne . Le code utilise un curseur pour afficher les détails de chaque page du tableau, vous pouvez donc vous assurer de ne pas l'exécuter sur une grande table.

DBCC TRACEON(3604); --directs out from DBCC commands to the console, instead of the error log
DECLARE @dbid int = DB_ID();
DECLARE @fileid int;
DECLARE @pageid int;
DECLARE cur CURSOR LOCAL FORWARD_ONLY STATIC READ_ONLY
FOR
SELECT dpa.allocated_page_file_id
    , dpa.allocated_page_page_id
FROM sys.schemas s  
    INNER JOIN sys.objects o ON o.schema_id = s.schema_id
CROSS APPLY sys.dm_db_database_page_allocations(DB_ID(), o.object_id, NULL, NULL, 'DETAILED') dpa
WHERE o.name = N'DropColumnTest'
    AND s.name = N'dbo'
    AND dpa.page_type_desc = N'DATA_PAGE';
OPEN cur;
FETCH NEXT FROM cur INTO @fileid, @pageid;
WHILE @@FETCH_STATUS = 0
BEGIN
    DBCC PAGE (@dbid, @fileid, @pageid, 3);
    FETCH NEXT FROM cur INTO @fileid, @pageid;
END
CLOSE cur;
DEALLOCATE cur;
DBCC TRACEOFF(3604);

En regardant la sortie de la première page de ma démo (après la suppression de la colonne, mais avant la mise à jour de la colonne), je vois ceci:

 PAGE: (1: 100104) 
 
 
 TAMPON: 
 
 
 BUF @ 0x0000021793E42040 
 
 bpage = 0x000002175A7A0000 bhash = 0x0000000000000000 bpageno = (1: 100104) 
 bdbid = 10 breferences = 1 bcputicks = 0 
 bsampleCount = 0 bUse1 = 13760 bstat = 0x10b 
 blog = 0x212121cc bnext = 0x0000000000000000 bDirtyContext = 0x000002175004B640 
 bstat2 = 0x0 
 
 EN-TÊTE DE PAGE: 
 
 
 Page @ 0x000002175A7A0000 
 
 M_pageId = (1: 100104) m_headerVersion = 1 m_type = 1 
 M_typeFlagBits = 0x0 m_level = 0 m_flagBits = 0xc000 
 M_objId (AllocUnbJ) (AllocUnbId). 300 m_indexId (AllocUnitId.idInd) = 256 
 Métadonnées: AllocUnitId = 72057594057588736 
 Méta données: PartitionId = 72057594051756032 Métadonnées: IndexId = 1 
 Métadonnées: ObjectId = 174623665 m_prevPage = (0: 0) m_nextPage = (0: 0) 
 pminlen = 8 m_slotCnt = 1 m_freeCnt = 79 
 m_freeData = 8111 m_reservedCnt = 0 m_lsn = (616: 14191: 25) 
 m_xactReserved = 0 m_xdesId = (0: 0) m_ghostRecCnt = 0 
 m_tornBits = 0 DB Frag ID = 1 
 
 Statut d'allocation 
 
 GAM (1: 2) = ALLOCATED SGAM (1: 3) = NOT ALLOCATED 
 PFS (1: 97056) = 0x40 ATTRIBUÉ 0_PCT_FULL DIFF (1: 6) = CHANGÉ 
 ML (1: 7) = NON MIN_LOGGED 
 
 Slot 0 Offset 0x60 Longueur 8015 
 
 Type d'enregistrement = PRIMARY_RECORD Attributs d'enregistrement = NULL_BITMAP VARIABLE_COLUMNS 
 Taille d'enregistrement = 8015 
 Vidage de la mémoire @ 0x000000B75227A060 
 
 0000000000000000: 30000800 01000000 02000001 004f1f5a 5a5a5a5a 0 ............ O.ZZZZZ 
 0000000000000014: 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a ZZZZZZZZZZZZZZZZZZZZ 
 
 
 
 0000000000001F2C:... 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a ZZZZZZZZZZZZZZZZZZZZ 0000000000001F40 
: 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a zzzzzzzzzzzzzzz 
 
 Emplacement 0 Colonne 1 Décalage 0x4 Longueur 4 Longueur (physique) 4 
 
 Rid = 1 
 
 Emplacement 0 Colonne 67108865 Décalage 0xf Longueur 0 Longueur (physique) 8000 
 
 DROPPED = NULL 
 
 Slot 0 Offset 0x0 Longueur 0 Longueur (physique) 0 
 
 KeyHashValue = (8194443284a0) 

J'ai supprimé la plupart du vidage de page brut de la sortie ci-dessus pour plus de brièveté. À la fin de la sortie, vous verrez ceci pour la colonne rid:

Emplacement 0 Colonne 1 Décalage 0x4 Longueur 4 Longueur (physique) 4 
 
 Rid = 1 

La dernière ligne ci-dessus, rid = 1, Renvoie le nom de la colonne et la valeur actuelle stockée dans la colonne de la page.

Ensuite, vous verrez ceci:

Emplacement 0 Colonne 67108865 Décalage 0xf Longueur 0 Longueur (physique) 8000 
 
 DROPPED = NULL 

La sortie montre que l'emplacement 0 contient une colonne supprimée, grâce au texte DELETED où le nom de la colonne serait normalement. La valeur de la colonne est renvoyée sous la forme NULL car la colonne a été supprimée. Cependant, comme vous pouvez le voir dans les données brutes, la valeur longue de 8 000 caractères, REPLICATE('Z', 8000), pour cette colonne existe toujours sur la page. Voici un exemple de cette partie de la sortie DBCC PAGE:

0000000000001EDC: 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a ZZZZZZZZZZZZZZZZZZZZ 
 0000000000001EF0: 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a ZZZZZZZZZZZZZZZZZZZZ 
 0000000000001F04: 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a ZZZZZZZZZZZZZZZZZZZZ 0000000000001F18 
: 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a 5a5a5a5a ZZZZZZZZZZZZZZZZZZZZ
14
Max Vernon