web-dev-qa-db-ja.com

ALTER COLUMNをNOT NULLにすると、ログファイルが大幅に増大するのはなぜですか?

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

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

データ型Datetimeoffset(0)のNULLABLE列を追加しました。

次に、すべての行に対してこの列を更新し、すべての新しい挿入がこの列に値を配置することを確認しました。

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

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

その結果、トランザクションログのサイズが6GBから36GB以上に大幅に増加しました。

この単純なコマンドでSQL Server 2008 R2がこのような巨大な成長を遂げるために、SQL Server 2008 R2がいったい何をしているのか、だれでも知っていますか?

56
PapillonUK

列をNOT NULLに変更すると、NULL値がない場合でも、SQL Serverはevery単一ページにアクセスする必要があります。 FILL FACTORによっては、実際には多くのページ分割が発生する可能性があります。もちろん、タッチされたすべてのページをログに記録する必要があります。分割が原因で、多くのページで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);

enter image description here

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

enter image description here

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

enter image description here

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

48
Aaron Bertrand

コマンドを実行するとき

_ALTER COLUMN ... NOT NULL
_

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

  • 新しい列を表すために、新しい行が_sys.sysrscols_に挿入されます。 _128_のstatusビットが設定され、列がNULLsを許可しないことを示します
  • テーブルのすべての行で更新が実行され、新しいcolumnnの値が古いcolumの値に設定されます。行の「前」と「後」のバージョンがまったく同じ場合、トランザクションログに何も書き込まれません。それ以外の場合は、更新が記録されます。
  • 元の列は削除されたものとしてマークされます(これは_sys.sysrscols_のメタデータのみの変更です。rscolidが大きな整数に更新され、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アレイの列カウントセクションが増加するため、行の前バージョンと後バージョンは異なります。
  • スナップショット分離オプションの1つが有効になっているデータベースでは、各行のバージョン情報が更新されます(@SQL Kiwiは、これがSIが有効になっていないデータベースでも発生する可能性があることを指摘 ここで説明 )。
  • メタデータのみの変更として実装され、まだ行に適用されていない以前の_ALTER TABLE_操作がいくつかある可能性があります。たとえば、新しいnull可能な可変長列が追加された場合、これはもともとメタデータのみの変更として適用され、次に更新されたときに実際に行に書き出されるだけです(この最後のインスタンスで実際に行われる書き込みは、列カウントセクションと、行の最後にあるNULLvarchar列としての_NULL_BITMAP_はスペースを取らない)
32
Martin Smith

200.000.000行のテーブルについても同じ問題に直面しました。最初にnullableの列を追加してから、すべての行を更新し、最後に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)を実行しています。

5
Fritz

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

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関数を見て、予約しようとしているスペースの量を確認します。

2
Keith Tate