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


11

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

ソース

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

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

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

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


2
まあ、言語は製品の多くのバージョンとドキュメントのより多くの反復を通して生き残ったと思います。時間の経過とともに、列を含む操作がオンラインになり、メタデータのみが変更されます。これは今のところ悪い具体例かもしれませんが、この文の目的は単に、特定のシナリオをすべてリストするのではなく、一般に、一部の変更操作特定のシナリオのデータサイズ操作である可能性があることを警告することです。
アーロンバートランド

回答:


14

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

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

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

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

これは、列を削除した後にトランザクションログの内容を確認することで、かなり簡単に表示できます。以下のコードは、単一の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」は、ページヘッダーと行ごとの詳細な解釈が必要であることを示します。コードはカーソルを使用してテーブル内のすべてのページの詳細を表示するため、大きなテーブルではこれを実行しないようにする必要がある場合があります。

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
ブログ= 0x212121cc bnext = 0x0000000000000000 bDirtyContext = 0x000002175004B640
bstat2 = 0x0                        

ページヘッダー:


ページ@ 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フラグメントID = 1                      

割り当てステータス

GAM(1:2)=割り当て済みSGAM(1:3)=割り当てなし          
PFS(1:97056)= 0x40 ALLOCATED 0_PCT_FULL DIFF(1:6)=変更されました
ML(1:7)= MIN_LOGGEDではない           

スロット0オフセット0x60長さ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

削除= NULL                      

スロット0オフセット0x0長さ0長さ(物理)0

KeyHashValue =(8194443284a0)       

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

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

rid = 1                             

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

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

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

削除= NULL                      

出力はDELETED、列名が通常あるテキストのおかげで、スロット0に削除された列が含まれていることを示しています。NULL列が削除されてからの列の値が返されます。ただし、生データを見るとわかるように、REPLICATE('Z', 8000)その列の8,000文字の長さの値は、ページにまだ存在しています。これは、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
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.