SQL Server IDの値を順番に読み取ることに依存できますか?


24

TL; DR:次の質問に要約します:行を挿入するとき、新しい値の生成とクラスター化インデックス内の対応する行キーのロックの間に機会がありますか?外部オブザーバーはより新しいものを見ることができます並行トランザクションによって挿入された値?(SQL Serverで。)Identity Identity

詳細バージョン

Identityという名前の列を持つSQL ServerテーブルがありますCheckpointSequence。これは、テーブルのクラスター化インデックスのキーです(これには、追加の非クラスター化インデックスもいくつかあります)。行は、いくつかの並行プロセスとスレッドによって(分離レベルで、なしで)テーブルに挿入されます。同時に、クラスター化インデックスから定期的に行を読み取るプロセスがあり、その列で並べ替えられます(分離レベルでも、オプションはオフになっています)。READ COMMITTEDIDENTITY_INSERTCheckpointSequenceREAD COMMITTEDREAD COMMITTED SNAPSHOT

私は現在、読み取りプロセスがチェックポイントを「スキップ」できないという事実に依存しています。私の質問は、このプロパティに依存できますか?そうでない場合、それを実現するために何ができますか?

例:ID値が1、2、3、4、および5 の行を挿入する場合、値4の行を表示する前に、値5の行を参照してはなりません。テストは、ORDER BY CheckpointSequence句(そしてWHERE CheckpointSequence > -1確実句)、ブロック行4、行5が既にコミットされていても、読まれるべきであるが、まだコミットされていない時はいつでも。

