У мене є таблиця, яка використовується застарілим додатком як заміна IDENTITY
полів у різних інших таблицях.
Кожен рядок таблиці зберігає останній використаний ідентифікатор LastID
для поля, названого в 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
Збережена процедура, яка використовується для оновлення значень, збережених у таблиці, та повернення наступного ідентифікатора:
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. У будь-якому випадку, цей індекс IS використовується в реальному плані виконання:
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
на 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, то намір додає новий ідентифікатор до списку, інакше ми оновлюємо існуючий рядок у списку.
SERIALIZABLE
сюди.