web-dev-qa-db-ja.com

SQL Server-別のトランザクションで挿入中に選択すると予期しない結果が発生する

トランザクションとロックに関する私の知識を根本的に変える状況に遭遇しましたが(私はあまり知りません)、それを理解するための助けが必要です。

次のようなテーブルがあるとします。

CREATE TABLE [dbo].[SomeTable](
[Id] [bigint] IDENTITY(1,1) NOT NULL,
[SomeData] [varchar](200) NOT NULL,
[Moment] [datetime] NOT NULL,
[SomeInt] [bigint] NOT NULL
) ON [PRIMARY]

そして、私はこの「トランザクション内に1000行を挿入する」クエリを実行します。

BEGIN TRAN t1

DECLARE @i INT = 0

WHILE @i < 1000
BEGIN
    SET @i = @i + 1

    INSERT INTO [SomeTable] ([SomeData] ,Moment, SomeInt)
    VALUES (CONVERT(VARCHAR(255), NEWID()), getdate(), @i)

    WAITFOR DELAY '00:00:00:010'
END

COMMIT TRAN t1

このトランザクションが実行されている間、私は単純な選択を実行しています:

SELECT Id, Moment, SomeData, SomeInt FROM [SomeTable]

常にそれを再現できるとは限りません(どうやらタイミングに依存するようです)が、selectクエリは、挿入トランザクションが完了した後、1000行未満を返すことがあります。私の知らないところで、selectは常に1000行を返すと信じていましたが(分離レベルがRead Committedの場合)、トランザクションとロックがどのように機能するかを誤解しています。

ただし、(クラスター化インデックスを生成する)Id列に主キーを置くと、selectクエリは、私が試した限り、1000行すべてを返します。複合キーにクラスター化インデックスを使用し、他のいくつかの列に非クラスター化インデックスを使用して、インデックスを別の方法で配置すると、予想よりも少ない数の行が返される可能性があります。

だから、私はこれらの質問があります:

  1. Selectがトランザクションによってコミットされたすべての行を常に返さないのはなぜですか?
  2. これが予想される動作である場合、実際にそれを期待どおりに機能させる最良の方法は何ですか?基本的に、トランザクションの後で(または前に)テーブルの状態を返すように選択したいのです。現在、スナップショット分離はオプションではありません。 TABLOCKを置くことは仕事をしているようですが、より良い解決策はありますか?実際には、絶対に必要ではない場合にこのレベルでロックしたくないテーブルがあります。
  3. インデックスを付けるとこの動作が変わるのはなぜですか?

前もって感謝します。

8
poke

コードを数回実行した後、これを再現することができませんでした。

ただし、ファイルの前のページに後の行が挿入されたときに発生する必要があると思います。

したがって、操作の順序は(たとえば)

  • 200、207、223ページのヒープに挿入された行
  • Selectステートメントが開始し、割り当て順スキャンを実行します。最初のページが200であり、行ロックが解放されるのを待ってブロックされていることがわかります。
  • 他の行は最初のトランザクションによって挿入されます。それらのいくつかは200より前のページに割り当てられます。挿入トランザクションコミット。
  • 行ロックが解放され、割り当て順スキャンが続行されます。ファイルの前の行は失われます。

表は10ページで構成されています。デフォルトでは、最初の8ページは混合エクステントから割り当てられ、次に均一エクステントが割り当てられます。多分あなたのケースでは、使用された混合エクステントより前の空き均一エクステントのためのスペースがファイルで利用可能でした。

この理論をテストするには、問題を再現した後で別のウィンドウで次のコマンドを実行し、元のSELECTから欠落している行がすべてこの結果セットの先頭に表示されるかどうかを確認します。

SELECT [SomeData],
       Moment,
       SomeInt,
       file_id,
       page_id,
       slot_id
FROM   [SomeTable] 
/*Undocumented - Use at own risk*/
CROSS APPLY sys.fn_PhysLocCracker(%% physloc %%)
ORDER BY page_id, SomeInt

インデックス付きテーブルに対する操作は、割り当ての順序ではなくインデックスキーの順序になるため、この特定のシナリオによる影響はありません。

インデックスに対して割り当て順スキャンを実行できますが、それは、テーブルが十分に大きく、分離レベルがコミットされずに読み取られるか、テーブルロックが保持されている場合にのみ考慮されます。

コミットされた読み取りは通常、データが読み取られるとすぐにロックを解放するため、インデックスに対するスキャンで行を2回読み取るか、まったく読み取らない可能性があります(インデックスキーが同時トランザクションによって更新され、行が前後に移動する場合)。 )このタイプの問題の詳細については、 Read Committed Isolation Level を参照してください。


ちなみに、元々、インデックスが挿入順序(Id、Moment、SomeIntのいずれか)に対して増加する列の1つにあるというインデックス付きのケースを想定していました。ただし、クラスター化インデックスがランダムSomeDataにある場合でも、問題は発生しません。

私は試した

DBCC TRACEON(3604, 1200, -1) /*Caution. Global trace flag. Outputs lock info
                               on every connection*/

SELECT TOP 2 *,
             %%LOCKRES%%
FROM   [SomeTable] WITH(nolock)
ORDER BY [SomeData];

SELECT *,
       %%LOCKRES%%
FROM   [SomeTable]
ORDER BY [SomeData];

/*Turn off trace flags. Doesn't check whether or not they were on already 
  before we started, with TRACEOFF*/
DBCC TRACEOFF(3604, 1200, -1)

結果は以下の通りでした

enter image description here

2番目の結果セットには、1,000行すべてが含まれます。ロック情報は、ロックが解放されたときにロックリソース24c910701749での待機がブロックされていたとしても、それがスキャンを続行しないだけではないことを示しています。その時点から。代わりに、そのロックを直ちに解放し、新しい最初の行の行ロックを取得します。

enter image description here

12
Martin Smith