J'ai une table qui est utilisée par une application existante pour remplacer les IDENTITY
champs de diverses autres tables.
Chaque ligne de la table stocke le dernier ID utilisé LastID
pour le champ nommé dans IDName
.
Parfois, le proc stocké se trouve dans une impasse - je crois avoir construit un gestionnaire d'erreur approprié; Cependant, je suis intéressé de voir si cette méthodologie fonctionne comme je le pense, ou si je me trompe d'arbre ici.
Je suis à peu près certain qu'il devrait y avoir un moyen d'accéder à cette table sans aucune impasse.
La base de données elle-même est configurée avec READ_COMMITTED_SNAPSHOT = 1
.
Tout d'abord, voici le tableau:
CREATE TABLE [dbo].[tblIDs](
[IDListID] [int] NOT NULL
CONSTRAINT PK_tblIDs
PRIMARY KEY CLUSTERED
IDENTITY(1,1) ,
[IDName] [nvarchar](255) NULL,
[LastID] [int] NULL,
);
Et l'index non clusterisé sur le IDName
champ:
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
Quelques exemples de données:
INSERT INTO tblIDs (IDName, LastID)
VALUES ('SomeTestID', 1);
INSERT INTO tblIDs (IDName, LastID)
VALUES ('SomeOtherTestID', 1);
GO
La procédure stockée utilisée pour mettre à jour les valeurs stockées dans la table et renvoyer l'ID suivant:
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
Exemples d'exécution du proc stocké:
EXEC GetNextID 'SomeTestID';
NewID
2
EXEC GetNextID 'SomeTestID';
NewID
3
EXEC GetNextID 'SomeOtherTestID';
NewID
2
MODIFIER:
J'ai ajouté un nouvel index car l'index existant IX_tblIDs_Name n'est pas utilisé par le SP; Je suppose que le processeur de requêtes utilise l'index clusterisé car il a besoin de la valeur stockée dans LastID. Quoi qu'il en soit, cet index est utilisé par le plan d'exécution réel:
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);
EDIT # 2:
J'ai suivi le conseil donné par @AaronBertrand et l'ai légèrement modifié. L’idée générale ici est d’affiner la déclaration pour éliminer les verrouillages inutiles et, dans l’ensemble, pour rendre le SP plus efficace.
Le code ci-dessous remplace le code ci-dessus de 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;
Étant donné que notre code n'ajoute jamais d'enregistrement à cette table avec 0 dans, LastID
nous pouvons supposer que si @NewID est égal à 1, l'intention est d'ajouter un nouvel ID à la liste, sinon nous mettons à jour une ligne existante de la liste.
SERIALIZABLE
ici.