昨日、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を使用する場合は注意が必要です。
トランザクションはドキュメントの本文でほとんど言及されておらず(同時実行ではなく、失敗のコンテキストでのみ)、どのサンプルでもトランザクションは使用されていません。それでは、ケンダルが何をしていたかをテストして、複数のセッションが同時に実行されているときに失敗する可能性があるかどうかを確認しましょう。各セッションで生成された値を追跡するためにログテーブルを作成します。実際に生成されたID値(afterトリガーを使用)と、IDENT_CURRENT()に従って生成されたと主張された値の両方です。
まず、テーブルとトリガー:
次に、いくつかのクエリウィンドウを開き、このコードを貼り付けて、できるだけ近くで実行し、重複を最大限に確保します。
すべてのクエリウィンドウが完了したら、このクエリを実行して、IDENT_CURRENTが間違った値を返したいくつかのランダムな行と、この誤って報告された数の影響を受けた合計行数を確認します。
これが1つのテストの10行です:
行のほぼ3分の1がずれているのは驚きでした。結果は確かに異なり、ドライブの速度、リカバリモデル、ログファイルの設定、またはその他の要因によって異なる場合があります。 2つの異なるマシンでは、障害率が大きく異なり、10倍になりました(低速のマシンでは、10,000の障害、つまり約3%しか発生しませんでした)。
IDENT_CURRENTが他のセッションによって生成されたIDENTITY値をプルするのを防ぐには、トランザクションだけでは不十分であることはすぐにわかります。 SERIALIZABLEトランザクションはどうですか?まず、2つのテーブルをクリアします:
次に、このコードを複数のクエリウィンドウのスクリプトの先頭に追加し、可能な限り同時に実行します。
今回、IdentityLogテーブルに対してクエリを実行すると、SERIALIZABLEが少し役に立った可能性があることが示されていますが、問題は解決していません。
そして、間違っているのは間違っていますが、私のサンプル結果から、IDENT_CURRENTの値は通常1つか2つだけずれているように見えます。ただし、このクエリは、*ウェイ*オフになる可能性があることを示しているはずです。私のテスト実行では、この結果は236と高かった:
この証拠から、IDENT_CURRENTはトランザクションセーフではないと結論付けることができます。これは、オブジェクトがOBJECT_NAME()のようなメタデータ関数がブロックされるという、類似しているがほとんど反対の問題を彷彿とさせるようです。分離レベルがREAD UNCOMMITTEDの場合でも、周囲の分離セマンティクスに従わないためです。 (詳細については、接続アイテム#432497を参照してください。)
表面的には、アーキテクチャとアプリケーションについて詳しく知らない限り、Kendalについて本当に良い提案はありません。 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
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();
TRUNCATE TABLE dbo.TableName;
TRUNCATE TABLE dbo.IdentityLog;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
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';