Ho una tabella che viene utilizzata da un'applicazione legacy come sostituto dei IDENTITY
campi in varie altre tabelle.
Ogni riga nella tabella memorizza l'ultimo ID utilizzato LastID
per il campo indicato IDName
.
Occasionalmente il proc memorizzato ottiene un deadlock - credo di aver creato un appropriato gestore degli errori; tuttavia sono interessato a vedere se questa metodologia funziona come penso, o se abbaio qui l'albero sbagliato.
Sono abbastanza certo che ci dovrebbe essere un modo per accedere a questa tabella senza alcun deadlock.
Il database stesso è configurato con READ_COMMITTED_SNAPSHOT = 1
.
Innanzitutto, ecco la tabella:
CREATE TABLE [dbo].[tblIDs](
[IDListID] [int] NOT NULL
CONSTRAINT PK_tblIDs
PRIMARY KEY CLUSTERED
IDENTITY(1,1) ,
[IDName] [nvarchar](255) NULL,
[LastID] [int] NULL,
);
E l'indice non cluster sul 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
Alcuni dati di esempio:
INSERT INTO tblIDs (IDName, LastID)
VALUES ('SomeTestID', 1);
INSERT INTO tblIDs (IDName, LastID)
VALUES ('SomeOtherTestID', 1);
GO
La procedura memorizzata utilizzata per aggiornare i valori memorizzati nella tabella e restituire l'ID successivo:
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
Esecuzioni di esempio del proc memorizzato:
EXEC GetNextID 'SomeTestID';
NewID
2
EXEC GetNextID 'SomeTestID';
NewID
3
EXEC GetNextID 'SomeOtherTestID';
NewID
2
MODIFICARE:
Ho aggiunto un nuovo indice, poiché l'indice esistente IX_tblIDs_Name non viene utilizzato dall'SP; Presumo che il Query Processor stia utilizzando l'indice cluster poiché ha bisogno del valore memorizzato in LastID. Ad ogni modo, questo indice viene utilizzato dal piano di esecuzione effettivo:
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:
Ho seguito il consiglio dato da @AaronBertrand e l'ho modificato leggermente. L'idea generale qui è quella di affinare l'affermazione per eliminare il blocco non necessario e, nel complesso, per rendere l'SP più efficiente.
Il codice seguente sostituisce il codice sopra da 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;
Dal momento che il nostro codice non aggiunge mai un record a questa tabella con 0 in, LastID
possiamo supporre che se @NewID è 1, l'intenzione è di aggiungere un nuovo ID all'elenco, altrimenti stiamo aggiornando una riga esistente nell'elenco.
SERIALIZABLE
qui.