コミットされた読み取りは2番目に弱いです SQL標準で定義されている4つの分離レベルのうち。それでも、SQLServerを含む多くのデータベースエンジンのデフォルトの分離レベルです。トランザクションの分離レベルとACIDプロパティに関する一連のこの投稿では、読み取りコミット分離によって実際に提供される論理的および物理的な保証について説明します。
論理的保証
SQL標準では、読み取りコミット分離で実行されているトランザクションは、コミットのみを読み取る必要があります。 データ。ダーティリードと呼ばれる同時実行現象を禁止することで、この要件を表現しています。ダーティリードは、2番目のトランザクションが完了する前に、トランザクションが別のトランザクションによって書き込まれたデータを読み取る場合に発生します。これを表現する別の方法は、トランザクションがコミットされていないデータを読み取るときにダーティ読み取りが発生すると言うことです。
この標準では、読み取りコミット分離で実行されているトランザクションが、繰り返し不可能な読み取りとして知られる同時実行現象に遭遇する可能性があることにも言及しています。 およびファントム 。多くの本は、トランザクションが後でデータを再読み込みした場合に変更されたデータ項目または新しいデータ項目を見ることができるという観点からこれらの現象を説明していますが、この説明は誤解を補強する可能性があります その並行性の現象は、複数のステートメントを含む明示的なトランザクション内でのみ発生する可能性があります。これはそうではありません。 単一のステートメント 明示的なトランザクションがないと、後で説明するように、繰り返し不可能な読み取りおよびファントム現象に対して同様に脆弱です。
これは、読み取りコミットされた分離に関して標準が言わなければならないことのほとんどすべてです。一見すると、コミットされたデータのみを読み取ることは、賢明な振る舞いをかなり保証しているように見えますが、いつものように、悪魔は詳細にあります。潜在的な抜け穴を探し始めるとすぐに この定義では、読み取りコミットされたトランザクションが期待する結果を生成しない可能性があるインスタンスを見つけるのは簡単になりすぎます。繰り返しになりますが、これらについては後ほど詳しく説明します。
さまざまな物理的実装
読み取りコミットされた分離レベルの観察された動作が異なるデータベースエンジンでかなり異なる可能性があることを意味する少なくとも2つのことがあります。まず、コミットされたデータのみを読み取るというSQL標準要件はしません 必然的に、トランザクションによって読み取られるコミットされたデータが最近になることを意味します コミットされたデータ。
データベースエンジンは、コミットされたバージョンの行を過去の任意の時点から読み取ることができます。 、SQL標準定義に準拠しています。いくつかの一般的なデータベース製品は、この方法で読み取りコミット分離を実装しています。読み取りコミット分離のこの実装の下で取得されたクエリ結果は、任意に古くなっている可能性があります 、データベースの現在のコミット状態と比較した場合。このトピックは、シリーズの次の投稿でSQLServerに適用されるため説明します。
次に注意したいのは、SQL標準定義はないということです。 ダーティリードの防止を超えて、特定の実装が追加の同時実行効果保護を提供することを排除します 。この標準では、ダーティ読み取りは許可されないことのみが指定されており、他の同時実行現象を許可する必要はありません。 任意の分離レベルで。
この2番目のポイントを明確にするために、標準に準拠したデータベースエンジンは、シリアライズ可能を使用してすべての分離レベルを実装できます。 そのように選択した場合の動作。一部の主要な商用データベースエンジンは、ダーティリードを単に防止するだけでなく、リードコミットの実装も提供します(ただし、 ACIDで完全な分離を提供することはできません。 言葉の意味)。
それに加えて、いくつかの人気のある製品については、コミット済みを読む 分離は最低です 利用可能な分離レベル。読み取りの実装コミットされていない 分離は、読み取りコミットとまったく同じです。これは標準で許可されていますが、これらの種類の違いにより、あるプラットフォームから別のプラットフォームにコードを移行するというすでに困難なタスクが複雑になります。分離レベルの動作について話すときは、通常、特定のプラットフォームも指定することが重要です。
私の知る限り、SQL Serverは、2つを提供するという点で主要な商用データベースエンジンの中でユニークです。 読み取りコミット分離レベルの実装。それぞれ、物理的な動作が大きく異なります。この投稿では、これらの最初のロックについて説明します。 コミット済みを読んでください。
SQLServerのロック読み取りがコミットされました
データベースオプションREAD_COMMITTED_SNAPSHOT
の場合 OFF
です 、SQLServerはロックを使用します 読み取りコミット分離レベルの実装。変更には共有ロックと互換性のない排他ロックが必要になるため、同時トランザクションがデータを同時に変更するのを防ぐために共有ロックが使用されます。
SQL Serverのロック読み取りコミットとロック反復可能読み取り(データの読み取り時に共有ロックも取得)の主な違いは、読み取りコミットが共有ロックをできるだけ早く解放することです。 、繰り返し可能な読み取りは、これらのロックを囲んでいるトランザクションの最後まで保持します。
コミットされた読み取りをロックすると、行の粒度でロックが取得され、行で取得された共有ロックが解放されます。 次の行で共有ロックが取得されたとき 。ページの粒度では、次のページの最初の行が読み取られると、共有ページのロックが解除されます。ロック粒度のヒントがクエリで提供されない限り、データベースエンジンは、どのレベルの粒度で開始するかを決定します。粒度のヒントはエンジンによる提案としてのみ扱われることに注意してください。要求されたものよりも粒度の低いロックが最初に取得される可能性があります。システム構成によっては、実行中に行またはページレベルからパーティションまたはテーブルレベルにロックがエスカレートされる場合もあります。
ここで重要な点は、共有ロックは通常、非常に短い時間しか保持されないということです。 ステートメントの実行中。 1つの一般的な誤解に明示的に対処するために、コミットされた読み取りをロックすることはしません ステートメントの最後まで共有ロックを保持します。
読み取りコミット動作のロック
SQL Serverのロック読み取りコミット実装で使用される短期間の共有ロックは、T-SQLプログラマーがデータベーストランザクションに一般的に期待する保証をほとんど提供しません。特に、読み取りコミットのロックの下で実行されているステートメント 分離:
- 同じ行に複数回遭遇する可能性があります;
- 一部の行を完全に見逃す可能性があります;および
- しません 特定の時点のビューを提供する データの
そのリストは、NOLOCK
の使用に関連する可能性のある奇妙な動作の説明のように見えるかもしれません。 ヒントですが、これらすべてのことは実際に可能であり、ロック読み取りコミット分離を使用すると発生します。
例
明白な単一ステートメントクエリを使用して、テーブル内の行をカウントするという単純なタスクを考えてみましょう。行ロックの粒度で読み取りコミット分離をロックすると、クエリは最初の行で共有ロックを取得し、それを読み取り、共有ロックを解放し、次の行に移動し、構造の最後に到達するまで続きます。読んでいます。この例では、クエリがインデックスbツリーをキーの昇順で読み取っていると仮定します(ただし、降順やその他の戦略を使用することもできます)。
単一行のみなので は任意の時点で共有ロックされているため、クエリが通過しているインデックスのロックされていない行を同時トランザクションで変更できることは明らかです。これらの同時変更によってインデックスキー値が変更されると、行がインデックス構造内を移動します。その可能性を念頭に置いて、次の図は、発生する可能性のある2つの問題のあるシナリオを示しています。
一番上の矢印は、インデックスキーが同時に変更されてすでにカウントされている行を示しています。これにより、行はインデックス内の現在のスキャン位置よりも先に移動します。つまり、行は2回カウントされます。 。 2番目の矢印は、スキャンがまだ検出されていない行がスキャン位置の後ろに移動していることを示しています。つまり、その行はカウントされません まったく。
特定の時点のビューではありません
前のセクションでは、コミットされた読み取りをロックすると、データが完全に失われたり、同じアイテムが複数回(運が悪ければ2回以上)カウントされたりする可能性があることを示しました。予期しない動作のリストの3番目の箇条書きは、コミットされた読み取りをロックしても、データの特定の時点のビューは提供されないことを示しています。
そのステートメントの背後にある理由は、今では見やすいはずです。たとえば、カウントクエリは、クエリの実行開始後に同時トランザクションによって挿入されたデータを簡単に読み取ることができます。同様に、クエリが表示するデータは、クエリの開始後と完了前の同時アクティビティによって変更される可能性があります。最後に、読み取りてカウントしたデータは、クエリが完了する前に同時トランザクションによって削除される可能性があります。
明らかに、読み取りコミットされた分離のロックの下で実行されているステートメントまたはトランザクションによって表示されるデータは、単一の状態がないに対応します。 特定の時点でのデータベースの 。私たちが遭遇するデータは、さまざまな時点からのものである可能性があります。唯一の共通の要因は、各アイテムが読み取られた時点でそのデータの最新のコミット値を表していることです。 (それ以降、変更または消失した可能性があります)。
これらの問題はどの程度深刻ですか?
単一ステートメントのクエリと明示的なトランザクションを論理的に瞬時に実行する、またはデータベースを使用するときにデータベースの単一のコミットされた特定の時点の状態に対して実行するものと考えることに慣れている場合、これはすべてかなり厄介な状態のように見えるかもしれません。デフォルトのSQLServer分離レベル。確かに、ACIDの意味での分離の概念にはうまく適合しません。
読み取りコミットされた分離をロックすることによって提供される保証の明らかな弱点を考えると、どのように何か 実稼働T-SQLコードの一部が適切に機能したことがあります。もちろん、シリアル化可能なレベルよりも低い分離レベルを使用すると、他の潜在的なメリットと引き換えに完全なACIDトランザクション分離を放棄することを受け入れることができますが、これらの問題が実際にどの程度深刻になると予想できますか?
行の欠落と二重カウント
これらの最初の2つの問題は、基本的に、キーを変更する同時アクティビティに依存しています。 現在スキャンしているインデックス構造で。 スキャンに注意してください ここには、インデックスの部分範囲スキャン部分が含まれますシーク 、およびおなじみの無制限のインデックスまたはテーブルスキャン。
キーが通常同時アクティビティによって変更されないインデックス構造を(範囲で)スキャンしている場合、これらの最初の2つの問題は実際的な問題ではないはずです。ただし、クエリプランは別のアクセス方法を使用するように変更される可能性があり、新しい検索されたインデックスには揮発性キーが組み込まれている可能性があるため、これについて確実にすることは困難です。
また、多くの本番クエリで実際に必要なのは概算のみであることも覚えておく必要があります。 またはとにかくいくつかのタイプの質問に対するベストエフォートの答え。一部の行が欠落しているか、二重にカウントされているという事実は、より広範なスキームではそれほど重要ではない可能性があります。同時に多くの変更が行われるシステムでは、結果がだったことを確認するのが難しい場合もあります。 データが頻繁に変更されることを考えると、不正確です。このような状況では、データコンシューマーの目的には、大まかに正しい答えで十分な場合があります。
特定時点のビューはありません
3番目の問題(データのいわゆる「一貫性のある」ポイントインタイムビューの問題)も、同じ種類の考慮事項に帰着します。不整合がデータコンシューマーからの厄介な質問につながる傾向があるレポートの目的では、スナップショットビューがしばしば好まれます。その他の場合、データの特定の時点のビューの欠如から生じる種類の不整合は許容できる可能性があります。
問題のあるシナリオ
記載されている懸念事項がなる場合もたくさんあります。 重要です。たとえば、ビジネスルールを適用するコードを記述した場合 T-SQLでは、正確性を保証するために、分離レベルを慎重に選択する(または他の適切なアクションを実行する)必要があります。多くのビジネスルールは、外部キーまたは制約を使用して適用できます。この場合、分離レベルの選択の複雑さは、データベースエンジンによって自動的に処理されます。一般的な経験則として、組み込みの宣言型整合性のセットを使用します T-SQLで独自のルールを作成するよりも、機能の方が望ましいです。
ビジネスルール自体を完全に強制しない別の幅広いクラスのクエリがあります 、ただし、デフォルトのロッキング読み取りコミット分離レベルで実行すると、残念な結果が生じる可能性があります。これらのシナリオは、銀行口座間で送金する、またはリンクされた多数の口座の残高がゼロを下回らないようにするという、よく引用される例ほど明白ではありません。たとえば、期限切れの請求書を、厳しい言葉で書かれたリマインダーレターを送信するプロセスへの入力として識別する次のクエリについて考えてみます。
INSERT dbo.OverdueInvoices SELECT I.InvoiceNumber FROM dbo.Invoices AS INV WHERE INV.TotalDue > ( SELECT SUM(P.Amount) FROM dbo.Payments AS P WHERE P.InvoiceNumber = I.InvoiceNumber );
明らかに、請求書を分割払いで全額支払った人に手紙を送りたくないのは、クエリの実行時にデータベースアクティビティが同時に発生したために、誤った合計が計算されたためです。 受け取った支払いの。もちろん、実際の本番システムでの実際のクエリは、上記の単純な例よりもはるかに複雑になることがよくあります。
今日の締めくくりとして、次のクエリを見て、意図しない何かが発生する機会がいくつあるかを確認します。そのようなクエリが、ロックされた読み取りコミット分離レベルで同時に実行された場合(おそらく他の無関係なトランザクション中に) Casesテーブルも変更しています):
-- Allocate the oldest unallocated case ID to -- the current case worker, while ensuring -- the worker never has more than three -- active cases at once. UPDATE dbo.Cases SET WorkerID = @WorkerID WHERE CaseID = ( -- Find the oldest unallocated case ID SELECT TOP (1) C2.CaseID FROM dbo.Cases AS C2 WHERE C2.WorkerID IS NULL ORDER BY C2.DateCreated DESC ) AND ( SELECT COUNT_BIG(*) FROM dbo.Cases AS C3 WHERE C3.WorkerID = @WorkerID ) < 3;
この分離レベルでクエリが失敗する可能性のあるすべての小さな方法を探し始めると、停止するのが難しい場合があります。完全に分離された特定の時点で正確な結果が実際に必要であるという前述の注意事項に留意してください。読み取りコミットを使用して行うトレードオフを認識している限り、十分に良好な結果を返すクエリを作成することはまったく問題ありません。
次回
このシリーズの次のパートでは、SQLServerで使用可能な読み取りコミット済み分離の2番目の物理実装である読み取りコミット済みスナップショット分離について説明します。
[シリーズ全体のインデックスを参照]