У меня есть таблица, которая используется устаревшим приложением в качестве замены 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. В любом случае, этот индекс используется фактическим планом выполнения:
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
здесь.