少なくとも理論的には、この仮定を破る可能性のある競合状態がここにあると思います。残念ながら、上のドキュメントでIdentityIdentity、複数の同時トランザクションのコンテキストでどのように機能するかについてはあまり言及されていません。「特定のトランザクションの新しい値はそれぞれ、テーブル上の他の同時トランザクションとは異なります。」(MSDN

私の推論は、それはこのように何らかの形で動作する必要があります:

  1. トランザクションが(明示的または暗黙的に)開始されます。
  2. ID値(X)が生成されます。
  3. 対応する行ロックは、ID値に基づいてクラスター化インデックスで取得されます(ロックエスカレーションが開始されない限り、この場合、テーブル全体がロックされます)。
  4. 行が挿入されます。
  5. トランザクションがコミットされる(おそらくかなり長い時間後に)ので、ロックは再び削除されます。

ステップ2と3の間に、非常に小さなウィンドウがあると思います

  • 同時セッションは次のID値(X + 1)を生成し、残りのすべてのステップを実行できます。
  • したがって、その時点で正確に来ている読者が値X + 1を読み取ることができ、Xの値は失われます。

もちろん、これの可能性は非常に低いようです。しかし、まだ-それは起こる可能性があります。それともできますか?

(コンテキストに興味がある場合:これは、NEventStoreのSQL Persistence Engineの実装です。NEventStoreは、すべてのイベントが新しい昇順チェックポイントシーケンス番号を取得する追加専用のイベントストアを実装します。クライアントは、チェックポイント順にイベントストアからイベントを読み取りますすべての種類の計算を実行するため。チェックポイントXのイベントが処理されると、クライアントは「新しい」イベント、つまりチェックポイントX + 1以上のイベントのみを考慮します。したがって、イベントをスキップしないことが重要です。現在、Identityベースのチェックポイント実装がこの要件を満たしているかどうかを判断しようとしています。これらは、使用されている正確なSQLステートメントです。SchemaWriterのquery読者の質問。)

私が正しいと上記の状況が発生する可能性がある場合、それらに対処する2つのオプションのみが表示されますが、どちらも不十分です。

  • Xを確認する前にチェックポイントシーケンス値X + 1を確認した場合は、X + 1を終了し、後で再試行してください。ただし、Identityもちろんギャップが発生する可能性があるため(たとえば、トランザクションがロールバックされるとき)、Xが来ることはありません。
  • したがって、同じアプローチですが、nミリ秒後にギャップを受け入れます。ただし、nのどの値を想定する必要がありますか?

より良いアイデアはありますか?


IDの代わりにSequenceを使用してみましたか?IDを使用すると、どの挿入が特定のID値を取得するかを確実に予測できるとは思いませんが、これはシーケンスを使用しても問題になりません。もちろん、それはあなたの今のやり方を変えます。
アントワーヌ・ヘルナンデス

@SoleDBAGuyシーケンスは、上記で説明した競合状態をさらに発生させませんか?新しいシーケンス値Xを生成し(上記の手順2を置き換えます)、行を挿入します(手順3および4)。2と3の間に、他の誰かが次のシーケンス値X + 1を生み出す可能性がある、それをコミットし、私もシーケンス値Xと私の行を挿入するに着く前に、読者はその値X + 1を読み込む
ファビアン・Schmied

回答:


26

行を挿入するときに、新しいID値の生成とクラスター化インデックス内の対応する行キーのロックの間に機会があります。外部オブザーバーは、同時トランザクションによって挿入された新しいID値を見ることができますか?

はい。

アイデンティティ値配分は含んでいるユーザトランザクションから独立しています。これは、トランザクションがロールバックされてもID値が消費される理由の1つです。インクリメント操作自体は、破損を防ぐためにラッチで保護されていますが、それが保護の範囲です。

実装の特定の状況では、挿入のユーザートランザクションがアクティブになる前(およびロックが取得されるCMEDSeqGen::GenerateNewValue)にID割り当て(への呼び出し)が行われます

ID値がインクリメントされて割り当てられた直後に1つのスレッドをフリーズできるように、デバッガーを接続して2つの挿入を同時に実行することで、次のようなシナリオを再現することができました。

  1. セッション1はID値を取得します(3)
  2. セッション2はID値を取得します(4)
  3. セッション2は挿入とコミットを実行します(したがって、行4は完全に表示されます)
  4. セッション1は挿入とコミットを実行します(行3)

ステップ3の後、コミットされたロック読み取りでrow_numberを使用するクエリは次を返しました。

スクリーンショット

実装では、これにより、チェックポイントID 3が誤ってスキップされます。

機会の機会は比較的小さいが、存在する。デバッガーをアタッチするよりも現実的なシナリオを提供するには:上記の手順1の後、クエリスレッドを実行すると、スケジューラーが生成されます。これにより、元のスレッドが挿入の実行を再開する前に、2番目のスレッドがID値を割り当て、挿入およびコミットできます。

明確にするために、割り当てられた後、使用される前にID値を保護するロックまたはその他の同期オブジェクトはありません。たとえば、上記の手順1の後、同時トランザクションはIDENT_CURRENT、テーブルに行が存在する前(コミットされていない場合でも)と同様に、T-SQL関数を使用して新しいID値を確認できます。

基本的に、文書化されたものよりもアイデンティティ値に関する保証はありません:

  • それぞれの新しい値は、現在のシードと増分に基づいて生成されます。
  • 特定のトランザクションの新しい値はそれぞれ、テーブル上の他の同時トランザクションとは異なります。

それは本当にそれです。

場合は、厳密なトランザクションFIFO処理が必要とされ、おそらく選択肢がないが、手動でシリアライズします。アプリケーションの要件が少ない場合は、より多くのオプションがあります。その点で、問題は完全に明確ではありません。それでも、Remus Rusanuの記事「Using Tables as Queues」で役立つ情報を見つけることができます。


7

ポール・ホワイトが完全に正しいと答えたように、一時的に「スキップされた」ID行が存在する可能性があります。以下は、このケースを自分用に再現するためのほんの小さなコードです。

データベースとテストテーブルを作成します。

create database IdentityTest
go
use IdentityTest
go
create table dbo.IdentityTest (ID int identity, c1 char(10))
create clustered index CI_dbo_IdentityTest_ID on dbo.IdentityTest(ID)

C#コンソールプログラムでこのテーブルで同時挿入と選択を実行します。

using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Threading;

namespace IdentityTest
{
    class Program
    {
        static void Main(string[] args)
        {
            var insertThreads = new List<Thread>();
            var selectThreads = new List<Thread>();

            //start threads for infinite inserts
            for (var i = 0; i < 100; i++)
            {
                insertThreads.Add(new Thread(InfiniteInsert));
                insertThreads[i].Start();
            }

            //start threads for infinite selects
            for (var i = 0; i < 10; i++)
            {
                selectThreads.Add(new Thread(InfiniteSelectAndCheck));
                selectThreads[i].Start();
            }
        }

        private static void InfiniteSelectAndCheck()
        {
            //infinite loop
            while (true)
            {
                //read top 2 IDs
                var cmd = new SqlCommand("select top(2) ID from dbo.IdentityTest order by ID desc")
                {
                    Connection = new SqlConnection("Server=localhost;Database=IdentityTest;Integrated Security=SSPI;Application Name=IdentityTest")
                };

                try
                {
                    cmd.Connection.Open();
                    var dr = cmd.ExecuteReader();

                    //read first row
                    dr.Read();
                    var row1 = int.Parse(dr["ID"].ToString());

                    //read second row
                    dr.Read();
                    var row2 = int.Parse(dr["ID"].ToString());

                    //write line if row1 and row are not consecutive
                    if (row1 - 1 != row2)
                    {
                        Console.WriteLine("row1=" + row1 + ", row2=" + row2);
                    }
                }
                finally
                {
                    cmd.Connection.Close();
                }
            }
        }

        private static void InfiniteInsert()
        {
            //infinite loop
            while (true)
            {
                var cmd = new SqlCommand("insert into dbo.IdentityTest (c1) values('a')")
                {
                    Connection = new SqlConnection("Server=localhost;Database=IdentityTest;Integrated Security=SSPI;Application Name=IdentityTest")
                };

                try
                {
                    cmd.Connection.Open();
                    cmd.ExecuteNonQuery();
                }
                finally
                {
                    cmd.Connection.Close();
                }
            }
        }
    }
}

このコンソールは、読み取りスレッドの1つがエントリを「ミス」した場合に、すべてのケースについて行を出力します。


1
素敵なコードですが、連続したIDのみをチェックします(「// row1とrowが連続していない場合は行を書き込みます」)。コードが印刷するギャップが生じる場合があります。それは、これらのギャップが後で埋められるということではありません。
ypercubeᵀᴹ

1
コードはIDENTITYギャップを生成するシナリオ(トランザクションのロールバックなど)をトリガーしないため、印刷された行には実際に「スキップされた」値が表示されます(または、少なくともマシンで実行してチェックしたときに表示されました)。非常に素晴らしい再現サンプル!
ファビアンシュミード

5

ギャップを残すことができる多くのシナリオがあるため、IDが連続しているとは思わないのが最善です。アイデンティティを抽象的な数字のように考え、ビジネス上の意味をそれに付けない方が良いです。

基本的に、INSERT操作をロールバックする(または明示的に行を削除する)とギャップが発生し、テーブルプロパティIDENTITY_INSERTをONに設定すると重複が発生する可能性があります。

ギャップは次の場合に発生する可能性があります。

  1. レコードが削除されます。
  2. 新しいレコードを挿入しようとしたときにエラーが発生しました(ロールバック)
  3. 明示的な値による更新/挿入(identity_insertオプション)。
  4. 増分値が1を超えています。
  5. トランザクションがロールバックします。

列のidentityプロパティは保証されていません。

•一意性

•トランザクション内の連続した値。値が連続している必要がある場合、トランザクションはテーブルの排他ロックを使用するか、SERIALIZABLE分離レベルを使用する必要があります。

•サーバーの再起動後の連続値。

•値の再利用。

このためID値を使用できない場合は、現在の値を保持する別のテーブルを作成し、アプリケーションへのテーブルと番号の割り当てへのアクセスを管理します。これには、パフォーマンスに影響を与える可能性があります。

https://msdn.microsoft.com/en-us/library/ms186775(v=sql.105).aspx
https://msdn.microsoft.com/en-us/library/ms186775(v=sql.110) .aspx


ギャップは私の主な問題ではないと思います-私の主な問題は価値の昇順の可視性です。(つまり、アイデンティティ値7は、アイデンティティ値6よりも前に、その値によって順序付けされたクエリに対して観察可能ではありません。)
ファビアンシュミード

1
ID値が
1、2、5、3、4の

確かに、これは簡単に再現できます。たとえば、レナートの答えのシナリオを使用します。私が苦労しているのは、句を含むクエリを使用するときに、そのコミット順序(クラスター化インデックスの順序)を観察できるかどうかですORDER BY CheckpointSequence。要するに、Identity値の生成がINSERTステートメントによって行われたロックにリンクされているのか、それとも単にSQL Serverによって次々に実行される2つの無関係なアクションであるのかという質問に帰着すると思います。
ファビアンシュミード

1
クエリとは何ですか?コミットされた読み取りを使用している場合、例では、order byは1、2、3、5を示します。これらはコミットされており、4はコミットされていない、つまりダーティリードです。また、NEventStoreの説明には、「したがって、イベントが再び考慮されることはないため、イベントをスキップしないことが重要です」と述べられています。
-stacylaray

クエリは上記のとおりです(gist.github.com/fschmied/47f716c32cb64b852f90)-ページングされますが、簡単に要約されSELECT ... FROM Commits WHERE CheckpointSequence > ... ORDER BY CheckpointSequenceます。このクエリは、ロックされた行4を超えて読み込まれるとは思わないでしょうか?(私の実験では、クエリが行4のKEYロックを取得しようとするとブロックされます。)
ファビアンシュミード

1

サーバーに大きな負荷がかかると、トラブルが発生する可能性があります。2つのトランザクションを検討します。

  1. T1:Tへの挿入...-5が挿入されると言う
  2. T2:Tへの挿入...-6が挿入されると言う
  3. T2:コミット
  4. リーダーには6が表示されるが5は表示されない
  5. T1:コミット

上記のシナリオでは、LAST_READ_IDは6になるため、5は読み取られません。


私のテストでは、リーダー(ステップ4)が値5の行を読み取ろうとすると(T1がロックを解除するまで)ブロックするため、このシナリオは問題ではないことを示しているようです。
ファビアンシュミード

あなたは正しいかもしれません、私はSQLサーバーのロックメカニズムをあまりよく知りません(そのため、答えに疑いがあります)。
レナート

読者の分離レベルに依存します。それは両方を見る、ブロックする、または6のみを見る
マイケルグリーン

0

このスクリプトの実行:

BEGIN TRAN;
INSERT INTO dbo.Example DEFAULT VALUES;
COMMIT;

以下は、拡張イベントセッションによってキャプチャされたときに取得および解放されたロックです。

name            timestamp                   associated_object_id    mode    object_id   resource_type   session_id  resource_description
lock_acquired   2016-03-29 06:37:28.9968693 1585440722              IX      1585440722  OBJECT          51          
lock_acquired   2016-03-29 06:37:28.9969268 7205759890195415040     IX      0           PAGE            51          1:1235
lock_acquired   2016-03-29 06:37:28.9969306 7205759890195415040     RI_NL   0           KEY             51          (ffffffffffff)
lock_acquired   2016-03-29 06:37:28.9969330 7205759890195415040     X       0           KEY             51          (29cf3326f583)
lock_released   2016-03-29 06:37:28.9969579 7205759890195415040     X       0           KEY             51          (29cf3326f583)
lock_released   2016-03-29 06:37:28.9969598 7205759890195415040     IX      0           PAGE            51          1:1235
lock_released   2016-03-29 06:37:28.9969607 1585440722              IX      1585440722  OBJECT          51      

作成される新しい行のXキーロックの直前に取得されたRI_N KEYロックに注意してください。RI_Nロックは互換性がないため、この短命の範囲ロックは、同時挿入が別のRI_N KEYロックを取得することを防ぎます。手順2と3の間に述べたウィンドウは、新しく生成されたキーの行ロックの前に範囲ロックが取得されるため、問題ではありません。

限り、あなたのようにSELECT...ORDER BY希望新しく挿入された行の前にスキャンを開始し、私はあなたがデフォルトに望む行動期待READ COMMITTEDのデータベース限り分離レベルREAD_COMMITTED_SNAPSHOTオプションをオフにします。


1
technet.microsoft.com/en-us/library/…によると、2つのロックRangeI_N互換性あります。つまり、互いにブロックしないでください(既存のシリアル化可能なリーダーでブロックするためにロックがほとんどあります)。
ファビアンシュミード

@FabianSchmied、興味深い。このトピックは、ロックが互換性がないことを示すtechnet.microsoft.com/en-us/library/ms186396(v=sql.105).aspxのロック互換性マトリックスと競合します。あなたが言及したリンクの挿入例は、私の答えのトレースに示されているのと同じ振る舞いを示しています(排他キーロックの前に範囲をテストするための短命の挿入範囲ロック)。
ダン・グスマン

1
実際、マトリックスは「競合なし」(「互換性なし」ではない)を表す「N」を示します。)
ファビアンシュミード

0

SQL Serverの理解から、デフォルトの動作では、2番目のクエリは最初のクエリがコミットされるまで結果を表示しません。最初のクエリがCOMMITではなくROLLBACKを実行する場合、列にIDがありません。

基本設定

データベース表

次の構造を持つデータベーステーブルを作成しました。

CREATE TABLE identity_rc_test (
    ID4VALUE INT IDENTITY (1,1), 
    TEXTVALUE NVARCHAR(20),
    CONSTRAINT PK_ID4_VALUE_CLUSTERED 
        PRIMARY KEY CLUSTERED (ID4VALUE, TEXTVALUE)
)

データベース分離レベル

次のステートメントを使用して、データベースの分離レベルを確認しました。

SELECT snapshot_isolation_state, 
       snapshot_isolation_state_desc, 
       is_read_committed_snapshot_on
FROM sys.databases WHERE NAME = 'mydatabase'

データベースに対して次の結果が返されました。

snapshot_isolation_state    snapshot_isolation_state_desc   is_read_committed_snapshot_on
0                           OFF                             0

(これは、SQL Server 2012のデータベースのデフォルト設定です)

テストスクリプト

次のスクリプトは、標準のSQL Server SSMSクライアント設定と標準のSQL Server設定を使用して実行されました。

クライアント接続設定

クライアントはREAD COMMITTED、SSMSのクエリオプションに従ってトランザクション分離レベルを使用するように設定されています。

クエリ1

次のクエリは、SPID 57のクエリウィンドウで実行されました

SELECT * FROM dbo.identity_rc_test
BEGIN TRANSACTION [FIRST_QUERY]
INSERT INTO dbo.identity_rc_test (TEXTVALUE) VALUES ('Nine')
/* Commit is commented out to prevent the INSERT from being commited
--COMMIT TRANSACTION [FIRST_QUERY]
--ROLLBACK TRANSACTION [FIRST_QUERY]
*/

クエリ2

次のクエリは、SPID 58のクエリウィンドウで実行されました

BEGIN TRANSACTION [SECOND_QUERY]
INSERT INTO dbo.identity_rc_test (TEXTVALUE) VALUES ('Ten')
COMMIT TRANSACTION [SECOND_QUERY]
SELECT * FROM dbo.identity_rc_test

クエリは完了せず、ページで排他的ロックが解除されるのを待っています。

ロックを決定するスクリプト

このスクリプトは、2つのトランザクションのデータベースオブジェクトで発生するロックを表示します。

SELECT request_session_id, resource_type,
       resource_description, 
       resource_associated_entity_id,
       request_mode, request_status
FROM sys.dm_tran_locks
WHERE request_session_id IN (57, 58)

結果は次のとおりです。

58  DATABASE                    0                   S   GRANT
57  DATABASE                    0                   S   GRANT
58  PAGE            1:79        72057594040549300   IS  GRANT
57  PAGE            1:79        72057594040549300   IX  GRANT
57  KEY         (a0aba7857f1b)  72057594040549300   X   GRANT
58  KEY         (a0aba7857f1b)  72057594040549300   S   WAIT
58  OBJECT                      245575913           IS  GRANT
57  OBJECT                      245575913           IX  GRANT

結果は、クエリウィンドウ1(SPID 57)がDATABASEにShared lock(S)、OBJECTにIntended eXlusive(IX)ロック、挿入するPAGEにIntended eXlusive(IX)ロック、およびeXclusiveを持っていることを示しています挿入されたがまだコミットされていないキーのロック(X)

コミットされていないデータのため、2番目のクエリ(SPID 58)には、DATABASEレベルにSharedロック(S)、OBJECTにIntended Shared(IS)ロック、Shared(S )要求ステータスWAITでKEYをロックします。

概要

最初のクエリウィンドウのクエリは、コミットせずに実行されます。2番目のクエリはREAD COMMITTEDデータしか取得できないため、タイムアウトが発生するか、最初のクエリでトランザクションがコミットされるまで待機します。

これは、Microsoft SQL Serverのデフォルトの動作を理解しているからです。

最初のステートメントがCOMMITする場合、SELECTステートメントによる後続の読み取りでは、IDが実際に順番に並んでいることに注意してください。

最初のステートメントがROLLBACKを実行すると、シーケンス内に欠落しているIDが見つかりますが、IDは昇順のままです(ID列にデフォルトまたはASCオプションを指定してINDEXを作成した場合)。

更新:

(簡潔に)はい、問題が発生するまで、ID列が正しく機能していることに依存できます。SQL Server 2000および MicrosoftのWebサイトのID列に関するHOTFIXは 1つだけです。

ID列が正しく更新されることに頼ることができなかった場合、MicrosoftのWebサイトにはさらに多くの修正プログラムまたはパッチがあると思います。

マイクロソフトサポート契約を結んでいる場合は、いつでもアドバイザリーケースを開き、追加情報を求めることができます。


1
分析に感謝しますが、私の質問は、次のIdentity値の生成と行のKEYロックの取得(同時読み取り/書き込みが陥る可能性がある)の間に時間枠があるかどうかです。クエリの実行を停止し、その非常に短い時間枠の間にロックを分析することはできないため、これがあなたの観察によって不可能であると証明されたとは思わない。
ファビアンシュミード

いいえ、あなたは文を止めることはできませんが、私の(遅い)観察は速い/普通に起こることです。1つのSPIDがロックを取得してデータを挿入するとすぐに、もう1つのSPIDは同じロックを取得できません。より高速なステートメントには、すでにロックとIDを順番に取得しているという利点があります。ロックが解除された後、次のステートメントは次のIDを受け取ります。
ジョン別名hot2use

1
通常、あなたの観察結果は私自身の(そして私の予想も)一致します-それは知っておくと良いことです。しかし、それらが保持されない例外的な状況があるのだろうか。
ファビアンシュミード
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.