ALTER COLUMN to NOT NULLが大量のログファイルの増加を引き起こすのはなぜですか?


56

データ用にディスク上に4.3 GBを使用する64m行のテーブルがあります。

各行は約30バイトの整数列に加えて、NVARCHAR(255)テキスト用の可変列です。

data-typeのNULLABLE列を追加しましたDatetimeoffset(0)

次に、すべての行でこの列を更新し、すべての新しい挿入でこの列に値が配置されるようにしました。

NULLエントリがなくなったら、このコマンドを実行して新しいフィールドを必須にしました。

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

その結果、トランザクションログサイズが大幅に増加しました。スペースがなくなるまで、6GBから36GBを超えました。

SQL Server 2008 R2がこの単純なコマンドでこのような大きな成長をもたらすために一体何をしているのか、誰にもわかりませんか?


7
SQL Server 2012 Enterprise は、NOT NULLメタデータ操作としてデフォルトで列を追加する機能を追加します。ドキュメントの「オンライン操作としてのNOT NULL列の追加」も参照してください。
ポールホワイト

回答:


48

列をNOT NULLに変更すると、NULL値がない場合でも、SQL Serverはすべてのページにアクセスする必要があります。フィルファクターによっては、実際には多くのページ分割が発生する可能性があります。もちろん、触れられたすべてのページをログに記録する必要があり、多くのページで2つの変更をログに記録する必要がある可能性があるため、分割のために疑います。ただし、すべて1回のパスで行われるため、ログはすべての変更を考慮に入れる必要があります。そのため、キャンセルをクリックすると、元に戻す内容が正確にわかります。


例。シンプルなテーブル:

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

それでは、ページの詳細を見てみましょう。まず、どのページとDB_IDを扱っているかを知る必要があります。私の場合、というデータベースを作成しましたがfoo、DB_IDはたまたま5でした。

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

出力は、159ページ(のDBCC IND出力の唯一の行PageType = 1)に興味があることを示しています。

次に、OPのシナリオをステップごとに選択するページの詳細を見てみましょう。

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

ここに画像の説明を入力してください

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

ここに画像の説明を入力してください

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

ここに画像の説明を入力してください

今、私はこれに対するすべての答えを持っているわけではありません。しかし、更新操作とNOT NULL制約の追加の両方が紛れもなくページに書き込みを行う一方で、後者はまったく異なる方法で書き込みを行うことは明らかです。ヌル値を許可する列をヌル値を許可しない列に交換することで、ビットをいじるのではなく、実際にレコードの構造を変更するようです。なぜそれをしなければならないのか、私にはよくわからない- ストレージエンジンチームにとって良い質問だと思う。SQL Server 2012はこれらのシナリオのいくつかをFWIWでより良く処理できると思いますが、徹底的なテストはまだ行っていません。


4
この動作は、SQL Serverの以降のバージョンで大幅に変更されました。2016 RC2を確認したところ、この正確なシナリオでは、列にすべての値が既に指定されている場合、NULLからNOT NULLへの変更中にテーブル内の100万行のみが生成されます。
エンドルジュ

32

コマンドを実行するとき

ALTER COLUMN ... NOT NULL

これは、列の追加、更新、列のドロップ操作として実装されているようです。

  • 新しい列sys.sysrscolsを表すために新しい行が挿入されます。のstatusビット128は、列がNULLsを許可しないことを示すために設定されます
  • テーブルのすべての行で更新が実行され、新しい列の値が古い列の値に設定されます。行の「前」と「後」のバージョンがまったく同じ場合、トランザクションログに何も書き込まれません。そうでない場合、更新がログに記録されます。
  • ドロップされたように元の列がマークされている(これは、メタデータのみの変化であるsys.sysrscolsrscolid大きな整数とに更新status滴下示さ上にビット2セット)
  • sys.sysrscols新しい列のエントリrscolidは、古い列のエントリに変更されます。

