このシリーズの前回の投稿では、読み取りコミットスナップショットアイソレーション( RCSI )で実行されるT-SQLステートメントがどのように実行されるかを示しました。 )通常、ステートメントの実行が開始されたときのデータベースのコミット状態のスナップショットビューが表示されます。これは、データを読み取るステートメントでどのように機能するかを説明するのに適していますが、重要な違いがあります。 既存の行を変更するRCSIで実行されるステートメントの場合 。
既存の行の変更を強調します 上記の理由は、次の考慮事項がUPDATE
にのみ適用されるためです。 およびDELETE
操作(およびMERGE
の対応するアクション 声明)。明確にするために、INSERT
ステートメントは特に除外されます 挿入は既存を変更しないため、これから説明する動作から データ。
ロックと行のバージョンを更新する
最初の違いは、ステートメントの更新と削除が行バージョンを読み取らないことです。 変更するソース行を検索するときのRCSIの下。代わりに、RCSIでステートメントを更新および削除して、更新ロックを取得します。 適格な行を検索する場合。更新ロックを使用すると、検索操作で最新のコミット済みデータを使用して変更する行が確実に検出されます。 。
更新ロックがない場合、検索はデータセットの古いバージョン(データ変更ステートメントが開始されたときのようにコミットされたデータ)に基づいて行われます。これは、前回見たトリガーの例を思い出させるかもしれません。ここでは、READCOMMITTEDLOCK
ヒントは、RCSIから読み取りコミット分離のロック実装に戻すために使用されました。その例では、古い情報に基づいて重要なアクションを実行することを避けるために、そのヒントが必要でした。ここでも同じ種類の推論が使用されています。 1つの違いは、READCOMMITTEDLOCK
ヒントは、更新ロックではなく共有ロックを取得します。さらに、SQL Serverは、明示的なヒントを追加しなくても、RCSIでのデータ変更を保護するために、更新ロックを自動的に取得します。
更新ロックを取得すると、更新または削除ステートメントがブロックされることも保証されます 互換性のないロックが発生した場合。たとえば、別の同時トランザクションによって実行された実行中のデータ変更を保護する排他的ロック。
さらに厄介なのは、変更された動作がのみ適用されることです。 ターゲットであるテーブルに 更新または削除操作の。 その他のテーブル 同じで 追加の参照を含むステートメントの削除または更新 ターゲットテーブルに対して、行バージョンの使用を継続 。
これらの紛らわしい動作を少し明確にするために、おそらくいくつかの例が必要です…
テストセットアップ
次のスクリプトは、すべての人がRCSIを使用するように設定されていることを確認し、単純なテーブルを作成して、それに2つのサンプル行を追加します。
ALTER DATABASE Sandpit SET READ_COMMITTED_SNAPSHOT ON WITH ROLLBACK IMMEDIATE; GO SET TRANSACTION ISOLATION LEVEL READ COMMITTED; GO CREATE TABLE dbo.Test ( RowID integer PRIMARY KEY, Data integer NOT NULL ); GO INSERT dbo.Test (RowID, Data) VALUES (1, 1234), (2, 2345);
次のステップは、別のセッションで実行する必要があります 。トランザクションを開始し、テストテーブルから両方の行を削除します(奇妙に思えますが、これはまもなくすべて意味があります):
BEGIN TRANSACTION; DELETE dbo.Test WHERE RowID IN (1, 2);
トランザクションは意図的に開いたままにすることに注意してください 。これにより、削除される両方の行の排他的ロックが維持されます(含まれているページとテーブル自体の通常の意図的排他的ロックとともに)。以下のクエリを使用して次のように表示できます。
SELECT resource_type, resource_description, resource_associated_entity_id, request_mode, request_status FROM sys.dm_tran_locks WHERE request_session_id = @@SPID;
選択テスト
元のセッションに戻る 、最初に示したいのは、RCSIを使用する通常のselectステートメントでは、2つの行が削除されていることを確認できることです。以下のselectクエリは、行バージョンを使用して、ステートメントの開始時の最新のコミット済みデータを返します。
SELECT * FROM dbo.Test;
意外に思われる場合は、行を削除済みとして表示すると、データのコミットされていないビューが表示されることになります。これは、読み取りコミットされた分離では許可されません。
削除テスト
選択テストは成功しましたが、削除を試みました 現在のセッションからのこれらの同じ行はブロックされます。このブロッキングは、操作が排他的を取得しようとしたときに発生することを想像するかもしれません。 ロックしますが、そうではありません。
削除は行のバージョン管理を使用しません 削除する行を見つけるため。代わりに更新ロックを取得しようとします。更新ロックは、開いているトランザクションとのセッションによって保持されている排他的な行ロックと互換性がないため、クエリは次のようにブロックします。
DELETE dbo.Test WHERE RowID IN (1, 2);
このステートメントの推定クエリプランは、別のオペレーターが実際の削除を実行する前に、削除される行が通常のシーク操作によって識別されることを示しています。
SPID参照をブロックされたクエリで使用されるものに変更することを忘れないで、以前と同じロッククエリを(別のセッションから)実行することで、この段階で保持されているロックを確認できます。結果は次のようになります:
削除クエリは、読み取りへの更新ロックの取得を待機しているクラスター化インデックスシークオペレーターでブロックされます データ。これは、RCSIで削除する行を見つけると、古くなっている可能性のあるバージョン管理されたデータを読み取るのではなく、更新ロックを取得することを示しています。また、排他ロックの取得を待機している操作の削除部分がブロックの原因ではないことも示しています。
更新テスト
ブロックされたクエリをキャンセルし、代わりに次の更新を試してください:
UPDATE dbo.Test SET Data = Data + 1000 WHERE RowID IN (1, 2);
推定される実行プランは、削除テストで見られるものと似ています:
Compute Scalarは、クラスター化インデックスシークによって読み取られる各行のデータ列の現在の値に1000を追加した結果を決定するためにあります。このステートメントもブロックします 実行時に、読み取り操作によって要求された更新ロックのため。以下のスクリーンショットは、クエリがブロックされたときに保持されるロックを示しています。
以前と同様に、クエリはシークでブロックされ、互換性のない排他ロックが解放されるのを待って、更新ロックを取得できるようにします。
挿入テスト
次のテストでは、既存の行のデータ列の値を使用して、テストテーブルに新しい行を挿入するステートメントを使用します。 テーブルにID1があります。この行は、開いているトランザクションとのセッションによって引き続き排他的にロックされていることを思い出してください。
INSERT dbo.Test (RowID, Data) SELECT 3, Data FROM dbo.Test WHERE RowID = 1;
実行計画も以前のテストと同様です:
今回は、クエリはブロックされていません 。これは、読み取り時に更新ロックが取得されなかったことを示しています 挿入のデータ。このクエリでは、代わりに行バージョン管理を使用して、新しく挿入された行のデータ列の値を取得しました。このステートメントは変更する行を見つけられなかったため、更新ロックは取得されませんでした 、挿入に使用するデータを読み取るだけです。
以前のselecttestクエリを使用して、この新しい行をテーブルに表示できます。
私たちはであることに注意してください 競合する排他ロックがないため、新しい行を更新および削除できます(更新ロックが必要になります)。オープントランザクションのセッションでは、行1と2にのみ排他ロックがあります。
-- Update the new row UPDATE dbo.Test SET Data = 9999 WHERE RowID = 3; -- Show the data SELECT * FROM dbo.Test; -- Delete the new row DELETE dbo.Test WHERE RowID = 3;
このテストは、挿入ステートメントが読み取り時に更新ロックを取得しないことを確認します 、更新や削除とは異なり、変更しないためです。 既存の行。 挿入の読み取り部分 ステートメントは、通常のRCSI行のバージョン管理動作を使用します。
複数の参照テスト
前に述べたように、変更する行を見つけるために使用される単一のテーブル参照のみが更新ロックを取得します。同じupdateまたはdeleteステートメント内の他のテーブルは、引き続き行バージョンを読み取ります。その一般原則の特殊なケースとして、同じテーブルへの複数の参照を含むデータ変更ステートメント 1つのインスタンスにのみ更新ロックを適用します 変更する行を見つけるために使用されます。この最終テストは、このより複雑な動作を段階的に示しています。
最初に必要なのは、テストテーブルの新しい3行目です。今回は、[データ]列にゼロがあります。
INSERT dbo.Test (RowID, Data) VALUES (3, 0);
予想どおり、この挿入はブロックせずに進行し、次のようなテーブルになります。
2番目のセッションはまだ排他的であることに注意してください この時点で行1と2をロックします。必要に応じて、3行目のロックを自由に取得できます。次のクエリは、ターゲットテーブルへの複数の参照がある動作を表示するために使用するクエリです。
-- Multi-reference update test UPDATE WriteRef SET Data = ReadRef.Data * 2 OUTPUT ReadRef.RowID, ReadRef.Data, INSERTED.RowID AS UpdatedRowID, INSERTED.Data AS NewDataValue FROM dbo.Test AS ReadRef JOIN dbo.Test AS WriteRef ON WriteRef.RowID = ReadRef.RowID + 2 WHERE ReadRef.RowID = 1;
これはより複雑なクエリですが、操作は比較的簡単です。テストテーブルへの参照は2つあります。1つはReadRefとしてエイリアスし、もう1つはWriteRefとしてエイリアスします。アイデアは読む 行1から(行バージョンを使用して)ReadRefを介して、更新 WriteRefを使用した3行目(更新ロックが必要)。
クエリは、読み取りテーブル参照のwhere句で行1を明示的に指定します。 同じテーブルへの書き込み参照に結合します そのRowIDに2を追加します(したがって、行3を識別します)。 updateステートメントは、output句を使用して、ソーステーブルから読み取られた値と行3に加えられた変更を示す結果セットも返します。
このステートメントの推定クエリプランは次のとおりです。
(1)というラベルの付いたシークのプロパティ このシークがReadRefにあることを示します エイリアス、RowID 1の行からデータを読み取る:
このシーク操作では更新される行が見つからないため、更新ロックはありません 取られた;読み取りは、バージョン管理されたデータを使用して実行されます。読み取りは、他のセッションによって保持されている排他ロックによってブロックされません。
(2)というラベルの付いた計算スカラー 更新されたデータ列の値を計算する1004というラベルの付いた式を定義します。式1009は、更新される行IDを計算します(1 + 2=行ID3):
2番目のシークは、同じテーブルへの参照です(3)。 このシークは、式1009を使用して更新される行(行3)を検索します。
このシークは変更する行を見つけるため、更新ロック 行バージョンを使用する代わりに取得されます。行ID3には競合する排他ロックがないため、ロック要求はすぐに許可されます。
最後に強調表示された演算子(4) 更新操作自体です。行3の更新ロックが排他的にアップグレードされます 変更が実際に実行される直前のこの時点でロックします。この演算子は、出力句で指定されたデータも返します。 更新ステートメントの:
(output句によって生成された)updateステートメントの結果を以下に示します。
テーブルの最終的な状態は次のとおりです。
プロファイラートレースを使用して、実行中に取得されたロックを確認できます:
これは、更新が1つだけであることを示しています 行キーロックを取得します。この行が更新演算子に到達すると、ロックは排他的に変換されます ロック。ステートメントの最後に、ロックが解除されます。
トレース出力から、更新ロックされた行のロックハッシュ値が(98ec012aa510)であることがわかる場合があります。 私のテストデータベースで。次のクエリは、このロックハッシュが実際にクラスター化インデックスのRowID3に関連付けられていることを示しています。
SELECT RowID, %%LockRes%% FROM dbo.Test;
これらの例で取得された更新ロックは、UPDLOCK
を指定した場合に取得された更新ロックよりも有効期間が短いことに注意してください。 ヒント。これらの内部更新ロックはステートメントの最後で解放されますが、UPDLOCK
ロックはトランザクションの最後まで保持されます。
これで、RCSIが行のバージョン管理を使用する代わりに、現在コミットされているデータを読み取るために更新ロックを取得する場合のデモンストレーションを終了します。
RCSIでの共有およびキー範囲ロック
データベースエンジンがRCSIの下でロックを取得する可能性があるシナリオは他にもたくさんあります。これらの状況はすべて、古くなっている可能性のあるバージョン管理されたデータに依存することによって脅かされる可能性のある正確性を維持する必要性に関連しています。
外部キー検証のために取得された共有ロック
単純な外部キー関係にある2つのテーブルの場合、データベースエンジンは、古いバージョンの読み取りに依存することによって制約に違反しないようにするための手順を実行する必要があります。現在の実装では、これをコミットされた読み取りのロックに切り替えることで行います。 自動外部キーチェックの一部としてデータにアクセスする場合。
共有ロックを取得すると、整合性チェックで最新のコミットされたデータ(古いバージョンではない)、または同時実行中の変更によるブロックが確実に読み取られます。読み取りコミットのロックへの切り替えは、外部キーデータのチェックに使用される特定のアクセス方法にのみ適用されます。同じステートメント内の他のデータアクセスは、引き続き行バージョンを使用します。
この動作は、データを変更するステートメントにのみ適用され、変更は外部キー関係に直接影響します。参照される(親)テーブルへの変更の場合、これは、参照される値に影響を与える更新を意味します(NULL
に設定されている場合を除く) )およびすべての削除。参照(子)テーブルの場合、これはすべての挿入と更新を意味します(ここでも、キー参照がNULL
でない限り) )。 MERGE
のコンポーネント効果にも同じ考慮事項が適用されます。 。
共有ロックを取得する外部キールックアップを示す実行プランの例を以下に示します。
外部キーをカスケード処理するためにシリアル化可能
外部キー関係にカスケードアクションがある場合、正確さには、シリアル化可能な分離セマンティクスへのローカルエスカレーションが必要です。これは、カスケード参照アクションに対して実行されたキー範囲ロックが表示されることを意味します。前に見た更新ロックの場合と同様に、これらのキー範囲ロックは、トランザクションではなくステートメントにスコープされます。 RCSIで内部シリアル化可能ロックが取得される場所を示す実行プランの例を以下に示します。
その他のシナリオ
エンジンがロックの有効期間を自動的に延長したり、正確性を確保するために分離レベルをローカルにエスカレートしたりする特定のケースは他にもたくさんあります。これには、関連するインデックス付きビューを維持する場合、またはIGNORE_DUP_KEY
を持つインデックスを維持する場合に使用されるシリアル化可能なセマンティクスが含まれます。 オプションセット。
重要なメッセージは、RCSIはロックの量を減らすが、常に完全に排除できるとは限らないということです。
次回
このシリーズの次の投稿では、スナップショットアイソレーションレベルについて説明します。
[シリーズ全体のインデックスを参照]