Mam tabelę, która jest używana przez starszą aplikację jako substytut IDENTITY
pól w różnych innych tabelach.
Każdy wiersz w tabeli przechowuje ostatnio używany identyfikator LastID
dla pola o nazwie wIDName
.
Czasami przechowywany proc dostaje impasu - wydaje mi się, że zbudowałem odpowiedni moduł obsługi błędów; jednak jestem zainteresowany, aby zobaczyć, czy ta metodologia działa tak, jak myślę, czy też szczekam tutaj niewłaściwe drzewo.
Jestem całkiem pewien, że powinien istnieć sposób na uzyskanie dostępu do tego stołu bez żadnych zakleszczeń.
Sama baza danych jest skonfigurowana za pomocą READ_COMMITTED_SNAPSHOT = 1
.
Po pierwsze, oto tabela:
CREATE TABLE [dbo].[tblIDs](
[IDListID] [int] NOT NULL
CONSTRAINT PK_tblIDs
PRIMARY KEY CLUSTERED
IDENTITY(1,1) ,
[IDName] [nvarchar](255) NULL,
[LastID] [int] NULL,
);
I indeks nieklastrowany w IDName
polu:
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
Niektóre przykładowe dane:
INSERT INTO tblIDs (IDName, LastID)
VALUES ('SomeTestID', 1);
INSERT INTO tblIDs (IDName, LastID)
VALUES ('SomeOtherTestID', 1);
GO
Procedura przechowywana używana do aktualizacji wartości przechowywanych w tabeli i zwracania następnego identyfikatora:
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
Przykładowe wykonania zapisanego proc:
EXEC GetNextID 'SomeTestID';
NewID
2
EXEC GetNextID 'SomeTestID';
NewID
3
EXEC GetNextID 'SomeOtherTestID';
NewID
2
EDYTOWAĆ:
Dodałem nowy indeks, ponieważ istniejący indeks IX_tblIDs_Name nie jest używany przez SP; Zakładam, że procesor zapytań korzysta z indeksu klastrowego, ponieważ potrzebuje wartości przechowywanej w LastID. W każdym razie ten indeks JEST używany przez rzeczywisty plan wykonania:
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);
EDYCJA 2:
Skorzystałem z porady udzielonej przez @AaronBertrand i nieco ją zmodyfikowałem. Ogólną ideą tutaj jest udoskonalenie instrukcji w celu wyeliminowania niepotrzebnego blokowania i ogólnie w celu zwiększenia wydajności SP.
Poniższy kod zastępuje powyższy kod od BEGIN TRANSACTION
do 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;
Ponieważ nasz kod nigdy nie dodaje rekordu do tej tabeli z wartością 0 LastID
, możemy założyć, że jeśli @NewID ma wartość 1, to intencją jest dodanie nowego identyfikatora do listy, w przeciwnym razie aktualizujemy istniejący wiersz na liście.
SERIALIZABLE
tutaj.