大量のロギングを引き起こす可能性のある操作UPDATEは、テーブル内のすべての行の操作ですがこれは常にこれが発生するという意味ではありません。行の「前」と「後」のイメージが同一である場合、これは非更新アップデートとして扱われ、これまでのテストでは記録されません。

したがって、多くのログを取得する理由に関する説明は、行の「前」と「後」のバージョンがまったく同じではない理由に依存します。

FixedVar形式で保存された可変長列のNOT NULL場合、ログに記録する必要がある行の変更が常に発生するように設定することがわかりました。列数と可変長列数の両方が増分され、新しい列がデータを複製する可変長セクションの最後に追加されます。

datetimeoffset(0)しかし、固定長であり、中に格納された固定長列のFixedVar形式新旧列の両方は、それらの両方が同じ長さと値を持つ行の固定長データ部分及び同じスロット与えられているように見える「前」と行の「後」バージョンは同じです。これは、@ Aaronの回答で見ることができます。前後の両方のバージョンの行ALTER TABLE dbo.floob ALTER COLUMN bar INT NOT NULL;

0x10000c00 01000000 00000000 020000

これは記録されません。

論理的には、イベントの説明から、列数02を増やす必要があるため、実際には行は異なるはずですが、実際に03はそのような変更は発生しません。

これが固定長列で発生する可能性がある理由として考えられる理由は次のとおりです。

  • 列が最初に宣言されたSPARSE場合、新しい列は元の行とは異なる行の部分に格納され、前後の行のイメージが異なります。
  • 圧縮オプションのいずれかを使用している場合、CDアレイの列カウントセクションが増分されるため、行の前バージョンと後バージョンが異なります。
  • スナップショット分離オプションのいずれかが有効になっているデータベースでは、各行のバージョン情報が更新されます(@SQL Kiwiは、これがSIが有効になっていないデータベースでも発生する可能性があることをここで説明します)。
  • ALTER TABLEメタデータのみの変更として実装され、まだ行に適用されていない以前の操作がある場合があります。たとえば、新しいnullable可変長列が追加された場合、これは元々メタデータとしてのみ適用され、行が次に更新されるときにのみ実際に書き込まれます(実際にこの最後のインスタンスで発生する書き込みは列カウントセクションと行の最後の列NULL_BITMAPとしてはNULL varcharスペースを使用しません)

5

200.000.000行のテーブルに関しても同じ問題に直面しました。最初にNULL可能列を追加し、すべての行を更新し、最後にステートメントをNOT NULL介して列を変更しましたALTER TABLE ALTER COLUMN。これにより、2つの巨大なトランザクションがログファイルを信じられないほど爆発させました(170 GBの増加)。

私が見つけた最速の方法は次のとおりです。

  1. デフォルト値を使用して列を追加します

    ALTER TABLE table1 ADD column1 INT NOT NULL DEFAULT (1)
  2. 制約に以前に名前が付けられていないため、動的SQLを使用してデフォルトの制約を削除します。

    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';

実行時間は、トランザクションレプリケーションによる変更の複製を含め、30分以上から10分に短縮されました。SQL Server 2008インストール(SP2)を実行しています。


2

次のテストを実行しました。

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)

これは、トランザクションをロールバックする場合に備えて、ログが保持する予約スペースに関係していると考えています。LOP_BEGIN_XACT行の「ログ予約」列でfn_dblog関数を調べ、予約しようとしているスペースの量を確認します。


試してみるとselect * FROM fn_dblog(null, null) where AllocUnitName='dbo.tblCheckResult' AND Operation = 'LOP_MODIFY_ROW'、10000行の更新が表示されます。
マーティンスミス

-2

これに対する動作は、SQL Server 2012では異なります。http://rusanu.com/2011/07/13/online-non-null-with-values-column-add-in-sql-server-11/を参照してください

SQL Server 2008 R2以前のリリースで生成されるログレコードの数は、SQL Server 2012のログレコードの数よりも大幅に多くなります。


2
問題は、既存の列を変更してNOT NULLログを記録する理由です。2012年の変更はNOT NULL、デフォルトの新しい列を追加することです。
マーティンスミス
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.