web-dev-qa-db-ja.com

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

TL; DR:要約すると、次のようになります。行を挿入する場合、生成の間に機会の窓がありますか新しいIdentity値と、クラスター化インデックスの対応する行キーのlockingの外部監視者がnewerIdentity値は同時トランザクションによって挿入されますか? (SQL Serverで。)

詳細バージョン

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

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

例:ID値が1、2、3、4、および5の行が挿入される場合、リーダーは、値4の行を表示する前に、値5の行を表示してはなりません 。テストは、ORDER BY CheckpointSequence句(およびWHERE CheckpointSequence > -1句)を含むクエリが、行5が既にコミットされている場合でも、行4が読み取られるがまだコミットされていない場合は確実にブロックすることを示しています。 。

少なくとも理論的には、この仮定が破られる可能性のある競合状態がここにあると思います。残念ながら、Identityに関するドキュメントでは、複数の同時トランザクションのコンテキストでIdentityがどのように機能するかについてはあまり言及されておらず、「各新しい値は現在のシードと増分に基づいて生成されます。 」 「特定のトランザクションの各新しい値は、テーブル上の他の同時トランザクションとは異なります。」 ( [〜#〜] 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's queryReader's Query 。)

私が正しいと上記の状況が発生する可能性がある場合、私はそれらに対処するための2つのオプションのみを見ることができますが、どちらも不十分です:

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

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

24
Fabian Schmied

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

はい

ID値の割り当ては、含まれているユーザートランザクションとは無関係です。これは、トランザクションがロールバックされてもID値が消費される1つの理由です。インクリメント操作自体は、破損を防ぐためにラッチによって保護されていますが、それが保護の範囲です。

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

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

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

手順3の後で、コミット読み取りロックの下でrow_numberを使用したクエリは、次を返しました。

Screenshot

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

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

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

基本的に、アイデンティティ値に関しては documented 以外の保証はありません。

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

それは本当にそれです。

strictトランザクションFIFO処理が必要な場合は、手動でシリアル化する以外に選択肢がない可能性があります。アプリケーションの要件がそれほど重要でない場合は、オプション。その点に関しては、質問が100%明確ではありませんが、それでも、Remus Rusanuの記事 キューとしてのテーブルの使用 に役立つ情報が見つかるかもしれません。

26
Paul White 9

ポールホワイトが完全に正しいと回答したため、一時的に「スキップされた」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つがエントリを「見逃した」場合に、すべてのケースについて行を出力します。

7
Stefan Kainz

ギャップを残す可能性のあるシナリオは多数あるため、IDが連続しているとは期待しないでください。アイデンティティを抽象的な番号のように考慮し、ビジネス上の意味を付けない方が良いでしょう。

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

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

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

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

•独自性

•トランザクション内の連続する値。値が連続している必要がある場合、トランザクションはテーブルで排他ロックを使用するか、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

5
stacylaray

サーバーに高負荷がかかると状況が悪化することもあると思います。次の2つのトランザクションを検討してください。

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

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

1
Lennart

このスクリプトを実行する:

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オプションがオフになっています。

0
Dan Guzman

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設定を使用して実行されました。

クライアント接続設定

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

クエリ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)に、データベースの共有ロック(S)、オブジェクトの意図的排他(IX)ロック、挿入先のページの意図的排他(IX)ロック、および排他的ロックがあることを示しています。挿入されているがまだコミットされていないKEYのロック(X)。

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

概要

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

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

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

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

更新:

(率直に)はい、問題が発生するまで、ID列が正しく機能していることを信頼できます。 MicrosoftのWebサイトには SQL Server 2000とID列に関するHOTFIX が1つだけあります。

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

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

0