web-dev-qa-db-ja.com

ALTER TABLE ... DROP COLUMNは本当にメタデータのみの操作ですか?

ALTER TABLE ... DROP COLUMNはメタデータのみの操作であるというソースをいくつか見つけました。

ソース

どうすればいいの? DROP COLUMN中のデータを、基になる非クラスター化インデックスおよびクラスター化インデックス/ヒープから削除する必要はありませんか?

さらに、なぜ Microsoft Docs は、それが完全にログに記録された操作であることを示唆しているのですか?

テーブルに加えられた変更はログに記録され、完全に回復可能です。 列の削除、SQL Serverの一部のエディションでは、デフォルトのNOT NULL列の追加など、大きなテーブルのすべての行に影響する変更値は、完了して多くのログレコードを生成するのに長い時間がかかる場合があります。これらのALTER TABLEステートメントは、多くの行に影響するINSERT、UPDATE、またはDELETEステートメントと同じように注意して実行してください。

二次的な質問として:データが基になるページから削除されない場合、エンジンはどのようにドロップされた列を追跡しますか?

11
George.Palacios

列の削除がメタデータのみの操作になる場合がある特定の状況があります。特定のテーブルの列定義は、行が格納されるすべてのページに含まれていません。列定義は、sys.sysrowsets、sys.sysrscolsなどのデータベースメタデータにのみ格納されます。

他のオブジェクトによって参照されていない列を削除する場合、ストレージエンジンは、さまざまなシステムテーブルから関連する詳細を削除することにより、列定義を存在しないものとしてマークします。メタデータを削除すると、プロシージャキャッシュが無効になり、クエリが後でそのテーブルを参照するたびに再コンパイルが必要になります。再コンパイルはcurrentlyがテーブルに存在する列のみを返すため、削除された列の列の詳細は要求されません。ストレージエンジンは、列が存在しないかのように、その列の各ページに格納されているバイトをスキップします。

テーブルに対して後続のDML操作が発生すると、影響を受けるページは、ドロップされた列のデータなしで再書き込みされます。クラスター化インデックスまたはヒープを再構築する場合、ドロップされた列のすべてのバイトはディスク上のページに書き戻されません。これにより、時間の経過とともにカラムを落下させる負荷が効果的に分散され、目立たなくなります。

列がインデックスに含まれている場合や、列の統計オブジェクトを手動で作成した場合など、列を削除できない状況があります。手動で作成した統計オブジェクトで列を変更しようとしたときに表示されるエラーを示す ブログ投稿 を書きました。列を削除するときにも同じセマンティクスが適用されます。列がany他のオブジェクトによって参照されている場合、単純に削除することはできません。参照するオブジェクトを最初に変更してから、列を削除する必要があります。

これは、列を削除した後にトランザクションログの内容を確認することで、かなり簡単に表示できます。以下のコードは、単一の8,000の長いchar列を持つテーブルを作成します。行を追加してからドロップし、ドロップ操作に適用できるトランザクションログの内容を表示します。ログレコードは、テーブルと列の定義が格納されているさまざまなシステムテーブルへの変更を示します。列データがテーブルに割り当てられたページから実際に削除されていた場合、実際のページデータを記録するログレコードが表示されます。そのような記録はありません。

_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)
_

(出力が大きすぎてここに表示できません。dbfiddle.ukではfn_dblogにアクセスできません)

出力の最初のセットは、DDLステートメントが列を削除した結果のログを示しています。 2番目の出力セットは、rid列を更新するDMLステートメントを実行した後のログを示しています。 2番目の結果セットでは、dbo.DropColumnTestに対する削除と、それに続くdbo.DropColumnTestへの挿入を示すログレコードが表示されます。各ログレコード長は8116で、実際のページが更新されたことを示します。

上記のテストの_fn_dblog_コマンドの出力からわかるように、操作全体は完全にログに記録されています。これは、単純な回復だけでなく、完全な回復にも当てはまります。 「完全にログに記録された」という用語は、データの変更がログに記録されないため、誤って解釈される可能性があります。これは何が起こるかではありません-変更が記録され、完全にロールバックできます。ログは単にのみタッチされたページを記録し、テーブルのデータページはDDL操作によってログに記録されなかったため、両方の_DROP COLUMN_、および発生する可能性のあるロールバックは、テーブルのサイズに関係なく、非常に迅速に発生します。

scienceの場合、次のコードは、_DBCC PAGE_、スタイル "3"を使用して、上記のコードに含まれるテーブルのデータページをダンプします。スタイル「3」は ページヘッダーと行ごとの詳細な解釈 が必要であることを示します。コードはカーソルを使用して、テーブルのeveryページの詳細を表示するため、これを大規模な環境で実行しないようにしたい場合がありますテーブル。

_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);
_

デモの最初のページの出力(列が削除された後、列が更新される前)を見ると、次のことがわかります。

ページ:(1:100104)
 
 
バッファ:
 
 
 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 
 
 PAGE HEADER:
 
 
 Page @ 0x000002175A7A0000 
 
 m_pageId =(1:100104)m_headerVersion = 1 m_type = 1 
 m_typeFlagBits = 0x0 m_level = 0 m_flagBits = 0xc000 
 m_objId(AllocUnitId.idObj) 300 m_indexId(AllocUnitId.idInd)= 256 
メタデータ:AllocUnitId = 72057594057588736 
メタデータ:PartitionId = 72057594051756032メタデータ:IndexId = 1 
メタデータ: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 
 
割り当てステータス
 
 GAM(1:2)= ALLOCATED SGAM(1:3)= NOT ALLOCATED 
 PFS(1:97056)= 0x40 ALLOCATED 0_PCT_FULL DIFF(1:6)= CHANGED 
 ML(1:7)= NOT MIN_LOGGED 
 
 Slot 0 Offset 0x60 Length 8015 
 
レコードタイプ= PRIMARY_RECORDレコード属性= NULL_BITMAP VARIABLE_COLUMNS 
レコードサイズ= 8015 
メモリダンプ@ 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 
 
スロット0列1オフセット0x4長さ4長さ(物理)4 
 
 rid = 1 
 
スロット0列67108865オフセット0xf長さ0長さ(物理的)8000 
 
 DROPPED = NULL 
 
スロット0オフセット0x0長さ0長さ(物理的)0 
 
 KeyHashValue =(8194443284a0)

簡潔にするために、上記の出力から生のページダンプのほとんどを削除しました。出力の最後に、rid列についてこれが表示されます。

スロット0列1オフセット0x4長さ4長さ(物理)4 
 
 rid = 1 

上記の最後の行_rid = 1_は、列の名前と、ページの列に格納されている現在の値を返します。

次に、これが表示されます:

スロット0列67108865オフセット0xf長さ0長さ(物理)8000 
 
 DROPPED = NULL 

出力は、列名が通常あるDELETEDテキストによって、スロット0に削除された列が含まれていることを示しています。列が削除されているため、列の値はNULLとして返されます。ただし、生データを見るとわかるように、その列の8,000文字の長さの値REPLICATE('Z', 8000)はまだページに存在しています。これは、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