sql >> データベース >  >> RDS >> Database

最後に、いいえ、IDENT_CURRENT()を信頼することはできません

    昨日、Kendal Van Dyke(@SQLDBA)とIDENT_CURRENT()について話し合いました。基本的に、ケンダルは自分でテストして信頼したこのコードを持っていて、大規模な同時環境でIDENT_CURRENT()が正確であることに依存できるかどうかを知りたいと考えていました。

    BEGIN TRANSACTION;
    INSERT dbo.TableName(ColumnName) VALUES('Value');
    SELECT IDENT_CURRENT('dbo.TableName');
    COMMIT TRANSACTION;

    彼がこれをしなければならなかった理由は、彼が生成されたIDENTITY値をクライアントに返す必要があるためです。これを行う一般的な方法は次のとおりです。

    • SCOPE_IDENTITY()
    • OUTPUT句
    • @@ IDENTITY
    • IDENT_CURRENT()

    これらのいくつかは他のものよりも優れていますが、それは死ぬまで行われているので、ここでは説明しません。ケンダルの場合、IDENT_CURRENTが彼の最後で唯一の手段でした。理由は次のとおりです。

    • TableNameにはINSTEADOFINSERTトリガーがあり、SCOPE_IDENTITY()とOUTPUT句の両方が呼び出し元から役に立たなくなりました。理由は次のとおりです。
      • SCOPE_IDENTITY()は、挿入が実際には別のスコープで行われたため、NULLを返します
      • トリガーが原因で、OUTPUT句がエラーメッセージ334を生成します
    • 彼は@@IDENTITYを排除しました。 INSTEAD OF INSERTトリガーは、独自のIDENTITY列を持つ他のテーブルに挿入できるようになりました(または後で変更される可能性があります)。これにより、戻り値が台無しになります。可能であれば、これはSCOPE_IDENTITY()も妨害します。
    • 最後に、トリガー内でOUTPUT句(または挿入された疑似テーブルの2番目のクエリからの結果セット)をトリガー内で使用できませんでした。この機能にはグローバル設定が必要であり、 SQL Server2005。当然のことながら、Kendalのコードは前方互換性が必要であり、可能であれば、特定のデータベースまたはサーバー設定に完全に依存しないようにする必要があります。

    では、ケンダルの現実に戻りましょう。彼のコードは十分に安全なようです–結局のところ、それはトランザクション内にあります。何がうまくいかない可能性がありますか?さて、IDENT_CURRENTドキュメントからいくつかの重要な文を見てみましょう(これらの警告は正当な理由でそこにあるので、私の強調です):

    指定されたテーブルまたはビューに対して生成された最後のID値を返します。生成される最後のID値は、任意のセッションのものです。 および任意のスコープ 。

    次に生成されるID値を予測するためにIDENT_CURRENTを使用する場合は注意が必要です。 実際に生成される値は異なる場合があります IDENT_CURRENTとIDENT_INCRから他のセッションによって挿入が実行されたため 。

    トランザクションはドキュメントの本文でほとんど言及されておらず(同時実行ではなく、失敗のコンテキストでのみ)、どのサンプルでもトランザクションは使用されていません。それでは、ケンダルが何をしていたかをテストして、複数のセッションが同時に実行されているときに失敗する可能性があるかどうかを確認しましょう。各セッションで生成された値を追跡するためにログテーブルを作成します。実際に生成されたID値(afterトリガーを使用)と、IDENT_CURRENT()に従って生成されたと主張された値の両方です。

    まず、テーブルとトリガー:

    -- the destination table:
     
    CREATE TABLE dbo.TableName
    (
      ID INT IDENTITY(1,1), 
      seq INT
    );
     
    -- the log table:
     
    CREATE TABLE dbo.IdentityLog
    (
      SPID INT, 
      seq INT, 
      src VARCHAR(20), -- trigger or ident_current 
      id INT
    );
    GO
     
    -- the trigger, adding my logging:
     
    CREATE TRIGGER dbo.InsteadOf_TableName
    ON dbo.TableName
    INSTEAD OF INSERT
    AS
    BEGIN
      INSERT dbo.TableName(seq) SELECT seq FROM inserted;
     
      -- this is just for our logging purposes here:
      INSERT dbo.IdentityLog(SPID,seq,src,id)
        SELECT @@SPID, seq, 'trigger', SCOPE_IDENTITY() 
        FROM inserted;
    END
    GO

    次に、いくつかのクエリウィンドウを開き、このコードを貼り付けて、できるだけ近くで実行し、重複を最大限に確保します。

    SET NOCOUNT ON;
     
    DECLARE @seq INT = 0;
     
    WHILE @seq <= 100000
    BEGIN
      BEGIN TRANSACTION;
     
      INSERT dbo.TableName(seq) SELECT @seq;
      INSERT dbo.IdentityLog(SPID,seq,src,id)
        SELECT @@SPID,@seq,'ident_current',IDENT_CURRENT('dbo.TableName');
     
      COMMIT TRANSACTION;
      SET @seq += 1;
    END

    すべてのクエリウィンドウが完了したら、このクエリを実行して、IDENT_CURRENTが間違った値を返したいくつかのランダムな行と、この誤って報告された数の影響を受けた合計行数を確認します。

    SELECT TOP (10)
      id_cur.SPID,  
      [ident_current] = id_cur.id, 
      [actual id] = tr.id, 
      total_bad_results = COUNT(*) OVER()
    FROM dbo.IdentityLog AS id_cur
    INNER JOIN dbo.IdentityLog AS tr
       ON id_cur.SPID = tr.SPID 
       AND id_cur.seq = tr.seq 
       AND id_cur.id <> tr.id
    WHERE id_cur.src = 'ident_current' 
       AND tr.src     = 'trigger'
    ORDER BY NEWID();

    これが1つのテストの10行です:

    行のほぼ3分の1がずれているのは驚きでした。結果は確かに異なり、ドライブの速度、リカバリモデル、ログファイルの設定、またはその他の要因によって異なる場合があります。 2つの異なるマシンでは、障害率が大きく異なり、10倍になりました(低速のマシンでは、10,000の障害、つまり約3%しか発生しませんでした)。

    IDENT_CURRENTが他のセッションによって生成されたIDENTITY値をプルするのを防ぐには、トランザクションだけでは不十分であることはすぐにわかります。 SERIALIZABLEトランザクションはどうですか?まず、2つのテーブルをクリアします:

    TRUNCATE TABLE dbo.TableName;
    TRUNCATE TABLE dbo.IdentityLog;

    次に、このコードを複数のクエリウィンドウのスクリプトの先頭に追加し、可能な限り同時に実行します。

    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;

    今回、IdentityLogテーブルに対してクエリを実行すると、SERIALIZABLEが少し役に立った可能性があることが示されていますが、問題は解決していません。

    そして、間違っているのは間違っていますが、私のサンプル結果から、IDENT_CURRENTの値は通常1つか2つだけずれているように見えます。ただし、このクエリは、*ウェイ*オフになる可能性があることを示しているはずです。私のテスト実行では、この結果は236と高かった:

    SELECT MAX(ABS(id_cur.id - tr.id))
    FROM dbo.IdentityLog AS id_cur
    INNER JOIN dbo.IdentityLog AS tr
      ON id_cur.SPID = tr.SPID 
      AND id_cur.seq = tr.seq 
      AND id_cur.id <> tr.id
    WHERE id_cur.src = 'ident_current' 
      AND tr.src     = 'trigger';

    この証拠から、IDENT_CURRENTはトランザクションセーフではないと結論付けることができます。これは、オブジェクトがOBJECT_NAME()のようなメタデータ関数がブロックされるという、類似しているがほとんど反対の問題を彷彿とさせるようです。分離レベルがREAD UNCOMMITTEDの場合でも、周囲の分離セマンティクスに従わないためです。 (詳細については、接続アイテム#432497を参照してください。)

    表面的には、アーキテクチャとアプリケーションについて詳しく知らない限り、Kendalについて本当に良い提案はありません。 IDENT_CURRENTが答えではないことを私は知っています。 :-)それを使用しないでください。何のために。これまで。値を読み取るときには、すでに間違っている可能性があります。


    1. Oracle ORA-12154:TNS:サービス名エラーを解決できませんでしたか?

    2. Oracleのコンマ区切り値

    3. 条件付き一意制約

    4. SQLServerテーブルのパーティション分割とパーティションのチュートリアル