Qual è il modello giusto per dati univoci nelle colonne?
-
19-08-2019 - |
Domanda
Ho una tabella [File] che ha il seguente schema
CREATE TABLE [dbo].[File]
(
[FileID] [int] IDENTITY(1,1) NOT NULL,
[Name] [varchar](256) NOT NULL,
CONSTRAINT [PK_File] PRIMARY KEY CLUSTERED
(
[FileID] ASC
)WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF, IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON, ALLOW_PAGE_LOCKS = ON) ON [PRIMARY]
) ON [PRIMARY]
L'idea è che il FileID sia usato come chiave per la tabella e il Nome sia il percorso completo che rappresenta un file.
Quello che ho cercato di fare è creare una Stored Procedure che verificherà se il Nome è già in uso, in tal caso quindi utilizzare quel record altrimenti creare un nuovo record.
Ma quando provo lo stress test del codice con molti thread che eseguono contemporaneamente la procedura memorizzata ottengo diversi errori.
Questa versione del codice creerà un deadlock e genererà un'eccezione di deadlock sul client.
CREATE PROCEDURE [dbo].[File_Create]
@Name varchar(256)
AS
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRANSACTION xact_File_Create
SET XACT_ABORT ON
SET NOCOUNT ON
DECLARE @FileID int
SELECT @FileID = [FileID] FROM [dbo].[File] WHERE [Name] = @Name
IF @@ROWCOUNT=0
BEGIN
INSERT INTO [dbo].[File]([Name])
VALUES (@Name)
SELECT @FileID = [FileID] FROM [dbo].[File] WHERE [Name] = @Name
END
SELECT * FROM [dbo].[File]
WHERE [FileID] = @FileID
COMMIT TRANSACTION xact_File_Create
GO
Questa versione del codice finisce per ottenere righe con gli stessi dati nella colonna Nome.
CREATE PROCEDURE [dbo].[File_Create]
@Name varchar(256)
AS
BEGIN TRANSACTION xact_File_Create
SET NOCOUNT ON
DECLARE @FileID int
SELECT @FileID = [FileID] FROM [dbo].[File] WHERE [Name] = @Name
IF @@ROWCOUNT=0
BEGIN
INSERT INTO [dbo].[File]([Name])
VALUES (@Name)
SELECT @FileID = [FileID] FROM [dbo].[File] WHERE [Name] = @Name
END
SELECT * FROM [dbo].[File]
WHERE [FileID] = @FileID
COMMIT TRANSACTION xact_File_Create
GO
Mi chiedo quale sia il modo giusto di fare questo tipo di azione? In generale, questo è un modello che vorrei utilizzare in cui i dati della colonna sono univoci in una singola colonna o in più colonne e un'altra colonna viene utilizzata come chiave.
Grazie
Soluzione
Se stai effettuando una ricerca approfondita nel campo Nome, probabilmente lo vorrai indicizzato (come univoco e forse anche raggruppato se questo è il campo di ricerca primario ). Dato che non usi @FileID dalla prima selezione, selezionerei semplicemente count (*) dal file dove Name = @Name e vedo se è maggiore di zero (questo impedirà a SQL di conservare i blocchi sulla tabella da la fase di ricerca, poiché nessuna colonna è selezionata).
Sei sulla strada giusta con il livello SERIALIZZABILE, poiché la tua azione influirà sul successo o il fallimento delle query successive con la presenza del Nome. Il motivo per cui la versione senza quel set causa duplicati è che due selezioni sono state eseguite contemporaneamente e hanno scoperto che non c'erano record, quindi entrambi sono andati avanti con gli inserti (che creano il duplicato).
Il deadlock con la versione precedente è molto probabilmente dovuto alla mancanza di un indice che richiede molto tempo per il processo di ricerca. Quando si carica il server in una transazione SERIALIZZABILE, tutto il resto dovrà attendere il completamento dell'operazione. L'indice dovrebbe velocizzare l'operazione, ma solo i test indicheranno se è abbastanza veloce. Si noti che è possibile rispondere alla transazione non riuscita reinviando: nelle situazioni del mondo reale si spera che il carico sia transitorio.
MODIFICA: Rendendo la tabella indicizzata, ma non usando SERIALIZABLE, si ottengono tre casi:
- Nome trovato, ID acquisito e utilizzato. Comune
- Il nome non è stato trovato, si inserisce come previsto. Comune
- Il nome non viene trovato, l'inserimento non riesce perché un'altra corrispondenza esatta è stata pubblicata entro millisecondi dal primo. Molto raro
Mi aspetterei che quest'ultimo caso fosse davvero eccezionale, quindi usare un'eccezione per catturare questo caso molto raro sarebbe preferibile impegnarsi in SERIALIZZABILE, che ha gravi conseguenze sulle prestazioni.
Se hai davvero l'aspettativa che sarà comune avere post tra millisecondi l'uno dell'altro con lo stesso nuovo nome, usa una transazione SERIALIZZABILE insieme all'indice. Sarà più lento nel caso generale, ma più veloce quando vengono trovati questi post.
Altri suggerimenti
Innanzitutto, crea un indice univoco nella colonna Nome. Quindi dal tuo codice client controlla prima se esiste il Nome selezionando il FileID e inserendo il Nome nella clausola where - in caso affermativo, usa il FileID. In caso contrario, inserirne uno nuovo.
L'uso della funzione Exists potrebbe ripulire un po 'le cose.
if (Exists(select * from table_name where column_name = @param)
begin
//use existing file name
end
else
//use new file name