この投稿は新しい情報を提供します 最小ログバルクロードの前提条件について INSERT ... SELECTを使用する場合 インデックス付きテーブルに 。
これらのケースを可能にする内部機能は、 FastLoadContextと呼ばれます。 。文書化されたトレースフラグ610を使用して、SQL Server2008から2014までアクティブ化できます。SQLServer2016以降、 FastLoadContext デフォルトで有効になっています。トレースフラグは必要ありません。
FastLoadContextなし 、最小限のログ記録が可能な唯一のインデックス挿入 空にそれらがあります このシリーズのパート2で説明されているように、セカンダリインデックスのないクラスター化インデックス。 最小限のロギング インデックス付けされていないヒープテーブルの条件については、パート1で説明しました。
背景について詳しくは、データパフォーマンス読み込みガイドと Tiger Teamをご覧ください。 SQLServer2016の動作の変更に関する注意事項。
簡単なリマインダーとして、 RowsetBulk ファシリティ(パート1および2で説明)により、最小限のログが可能になります バルクロード:
- 空および空でないヒープ 次のテーブル:
- テーブルロック;および
- セカンダリインデックスはありません。
- 空のクラスター化されたテーブル 、with:
- テーブルロック;および
- セカンダリインデックスはありません。および
-
DMLRequestSort =trueClustered Index Insert オペレーター。
FastLoadContext コードパスにより、最小限のログのサポートが追加されます および同時 バルクロード:
- 空および空でないクラスター化 bツリーインデックス。
- 空で空でないクラスター化されていない 専用によって維持されるbツリーインデックス インデックス挿入 プランオペレーター。
FastLoadContext DMLRequestSort =trueも必要です すべての場合において、対応するプランオペレーターについて。
RowsetBulkの重複に気付いたかもしれません およびFastLoadContext セカンダリインデックスのない空のクラスタ化テーブルの場合。 TABLOCK ヒントは不要です FastLoadContextを使用 、ただし、欠席する必要はありません また。結果として、 TABLOCKを使用した適切な挿入 最小限のロギングの対象となる可能性があります FastLoadContext経由 詳細なRowsetBulkが失敗した場合 テスト。
FastLoadContext 無効にすることができます 文書化されたトレースフラグ692を使用したSQLServer2016で。デバッグチャネル拡張イベントfastloadcontext_enabled FastLoadContextの監視に使用できます インデックスパーティション(行セット)ごとの使用量。このイベントは、 RowsetBulkでは発生しません ロード。
単一のINSERT... SELECT FastLoadContextを使用したステートメント 完全にログに記録する 最小限のロギング中のいくつかの行 その他。
行は一度に1つずつ挿入されます インデックス挿入による オペレーターと完全にログに記録された 次の場合:
- すべての行が最初のに追加されました インデックスが空だった場合のインデックスページ 操作の開始時。
- 既存に追加された行 インデックスページ。
- 行を移動 ページ分割によるページ間。
それ以外の場合は、順序付けられた挿入ストリームの行が新しいページに追加されます。 最適化された最小限のログを使用する コードパス。できるだけ多くの行が新しいページに書き込まれると、既存のターゲットインデックス構造に直接リンクされます。
新しく追加されたページは必ずしもではありません SQL Serverは、論理的に既存に属する新しいページに行を追加しないように注意する必要があるため、いっぱいになります(ただし、これは明らかに理想的なケースです)。 インデックスページ。新しいページは1つの単位としてインデックスに「ステッチ」されるため、新しいページに他の場所に属する行を含めることはできません。これは主に、内に行を追加するときに発生します。 既存のインデックスキー範囲の開始前または終了後ではなく、インデックスの既存のキー範囲。
まだ可能 内に新しいページを追加するには 既存のインデックスキーの範囲ですが、新しい行は前のの最も高いキーよりも上位に並べ替える必要があります 既存のインデックスページおよび 次のの一番下のキーより下に並べ替えます 既存のインデックスページ。 最小限のロギングを達成するための最良のチャンス このような状況では、挿入された行が既存の行と可能な限り重ならないようにしてください。
DMLRequestSort条件
FastLoadContextであることを忘れないでください DMLRequestSortの場合にのみアクティブ化できます trueに設定されています 対応するインデックス挿入 実行計画の演算子。
DMLRequestSortを設定できる2つの主要なコードパスがあります true インデックス挿入用。 どちらかのパス trueを返す 十分です。
1。 FOptimizeInsert
sqllang!CUpdUtil ::FOptimizeInsert コードに必要なもの:
- 250行以上 推定 挿入されます。 および
- 2ページ以上 推定 データサイズを挿入します。 および
- ターゲットインデックス リーフページが3ページ未満である必要があります 。
これらの条件は、 RowsetBulkと同じです。 空のクラスター化インデックスで、2つ以下のインデックスリーフレベルページの追加要件があります。これは既存のインデックスのサイズを指していることに注意してください。 挿入前、しない 追加するデータの推定サイズ。
以下のスクリプトは、このシリーズの前半で使用したデモを変更したものです。 最小限のロギングを示しています 前に入力されたインデックスページが3つ未満の場合 テストINSERT... SELECT 実行されます。テストテーブルスキーマは、データベースの行バージョン管理がオフの場合に、130行が単一の8KBページに収まるようになっています。最初のTOPの乗数 句を変更して、前の既存のインデックスページの数を決定できます。 テストINSERT... SELECT 実行されます:
IF OBJECT_ID(N'dbo.Test', N'U') IS NOT NULL
BEGIN
DROP TABLE dbo.Test;
END;
GO
CREATE TABLE dbo.Test
(
id integer NOT NULL IDENTITY
CONSTRAINT [PK dbo.Test (id)]
PRIMARY KEY,
c1 integer NOT NULL,
padding char(45) NOT NULL
DEFAULT ''
);
GO
-- 130 rows per page for this table
-- structure with row versioning off
INSERT dbo.Test
(c1)
SELECT TOP (3 * 130) -- Change the 3 here
CHECKSUM(NEWID())
FROM master.dbo.spt_values AS SV;
GO
-- Show physical index statistics
-- to confirm the number of pages
SELECT
DDIPS.index_type_desc,
DDIPS.alloc_unit_type_desc,
DDIPS.page_count,
DDIPS.record_count,
DDIPS.avg_record_size_in_bytes
FROM sys.dm_db_index_physical_stats
(
DB_ID(),
OBJECT_ID(N'dbo.Test', N'U'),
1, -- Index ID
NULL, -- Partition ID
'DETAILED'
) AS DDIPS
WHERE
DDIPS.index_level = 0; -- leaf level only
GO
-- Clear the plan cache
DBCC FREEPROCCACHE;
GO
-- Clear the log
CHECKPOINT;
GO
-- Main test
INSERT dbo.Test
(c1)
SELECT TOP (269)
CHECKSUM(NEWID())
FROM master.dbo.spt_values AS SV;
GO
-- Show log entries
SELECT
FD.Operation,
FD.Context,
FD.[Log Record Length],
FD.[Log Reserve],
FD.AllocUnitName,
FD.[Transaction Name],
FD.[Lock Information],
FD.[Description]
FROM sys.fn_dblog(NULL, NULL) AS FD;
GO
-- Count the number of fully-logged rows
SELECT
[Fully Logged Rows] = COUNT_BIG(*)
FROM sys.fn_dblog(NULL, NULL) AS FD
WHERE
FD.Operation = N'LOP_INSERT_ROWS'
AND FD.Context = N'LCX_CLUSTERED'
AND FD.AllocUnitName = N'dbo.Test.PK dbo.Test (id)';
GO クラスタ化されたインデックスに3ページがプリロードされている場合 、テスト挿入は完全にログに記録されます (簡潔にするためにトランザクションログの詳細レコードは省略されています):
テーブルに1ページまたは2ページのみがプリロードされている場合 、テスト挿入物は最小限ログに記録されます :
テーブルがプリロードされていない場合 どのページでも、テストはパート2の空のクラスター化テーブルのデモを実行するのと同じですが、なしです。 TABLOCK ヒント:
最初の130行は完全にログに記録されます 。これは、開始する前はインデックスが空であり、最初のページに130行が収まっているためです。 FastLoadContext の場合、最初のページは常に完全にログに記録されることを忘れないでください が使用され、インデックスは事前に空でした。残りの139行は、最小限のログで挿入されます 。
TABLOCKの場合 ヒントが挿入に追加されます。すべてのページ 空のクラスター化インデックスのロードがRowsetBulkの対象となるため、ログは最小限に抑えられます(最初のログを含む)。 メカニズム( Sch-Mを取ることを犠牲にして ロック)。
2。 FDemandRowsSortedForPerformance
FOptimizeInsertの場合 テストは失敗します、 DMLRequestSort まだtrueに設定されている可能性があります sqllang!CUpdUtil ::FDemandRowsSortedForPerformanceの2番目のテストセット コード。これらの条件はもう少し複雑なので、いくつかのパラメーターを定義すると便利です。
-
P–既存のリーフレベルのページの数 ターゲットインデックス 。 私–推定 挿入する行数。-
R=P/私コード> (挿入された行ごとのターゲットページ)。 -
T–ターゲットパーティションの数(パーティション化されていない場合は1)。
DMLRequestSortの値を決定するロジック その場合:
-
P <=16の場合 falseを返します 、それ以外の場合 :-
R <8の場合 :-
P> 524の場合 trueを返す 、それ以外の場合は false 。
-
-
R> =8の場合 :-
T> 1の場合 およびI>250trueを返す 、それ以外の場合は false 。
-
-
上記のテストは、プランのコンパイル中にクエリプロセッサによって評価されます。 最終条件があります ストレージエンジンコードによって評価されます( IndexDataSetSession ::WakeUpInternal )実行時:
-
DMLRequestSort現在true; および -
I> =100。
次に、このすべてのロジックを管理可能な部分に分解します。
16を超える既存のターゲットページ
最初のテストP<=16 つまり、既存のリーフページが17未満のインデックスは、 FastLoadContextの対象にはなりません。 このコードパスを介して。この点を完全に明確にするために、 P 前のターゲットインデックス内のリーフレベルのページ数です。 INSERT ... SELECT 実行されます。
ロジックのこの部分を示すために、テストクラスターテーブルに16ページをプリロードします。 データの。これには2つの重要な効果があります(両方のコードパスが falseを返す必要があることに注意してください 最終的にfalse DMLRequestSortの値 ):
- 前の
FOptimizeInsertを確実にします テスト失敗 、3番目の条件が満たされていないため(P <3。 -
P <=16FDemandRowsSortedForPerformanceの条件 また、しない 満たされる。
したがって、 FastLoadContextが必要です。 有効にしないでください。変更されたデモスクリプトは次のとおりです。
IF OBJECT_ID(N'dbo.Test', N'U') IS NOT NULL
BEGIN
DROP TABLE dbo.Test;
END;
GO
CREATE TABLE dbo.Test
(
id integer NOT NULL IDENTITY
CONSTRAINT [PK dbo.Test (id)]
PRIMARY KEY,
c1 integer NOT NULL,
padding char(45) NOT NULL
DEFAULT ''
);
GO
-- 130 rows per page for this table
-- structure with row versioning off
INSERT dbo.Test
(c1)
SELECT TOP (16 * 130) -- 16 pages
CHECKSUM(NEWID())
FROM master.dbo.spt_values AS SV;
GO
-- Show physical index statistics
-- to confirm the number of pages
SELECT
DDIPS.index_type_desc,
DDIPS.alloc_unit_type_desc,
DDIPS.page_count,
DDIPS.record_count,
DDIPS.avg_record_size_in_bytes
FROM sys.dm_db_index_physical_stats
(
DB_ID(),
OBJECT_ID(N'dbo.Test', N'U'),
1, -- Index ID
NULL, -- Partition ID
'DETAILED'
) AS DDIPS
WHERE
DDIPS.index_level = 0; -- leaf level only
GO
-- Clear the plan cache
DBCC FREEPROCCACHE;
GO
-- Clear the log
CHECKPOINT;
GO
-- Main test
INSERT dbo.Test
(c1)
SELECT TOP (269)
CHECKSUM(NEWID())
FROM master.dbo.spt_values AS SV1
CROSS JOIN master.dbo.spt_values AS SV2;
GO
-- Show log entries
SELECT
FD.Operation,
FD.Context,
FD.[Log Record Length],
FD.[Log Reserve],
FD.AllocUnitName,
FD.[Transaction Name],
FD.[Lock Information],
FD.[Description]
FROM sys.fn_dblog(NULL, NULL) AS FD;
GO
-- Count the number of fully-logged rows
SELECT
[Fully Logged Rows] = COUNT_BIG(*)
FROM sys.fn_dblog(NULL, NULL) AS FD
WHERE
FD.Operation = N'LOP_INSERT_ROWS'
AND FD.Context = N'LCX_CLUSTERED'
AND FD.AllocUnitName = N'dbo.Test.PK dbo.Test (id)'; 269行すべてが完全にログに記録されます 予測どおり:
挿入する新しい行の数をいくら高く設定しても、上記のスクリプトは決してないことに注意してください。 最小限のロギングを生成する P <=16のため テスト(および P <3 FOptimizeInsertでテストする 。
多数の行を使用してデモを自分で実行することを選択した場合は、個々のトランザクションログレコードを表示するセクションをコメントアウトしてください。コメントアウトしないと、非常に長い時間待機し、SSMSがクラッシュする可能性があります。 (公平を期すために、とにかくそうするかもしれませんが、なぜリスクを増すのですか。)
17以上がある場合 既存のインデックスのリーフページ、前の P <=16 テストは失敗しません。ロジックの次のセクションでは、既存のページの比率を扱います。 新しく挿入された行 。 最小限のロギングを達成するには、これも合格する必要があります 。注意点として、関連する条件は次のとおりです。
- 比率
R=P/私コード> 。 -
R <8の場合 :-
P> 524の場合 trueを返す 、それ以外の場合は false 。
-
また、少なくとも100行の最終的なストレージエンジンテストを覚えておく必要があります。
-
I> =100。
これらの条件を少し再編成し、すべて 次のうち、真でなければなりません:
-
P> 524(既存のインデックスページ) -
I> =100(挿入された行の推定値) -
P / I <8(比率R)
これらの3つの条件を満たすには複数の方法があります。 Pの可能な最小値を選択しましょう (525)および I (100) Rを与える (525/100)=5.25の値。これは( R <8 テスト)、したがって、この組み合わせにより、最小限のロギングが得られると予想されます。 :
IF OBJECT_ID(N'dbo.Test', N'U') IS NOT NULL
BEGIN
DROP TABLE dbo.Test;
END;
GO
CREATE TABLE dbo.Test
(
id integer NOT NULL IDENTITY
CONSTRAINT [PK dbo.Test (id)]
PRIMARY KEY,
c1 integer NOT NULL,
padding char(45) NOT NULL
DEFAULT ''
);
GO
-- 130 rows per page for this table
-- structure with row versioning off
INSERT dbo.Test
(c1)
SELECT TOP (525 * 130) -- 525 pages
CHECKSUM(NEWID())
FROM master.dbo.spt_values AS SV1
CROSS JOIN master.dbo.spt_values AS SV2;
GO
-- Show physical index statistics
-- to confirm the number of pages
SELECT
DDIPS.index_type_desc,
DDIPS.alloc_unit_type_desc,
DDIPS.page_count,
DDIPS.record_count,
DDIPS.avg_record_size_in_bytes
FROM sys.dm_db_index_physical_stats
(
DB_ID(),
OBJECT_ID(N'dbo.Test', N'U'),
1, -- Index ID
NULL, -- Partition ID
'DETAILED'
) AS DDIPS
WHERE
DDIPS.index_level = 0; -- leaf level only
GO
-- Clear the plan cache
DBCC FREEPROCCACHE;
GO
-- Clear the log
CHECKPOINT;
GO
-- Main test
INSERT dbo.Test
(c1)
SELECT TOP (100)
CHECKSUM(NEWID())
FROM master.dbo.spt_values AS SV1
CROSS JOIN master.dbo.spt_values AS SV2;
GO
-- Show log entries
SELECT
FD.Operation,
FD.Context,
FD.[Log Record Length],
FD.[Log Reserve],
FD.AllocUnitName,
FD.[Transaction Name],
FD.[Lock Information],
FD.[Description]
FROM sys.fn_dblog(NULL, NULL) AS FD;
GO
-- Count the number of fully-logged rows
SELECT
[Fully Logged Rows] = COUNT_BIG(*)
FROM sys.fn_dblog(NULL, NULL) AS FD
WHERE
FD.Operation = N'LOP_INSERT_ROWS'
AND FD.Context = N'LCX_CLUSTERED'
AND FD.AllocUnitName = N'dbo.Test.PK dbo.Test (id)';
100行のINSERT... SELECT 確かに最小限のログ :
推定を減らす 行を99に挿入しました( I> =100を壊します )、および/または既存のインデックスページの数を524に減らす( P> 524 を壊す) )結果は完全なログ記録になります 。 Rのように変更することもできます 完全なログ記録を生成するために8以上である 。たとえば、 P =1000を設定します およびI=125 R =8を与えます 、次の結果が得られます:
挿入された125行は完全にログに記録されました 予想通り。 (これは、インデックスが事前に空ではなかったため、最初のページの完全なログ記録によるものではありません。)
上記のすべてのテストが失敗した場合、残りの1つのテストには R> =8が必要です。 のみ パーティションの数( T )が1より大きいおよび 250以上の推定があります 挿入された行( I )。思い出してください:
-
R> =8の場合 :-
T> 1の場合 およびI>250trueを返す 、それ以外の場合は false 。
-
1つの微妙な点:パーティション化の場合 インデックス、すべての最初のページの行が完全にログに記録されるというルール(最初は空のインデックスの場合)は、パーティションごとに適用されます。 。 15,000のパーティションを持つオブジェクトの場合、これは15,000の完全にログに記録された「最初の」ページを意味します。
本文に記載されている式と評価の順序は、デバッガーを使用したコード検査に基づいています。それらは、実際のコードで使用されるタイミングと順序を厳密に表す形式で提示されました。
これらの条件を少し並べ替えて単純化して、最小限のロギングの実際的な要件のより簡潔な要約を作成することができます。 INSERT ... SELECTを使用してBツリーに挿入する場合 。以下の洗練された式は、次の3つのパラメーターを使用します。
-
P=既存の数 リーフレベルのページにインデックスを付けます。 私=推定 挿入する行数。-
S=推定 データサイズを8KBページに挿入します。
-
sqlmin!RowsetBulkを使用します 。 - 空が必要です
TABLOCKを使用したクラスター化インデックスターゲット (または同等のもの)。 -
DMLRequestSort =trueが必要です Clustered Index Insert オペレーター。 -
DMLRequestSorttrueに設定されていますI> 250の場合 およびS>2。 - 挿入されたすべての行は最小限にログに記録されます 。
-
Sch-Mロックはテーブルへの同時アクセスを防ぎます。
-
sqlmin!FastLoadContextを使用します 。 - 最小限のログを有効にします bツリーインデックスへの挿入:
- クラスター化または非クラスター化。
- テーブルロックの有無にかかわらず。
- ターゲットインデックスが空かどうか。
-
DMLRequestSort =trueが必要です 関連するインデックス挿入 プランオペレーター。 - ブランドの新しいページに書き込まれた行のみ 一括読み込みされ、最小限のログ 。
- 最初のページ 以前の空のインデックスの パーティションは常に完全にログに記録されます 。
-
I> =100の絶対最小値 。 - SQLServer2016の前にトレースフラグ610が必要です。
- SQL Server 2016からデフォルトで利用可能です(トレースフラグ692は無効になります)。
DMLRequestSort trueに設定されています 対象:
- 任意のインデックス (パーティション化されているかどうか)if:
-
I> 250およびP<3およびS>2; または -
I> =100およびP>524およびP
-
パーティションインデックスのみ (> 1パーティション)、 DMLRequestSort trueも設定されます 場合:
-
I> 250およびP>16およびP>=I * 8
これらのFastLoadContextから生じるいくつかの興味深いケースがあります 条件:
- すべて パーティション化されていないに挿入します 3から524の間のインデックス (包括的)既存のリーフページは完全にログに記録されます 追加された行の数と合計サイズに関係なく。これは、小さな(ただし空ではない)テーブルへの大きな挿入に最も顕著に影響します。
- すべて パーティション化されたに挿入します 3から16のインデックス 既存のページは完全にログに記録されます 。
- 大きなインサート 大きなパーティション化されていない インデックスは最小限にログに記録されることはできません 不等式のため
P 。Pの場合 大きい、それに応じて大きい推定 挿入された行の数(I) 必要とされている。たとえば、800万ページのインデックスは、最小限のロギングをサポートできません。 100万行以下を挿入する場合。
デモのクラスター化インデックスに適用されるのと同じ考慮事項と計算が、非クラスター化にも適用されます。 インデックスが専用のプランオペレーターによって維持されている限り、bツリーインデックスも同様です。 (ワイド 、またはインデックスごと 予定)。ベーステーブルオペレーターによって維持される非クラスター化インデックス(例:クラスター化インデックス挿入 ) FastLoadContextの対象ではありません 。
数式パラメータは、非クラスタ化ごとに新たに評価する必要があることに注意してください。 インデックス演算子—計算された行サイズ、既存のインデックスページの数、およびカーディナリティの見積もり。
低カーディナリティ推定値に注意してください インデックス挿入で 演算子、これらは Iに影響するため およびS パラメーター。カーディナリティ推定エラーが原因でしきい値に達しない場合、挿入は完全にログに記録されます 。
DMLRequestSort プランとともにキャッシュされます —再利用された計画の実行ごとに評価されません。これにより、よく知られているパラメータ感度問題(「パラメータスニッフィング」とも呼ばれます)の形式が導入される可能性があります。
Pの値 (インデックスリーフページ)は更新されません すべてのステートメントの開始時に。現在の実装では、バッチ全体の値がキャッシュされます。 。これは予期しない副作用を引き起こす可能性があります。たとえば、 TRUNCATE TABLE 同じバッチ INSERT ... SELECTとして Pはリセットされません この記事で説明されている計算ではゼロになります—切り捨て前の値を引き続き使用するため、再コンパイルは役に立ちません。回避策は、大きな変更を別々のバッチで送信することです。
FDemandRowsSortedForPerformanceを強制することができます trueを返す 文書化されていない、サポートされていないを設定する データを変更するT-SQLクエリの最適化で書いたように、フラグ2332をトレースします。 TF 2332がアクティブな場合、挿入する推定行数 それでも少なくとも100である必要があります 。 TF2332は最小限のロギングに影響します FastLoadContextの決定 のみ( DMLRequestSort までは、パーティション化されたヒープに対して有効です。 懸念されますが、 FastLoadContext であるため、ヒープ自体には影響しません。 インデックスにのみ適用されます)。
ワイド/インデックスごと 非クラスター化インデックス保守の計画形状は、トレースフラグ8790を使用して行ストアテーブルに強制できます(公式には文書化されていませんが、ナレッジベースの記事および上記のTF2332にリンクされている私の記事で言及されています)。
すべてSQLServerチームのSunilAgarwalによる:
- 一括インポートの最適化とは何ですか?
- 一括インポートの最適化(最小限のロギング)
- SQLServer2008での最小限のログ変更
- SQL Server 2008での最小限のログの変更(パート2)
- SQL Server 2008での最小限のログの変更(パート3)