並行性の問題は、マルチスレッドプログラミングが難しいのと同じように難しいです。シリアル化可能な分離を使用しない限り、他のユーザーが同時にデータベースに変更を加えているときに常に正しく機能するT-SQLトランザクションをコーディングするのは難しい場合があります。
問題の「トランザクション」が単純な単一のSELECT
であっても、潜在的な問題は自明ではない可能性があります。 声明。データの読み取りと書き込みを行う複雑なマルチステートメントトランザクションの場合、高い同時実行性の下で予期しない結果やエラーが発生する可能性は、すぐに圧倒される可能性があります。ランダムなロックのヒントやその他の試行錯誤の方法を適用して、微妙で再現が難しい同時実行の問題を解決しようとすると、非常に苛立たしい経験になる可能性があります。
多くの点で、スナップショットアイソレーションレベルは、これらの同時実行の問題に対する完璧なソリューションのようです。基本的な考え方は、各スナップショットトランザクションは、トランザクションの開始時に取得された、データベースのコミットされた状態の独自のプライベートコピーに対して実行されたかのように動作するというものです。コミットされたデータの不変のビューをトランザクション全体に提供することで、読み取り専用操作の一貫した結果が保証されることは明らかですが、データを変更するトランザクションについてはどうでしょうか?
スナップショットアイソレーションは、同時ライター間の競合が比較的まれであると暗黙的に想定して、データ変更を楽観的に処理します。書き込みの競合が発生した場合、最初のコミッターが勝ち、負けたトランザクションの変更はロールバックされます。もちろん、ロールバックされたトランザクションには不幸ですが、これが非常にまれな場合は、スナップショットアイソレーションの利点が、時折発生する障害と再試行のコストを簡単に上回ります。
スナップショット分離の比較的単純でクリーンなセマンティクス(代替手段と比較した場合)は、特にデータベースの世界で排他的に作業しておらず、したがってさまざまな分離レベルをよく知らない人々にとって、大きな利点になる可能性があります。経験豊富なデータベースの専門家でさえ、比較的「直感的な」分離レベルは歓迎すべき救済策です。
もちろん、物事が最初に表示されるほど単純になることはめったになく、スナップショットアイソレーションも例外ではありません。公式ドキュメントは、スナップショットアイソレーションの主な長所と短所を説明するのに非常に優れているため、この記事の大部分は、あまり知られていない驚くべき問題のいくつかを調査することに集中しています。ただし、最初に、この分離レベルの論理プロパティを簡単に確認します。
ACIDプロパティとスナップショットアイソレーション
スナップショットアイソレーションは、SQL標準で定義されている分離レベルの1つではありませんが、そこで定義されている「同時実行現象」を使用して比較されることがよくあります。たとえば、次の比較表は、SQLServerの技術記事「SQLServer2005の行バージョンベースのトランザクション分離」(Kimberly L.TrippとNealGravesによる)から複製されたものです。
特定の時点のビューを提供する コミットされたデータの 、スナップショットアイソレーションは、そこに示されている3つの同時実行現象すべてに対する保護を提供します。コミットされたデータのみが表示されるため、ダーティリードが防止され、スナップショットの静的な性質により、繰り返し不可能なリードとファントムの両方に遭遇することはありません。
ただし、この比較(特に強調表示されたセクション)は、スナップショットとシリアル化可能な分離レベルが同じ3つの特定の現象を防ぐことを示しているだけです。それはそれらがすべての点で同等であるという意味ではありません。重要なのは、SQL-92標準では、3つの現象だけでシリアル化可能な分離が定義されていないことです。規格のセクション4.28に完全な定義があります:
分離レベルSERIALIZABLEでの同時SQLトランザクションの実行は、シリアライズ可能であることが保証されています。シリアル化可能な実行とは、SQLトランザクションを同時に実行する操作の実行であり、同じSQLトランザクションのシリアル実行と同じ効果が得られると定義されています。シリアル実行とは、次のSQLトランザクションが開始する前に、各SQLトランザクションが完了するまで実行される実行です。
ここでの暗黙の保証の範囲と重要性は、見過ごされがちです。簡単な言葉で言うには:
単独で実行したときに正しく実行されるシリアル化可能なトランザクションは、同時トランザクションの任意の組み合わせで引き続き正しく実行されるか、エラーメッセージ(通常はSQL Serverの実装でのデッドロック)とともにロールバックされます。
スナップショット分離を含む、シリアル化できない分離レベルは、正確性について同じ強力な保証を提供しません。
古いデータ
スナップショットアイソレーションは、ほとんど魅力的に単純なようです。読み取りは常に単一の時点のコミットされたデータから行われ、書き込みの競合は自動的に検出されて処理されます。これは、並行性に関連するすべての問題に対する完全なソリューションではないのはなぜですか?
潜在的な問題の1つは、スナップショットの読み取りが必ずしもデータベースの現在のコミット状態を反映していないことです。スナップショットトランザクションは、スナップショットトランザクションの開始後に、他の同時トランザクションによって行われたコミットされた変更を完全に無視します。別の言い方をすれば、スナップショットトランザクションで古いデータや古いデータが表示されるということです。この動作は、正確なポイントインタイムレポートを生成するために必要なものである可能性がありますが、他の状況(たとえば、トリガーでルールを適用するために使用される場合)ではあまり適切ではない可能性があります。
スキューを書く
スナップショットアイソレーションは、書き込みスキューと呼ばれる多少関連する現象に対しても脆弱です。古いデータの読み取りがこれに関与しますが、この問題は、スナップショットの「書き込み競合検出」が実行することと実行しないことを明確にするのにも役立ちます。
書き込みスキューは、2つの同時トランザクションがそれぞれ、他のトランザクションが変更するデータを読み取るときに発生します。 2つのトランザクションが異なる行を変更するため、書き込みの競合は発生しません。どちらのトランザクションも、他のトランザクションによって行われた変更を認識しません。これは、両方が、それらの変更が行われる前の時点から読み取られているためです。
書き込みスキューの典型的な例は、白と黒の大理石の問題ですが、ここで別の簡単な例を示したいと思います:
-- Create two empty tables CREATE TABLE A (x integer NOT NULL); CREATE TABLE B (x integer NOT NULL); -- Connection 1 SET TRANSACTION ISOLATION LEVEL SNAPSHOT; BEGIN TRANSACTION; INSERT A (x) SELECT COUNT_BIG(*) FROM B; -- Connection 2 SET TRANSACTION ISOLATION LEVEL SNAPSHOT; BEGIN TRANSACTION; INSERT B (x) SELECT COUNT_BIG(*) FROM A; COMMIT TRANSACTION; -- Connection 1 COMMIT TRANSACTION;
スナップショットアイソレーションでは、そのスクリプトの両方のテーブルは、ゼロ値を含む単一の行になります。これは正しい結果ですが、シリアル化可能な結果ではありません。可能なシリアルトランザクションの実行順序に対応していません。真のシリアルスケジュールでは、一方のトランザクションがもう一方のトランザクションを開始する前に完了する必要があるため、2番目のトランザクションは最初のトランザクションによって挿入された行をカウントします。これは技術的なことのように聞こえるかもしれませんが、強力なシリアル化可能な保証は、トランザクションが本当にシリアル化可能である場合にのみ適用されることを覚えておいてください。
競合検出の微妙さ
スナップショット書き込みの競合は、スナップショットトランザクションの開始後にコミットされた別のトランザクションによって変更された行をスナップショットトランザクションが変更しようとするたびに発生します。ここには2つの微妙な点があります:
- トランザクションは実際に変更する必要はありません 任意のデータ値。および
- トランザクションは、共通の列を変更する必要はありません。 。
次のスクリプトは、両方のポイントを示しています。
-- Test table CREATE TABLE dbo.Conflict ( ID1 integer UNIQUE, Value1 integer NOT NULL, ID2 integer UNIQUE, Value2 integer NOT NULL ); -- Insert one row INSERT dbo.Conflict (ID1, ID2, Value1, Value2) VALUES (1, 1, 1, 1); -- Connection 1 BEGIN TRANSACTION; UPDATE dbo.Conflict SET Value1 = 1 WHERE ID1 = 1; -- Connection 2 SET TRANSACTION ISOLATION LEVEL SNAPSHOT; BEGIN TRANSACTION; UPDATE dbo.Conflict SET Value2 = 1 WHERE ID2 = 1; -- Connection 1 COMMIT TRANSACTION;
次の点に注意してください:
- 各トランザクションは、異なるインデックスを使用して同じ行を検索します
- どちらの更新でも、すでに保存されているデータは変更されません
- 2つのトランザクションは行の異なる列を「更新」します。
それにもかかわらず、最初のトランザクションがコミットすると、2番目のトランザクションは更新の競合エラーで終了します:
概要:競合の検出は常に行全体のレベルで動作し、「更新」は実際にデータを変更する必要はありません。 (不思議に思うかもしれませんが、行外のLOBまたはSLOBデータへの変更は、競合検出の目的で行への変更としてもカウントされます)。
外部キーの問題
競合の検出は、外部キー関係の親行にも適用されます。スナップショットアイソレーションで子行を変更する場合、別のトランザクションで親行を変更すると、競合が発生する可能性があります。以前と同様に、このロジックは親行全体に適用されます。親の更新が外部キー列自体に影響を与える必要はありません。実行プランで自動外部キーチェックを必要とする子テーブルに対する操作は、予期しない競合を引き起こす可能性があります。
これを実証するために、最初に次のテーブルとサンプルデータを作成します。
CREATE TABLE dbo.Dummy ( x integer NULL ); CREATE TABLE dbo.Parent ( ParentID integer PRIMARY KEY, ParentValue integer NOT NULL ); CREATE TABLE dbo.Child ( ChildID integer PRIMARY KEY, ChildValue integer NOT NULL, ParentID integer NULL FOREIGN KEY REFERENCES dbo.Parent ); INSERT dbo.Parent (ParentID, ParentValue) VALUES (1, 1); INSERT dbo.Child (ChildID, ChildValue, ParentID) VALUES (1, 1, 1);
コメントに示されているように、2つの別々の接続から次を実行します。
-- Connection 1 SET TRANSACTION ISOLATION LEVEL SNAPSHOT; BEGIN TRANSACTION; SELECT COUNT_BIG(*) FROM dbo.Dummy; -- Connection 2 (any isolation level) UPDATE dbo.Parent SET ParentValue = 1 WHERE ParentID = 1; -- Connection 1 UPDATE dbo.Child SET ParentID = NULL WHERE ChildID = 1; UPDATE dbo.Child SET ParentID = 1 WHERE ChildID = 1;
ダミーテーブルからの読み取りは、スナップショットトランザクションが正式に開始されたことを確認するためにあります。 BEGIN TRANSACTION
を発行する これを行うには十分ではありません。ユーザーテーブルに対して何らかのデータアクセスを実行する必要があります。
参照列をNULL
に設定しているため、子テーブルを最初に更新しても競合は発生しません。 実行プランで親テーブルをチェックする必要はありません(チェックするものはありません)。クエリプロセッサは実行プランの親行にアクセスしないため、競合は発生しません。
子テーブルの2回目の更新では、外部キーチェックが自動的に実行されるため、競合が発生します。親行がクエリプロセッサによってアクセスされると、更新の競合もチェックされます。この場合、スナップショットトランザクションの開始後に、参照されている親行でコミットされた変更が発生したため、エラーが発生します。親テーブルの変更は外部キー列自体には影響しなかったことに注意してください。
子テーブルへの変更が作成された親行を参照している場合にも、予期しない競合が発生する可能性があります。 同時トランザクション(およびスナップショットトランザクションの開始後にコミットされたトランザクション)によって。
概要:自動外部キーチェックを含むクエリプランは、スナップショットトランザクションの開始後に参照された行に何らかの変更(作成を含む)が発生した場合、競合エラーをスローする可能性があります。
テーブルの切り捨ての問題
スナップショットトランザクションは、トランザクションの開始以降にアクセスするテーブルが切り捨てられている場合、エラーで失敗します。これは、以下のスクリプトが示すように、切り捨てられたテーブルに最初から行がない場合でも適用されます。
CREATE TABLE dbo.AccessMe ( x integer NULL ); CREATE TABLE dbo.TruncateMe ( x integer NULL ); -- Connection 1 SET TRANSACTION ISOLATION LEVEL SNAPSHOT; BEGIN TRANSACTION; SELECT COUNT_BIG(*) FROM dbo.AccessMe; -- Connection 2 TRUNCATE TABLE dbo.TruncateMe; -- Connection 1 SELECT COUNT_BIG(*) FROM dbo.TruncateMe;
最後のSELECTはエラーで失敗します:
これは、既存のデータベースでスナップショットアイソレーションを有効にする前に確認する必要があるもう1つの微妙な副作用です。
次回
このシリーズの次の(そして最後の)投稿では、読み取りのコミットされていない分離レベル(愛情を込めて「nolock」として知られています)について説明します。
[シリーズ全体のインデックスを参照]