Tengo una tabla que es utilizada por una aplicación heredada como un sustituto de los IDENTITY
campos en varias otras tablas.
Cada fila de la tabla almacena la última ID utilizada LastID
para el campo mencionado IDName
.
Ocasionalmente, el proceso almacenado tiene un punto muerto: creo que he creado un controlador de errores apropiado; Sin embargo, estoy interesado en ver si esta metodología funciona como creo, o si estoy ladrando el árbol equivocado aquí.
Estoy bastante seguro de que debería haber una manera de acceder a esta tabla sin ningún punto muerto.
La base de datos en sí está configurada con READ_COMMITTED_SNAPSHOT = 1
.
Primero, aquí está la tabla:
CREATE TABLE [dbo].[tblIDs](
[IDListID] [int] NOT NULL
CONSTRAINT PK_tblIDs
PRIMARY KEY CLUSTERED
IDENTITY(1,1) ,
[IDName] [nvarchar](255) NULL,
[LastID] [int] NULL,
);
Y el índice no agrupado en el IDName
campo:
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
Algunos datos de muestra:
INSERT INTO tblIDs (IDName, LastID)
VALUES ('SomeTestID', 1);
INSERT INTO tblIDs (IDName, LastID)
VALUES ('SomeOtherTestID', 1);
GO
El procedimiento almacenado utilizado para actualizar los valores almacenados en la tabla y devolver la siguiente 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
Ejecuciones de muestra del proceso almacenado:
EXEC GetNextID 'SomeTestID';
NewID
2
EXEC GetNextID 'SomeTestID';
NewID
3
EXEC GetNextID 'SomeOtherTestID';
NewID
2
EDITAR:
He agregado un nuevo índice, ya que el SP no está utilizando el índice IX_tblIDs_Name existente; Supongo que el procesador de consultas está utilizando el índice agrupado ya que necesita el valor almacenado en LastID. De todos modos, este índice ES utilizado por el plan de ejecución real:
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);
EDITAR # 2:
Tomé el consejo que dio @AaronBertrand y lo modifiqué un poco. La idea general aquí es refinar la declaración para eliminar bloqueos innecesarios y, en general, hacer que el SP sea más eficiente.
El siguiente código reemplaza el código anterior de BEGIN TRANSACTION
a 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;
Dado que nuestro código nunca agrega un registro a esta tabla con 0 LastID
, podemos suponer que si @NewID es 1, entonces la intención es agregar una nueva ID a la lista, de lo contrario, estamos actualizando una fila existente en la lista.
SERIALIZABLE
aquí.