IDENTITY
他のさまざまなテーブルのフィールドの代わりとしてレガシーアプリケーションで使用されるテーブルがあります。
テーブルの各行にはLastID
、で名前が付けられたフィールドで最後に使用されたIDが格納されIDName
ます。
ストアドプロシージャがデッドロックを取得することがあります-適切なエラーハンドラを作成したと思います。しかし、この方法論が思うように機能するかどうか、またはここで間違ったツリーを探しているかどうかに興味があります。
デッドロックがまったくない状態でこのテーブルにアクセスする方法があるはずです。
データベース自体はで構成されREAD_COMMITTED_SNAPSHOT = 1
ます。
まず、表を次に示します。
CREATE TABLE [dbo].[tblIDs](
[IDListID] [int] NOT NULL
CONSTRAINT PK_tblIDs
PRIMARY KEY CLUSTERED
IDENTITY(1,1) ,
[IDName] [nvarchar](255) NULL,
[LastID] [int] NULL,
);
そして、IDName
フィールドの非クラスター化インデックス:
CREATE NONCLUSTERED INDEX [IX_tblIDs_IDName]
ON [dbo].[tblIDs]
(
[IDName] ASC
)
WITH (
PAD_INDEX = OFF
, STATISTICS_NORECOMPUTE = OFF
, SORT_IN_TEMPDB = OFF
, DROP_EXISTING = OFF
, ONLINE = OFF
, ALLOW_ROW_LOCKS = ON
, ALLOW_PAGE_LOCKS = ON
, FILLFACTOR = 80
);
GO
いくつかのサンプルデータ:
INSERT INTO tblIDs (IDName, LastID)
VALUES ('SomeTestID', 1);
INSERT INTO tblIDs (IDName, LastID)
VALUES ('SomeOtherTestID', 1);
GO
テーブルに保存されている値を更新し、次のIDを返すために使用されるストアドプロシージャ:
CREATE PROCEDURE [dbo].[GetNextID](
@IDName nvarchar(255)
)
AS
BEGIN
/*
Description: Increments and returns the LastID value from tblIDs
for a given IDName
Author: Max Vernon
Date: 2012-07-19
*/
DECLARE @Retry int;
DECLARE @EN int, @ES int, @ET int;
SET @Retry = 5;
DECLARE @NewID int;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
SET NOCOUNT ON;
WHILE @Retry > 0
BEGIN
BEGIN TRY
BEGIN TRANSACTION;
SET @NewID = COALESCE((SELECT LastID
FROM tblIDs
WHERE IDName = @IDName),0)+1;
IF (SELECT COUNT(IDName)
FROM tblIDs
WHERE IDName = @IDName) = 0
INSERT INTO tblIDs (IDName, LastID)
VALUES (@IDName, @NewID)
ELSE
UPDATE tblIDs
SET LastID = @NewID
WHERE IDName = @IDName;
COMMIT TRANSACTION;
SET @Retry = -2; /* no need to retry since the operation completed */
END TRY
BEGIN CATCH
IF (ERROR_NUMBER() = 1205) /* DEADLOCK */
SET @Retry = @Retry - 1;
ELSE
BEGIN
SET @Retry = -1;
SET @EN = ERROR_NUMBER();
SET @ES = ERROR_SEVERITY();
SET @ET = ERROR_STATE()
RAISERROR (@EN,@ES,@ET);
END
ROLLBACK TRANSACTION;
END CATCH
END
IF @Retry = 0 /* must have deadlock'd 5 times. */
BEGIN
SET @EN = 1205;
SET @ES = 13;
SET @ET = 1
RAISERROR (@EN,@ES,@ET);
END
ELSE
SELECT @NewID AS NewID;
END
GO
ストアドプロシージャのサンプル実行:
EXEC GetNextID 'SomeTestID';
NewID
2
EXEC GetNextID 'SomeTestID';
NewID
3
EXEC GetNextID 'SomeOtherTestID';
NewID
2
編集:
既存のインデックスIX_tblIDs_NameはSPによって使用されていないため、新しいインデックスを追加しました。LastIDに格納された値が必要なため、クエリプロセッサがクラスター化インデックスを使用していると仮定します。とにかく、このインデックスは実際の実行計画で使用されます:
CREATE NONCLUSTERED INDEX IX_tblIDs_IDName_LastID
ON dbo.tblIDs
(
IDName ASC
)
INCLUDE
(
LastID
)
WITH (FILLFACTOR = 100
, ONLINE=ON
, ALLOW_ROW_LOCKS = ON
, ALLOW_PAGE_LOCKS = ON);
編集#2:
@AaronBertrandが提供したアドバイスを参考にして、少し変更しました。ここでの一般的な考え方は、不必要なロックを排除するためにステートメントを改良し、全体的にSPをより効率的にすることです。
以下のコードは、上のコードをBEGIN TRANSACTION
toに置き換えますEND TRANSACTION
。
BEGIN TRANSACTION;
SET @NewID = COALESCE((SELECT LastID
FROM dbo.tblIDs
WHERE IDName = @IDName), 0) + 1;
IF @NewID = 1
INSERT INTO tblIDs (IDName, LastID)
VALUES (@IDName, @NewID);
ELSE
UPDATE dbo.tblIDs
SET LastID = @NewID
WHERE IDName = @IDName;
COMMIT TRANSACTION;
コードは0を含むこのテーブルにレコードを追加しないLastID
ため、@ NewIDが1の場合、新しいIDをリストに追加することを想定できます。そうでない場合は、リスト内の既存の行を更新します。
SERIALIZABLE
ここにエスカレートしています。