Debido a que está usando una secuencia, puede usar la misma función SIGUIENTE VALOR PARA , que ya tiene en una restricción predeterminada en el Idcampo Clave primaria, para generar un nuevo Idvalor antes de tiempo. Generar el valor primero significa que no necesita preocuparse por no tenerlo SCOPE_IDENTITY, lo que significa que no necesita la OUTPUTcláusula ni hacer un adicional SELECTpara obtener el nuevo valor; tendrá el valor antes de hacer el INSERT, y ni siquiera necesita meterse con SET IDENTITY INSERT ON / OFF:-)
Eso se encarga de parte de la situación general. La otra parte es manejar el problema de concurrencia de dos procesos, al mismo tiempo, no encontrar una fila existente para la misma cadena y continuar con el INSERT. La preocupación es evitar la violación de la restricción única que ocurriría.
Una forma de manejar estos tipos de problemas de concurrencia es forzar a esta operación en particular a ser de un solo subproceso. La forma de hacerlo es mediante el uso de bloqueos de aplicaciones (que funcionan en sesiones). Si bien son efectivos, pueden ser un poco pesados para una situación como esta donde la frecuencia de colisiones es probablemente bastante baja.
La otra forma de lidiar con las colisiones es aceptar que a veces ocurrirán y manejarlas en lugar de tratar de evitarlas. Usando la TRY...CATCHconstrucción, puede atrapar efectivamente un error específico (en este caso: "violación de restricción única", Msg 2601) y volver a ejecutar el SELECTpara obtener el Idvalor ya que sabemos que ahora existe debido a estar en el CATCHbloque con ese particular error. Otros errores pueden ser manejados en el típico RAISERROR/ RETURNo THROWforma.
Configuración de prueba: secuencia, tabla e índice único
USE [tempdb];
CREATE SEQUENCE dbo.MagicNumber
AS INT
START WITH 1
INCREMENT BY 1;
CREATE TABLE dbo.NameLookup
(
[Id] INT NOT NULL
CONSTRAINT [PK_NameLookup] PRIMARY KEY CLUSTERED
CONSTRAINT [DF_NameLookup_Id] DEFAULT (NEXT VALUE FOR dbo.MagicNumber),
[ItemName] NVARCHAR(50) NOT NULL
);
CREATE UNIQUE NONCLUSTERED INDEX [UIX_NameLookup_ItemName]
ON dbo.NameLookup ([ItemName]);
GO
Configuración de prueba: procedimiento almacenado
CREATE PROCEDURE dbo.GetOrInsertName
(
@SomeName NVARCHAR(50),
@ID INT OUTPUT,
@TestRaceCondition BIT = 0
)
AS
SET NOCOUNT ON;
BEGIN TRY
SELECT @ID = nl.[Id]
FROM dbo.NameLookup nl
WHERE nl.[ItemName] = @SomeName
AND @TestRaceCondition = 0;
IF (@ID IS NULL)
BEGIN
SET @ID = NEXT VALUE FOR dbo.MagicNumber;
INSERT INTO dbo.NameLookup ([Id], [ItemName])
VALUES (@ID, @SomeName);
END;
END TRY
BEGIN CATCH
IF (ERROR_NUMBER() = 2601) -- "Cannot insert duplicate key row in object"
BEGIN
SELECT @ID = nl.[Id]
FROM dbo.NameLookup nl
WHERE nl.[ItemName] = @SomeName;
END;
ELSE
BEGIN
;THROW; -- SQL Server 2012 or newer
/*
DECLARE @ErrorNumber INT = ERROR_NUMBER(),
@ErrorMessage NVARCHAR(4000) = ERROR_MESSAGE();
RAISERROR(N'Msg %d: %s', 16, 1, @ErrorNumber, @ErrorMessage);
RETURN;
*/
END;
END CATCH;
GO
La prueba
DECLARE @ItemID INT;
EXEC dbo.GetOrInsertName
@SomeName = N'test1',
@ID = @ItemID OUTPUT;
SELECT @ItemID AS [ItemID];
GO
DECLARE @ItemID INT;
EXEC dbo.GetOrInsertName
@SomeName = N'test1',
@ID = @ItemID OUTPUT,
@TestRaceCondition = 1;
SELECT @ItemID AS [ItemID];
GO
Pregunta de OP
¿Por qué es esto mejor que el MERGE? ¿No obtendré la misma funcionalidad sin TRYusar la WHERE NOT EXISTScláusula?
MERGEtiene varios "problemas" (varias referencias están vinculadas en la respuesta de @ SqlZim, por lo que no es necesario duplicar esa información aquí). Y, no hay bloqueo adicional en este enfoque (menos contención), por lo que debería ser mejor en concurrencia. En este enfoque, nunca obtendrá una violación de restricción única, todo sin ninguna HOLDLOCK, etc. Es prácticamente seguro que funcione.
El razonamiento detrás de este enfoque es:
- Si tiene suficientes ejecuciones de este procedimiento como para tener que preocuparse por las colisiones, entonces no desea:
- tomar más medidas de las necesarias
- mantener bloqueados los recursos por más tiempo del necesario
- Dado que las colisiones solo pueden ocurrir en nuevas entradas (nuevas entradas enviadas exactamente al mismo tiempo ), la frecuencia de caer en el
CATCHbloque en primer lugar será bastante baja. Tiene más sentido optimizar el código que se ejecutará el 99% del tiempo en lugar del código que se ejecutará el 1% del tiempo (a menos que no haya ningún costo para optimizar ambos, pero ese no es el caso aquí).
Comentario de la respuesta de @ SqlZim (énfasis agregado)
Personalmente prefiero probar y adaptar una solución para evitar hacerlo cuando sea posible . En este caso, no creo que usar los bloqueos serializablesea un enfoque pesado, y estaría seguro de que manejaría bien la alta concurrencia.
Estaría de acuerdo con esta primera oración si fuera enmendada para indicar "y _cuando sea prudente". El hecho de que algo sea técnicamente posible no significa que la situación (es decir, el caso de uso previsto) se beneficiaría de ello.
El problema que veo con este enfoque es que bloquea más de lo que se sugiere. Es importante volver a leer la documentación citada en "serializable", específicamente lo siguiente (énfasis agregado):
- Otras transacciones no pueden insertar nuevas filas con valores de clave que caerían en el rango de claves leídas por cualquier declaración en la transacción actual hasta que se complete la transacción actual.
Ahora, aquí está el comentario en el código de ejemplo:
SELECT [Id]
FROM dbo.NameLookup WITH (SERIALIZABLE) /* hold that key range for @vName */
La palabra operativa allí es "rango". El bloqueo que se está tomando no solo se basa en el valor @vName, sino que es más exactamente un rango que comienza enla ubicación donde debe ir este nuevo valor (es decir, entre los valores clave existentes a cada lado de donde encaja el nuevo valor), pero no el valor en sí. Es decir, se bloqueará la inserción de nuevos procesos en otros procesos, dependiendo de los valores que se estén buscando actualmente. Si la búsqueda se realiza en la parte superior del rango, se bloqueará la inserción de cualquier cosa que pueda ocupar esa misma posición. Por ejemplo, si existen los valores "a", "b" y "d", entonces si un proceso está haciendo SELECT en "f", entonces no será posible insertar valores "g" o incluso "e" ( ya que cualquiera de esos vendrá inmediatamente después de "d"). Pero, será posible insertar un valor de "c" ya que no se colocaría en el rango "reservado".
El siguiente ejemplo debería ilustrar este comportamiento:
(En la pestaña de consulta (es decir, Sesión) # 1)
INSERT INTO dbo.NameLookup ([ItemName]) VALUES (N'test5');
BEGIN TRAN;
SELECT [Id]
FROM dbo.NameLookup WITH (SERIALIZABLE) /* hold that key range for @vName */
WHERE ItemName = N'test8';
--ROLLBACK;
(En la pestaña de consulta (es decir, Sesión) # 2)
EXEC dbo.NameLookup_getset_byName @vName = N'test4';
-- works just fine
EXEC dbo.NameLookup_getset_byName @vName = N'test9';
-- hangs until you either hit "cancel" in this query tab,
-- OR issue a COMMIT or ROLLBACK in query tab #1
EXEC dbo.NameLookup_getset_byName @vName = N'test7';
-- hangs until you either hit "cancel" in this query tab,
-- OR issue a COMMIT or ROLLBACK in query tab #1
EXEC dbo.NameLookup_getset_byName @vName = N's';
-- works just fine
EXEC dbo.NameLookup_getset_byName @vName = N'u';
-- hangs until you either hit "cancel" in this query tab,
-- OR issue a COMMIT or ROLLBACK in query tab #1
Del mismo modo, si existe el valor "C" y se está seleccionando el valor "A" (y por lo tanto bloqueado), puede insertar un valor de "D", pero no un valor de "B":
(En la pestaña de consulta (es decir, Sesión) # 1)
INSERT INTO dbo.NameLookup ([ItemName]) VALUES (N'testC');
BEGIN TRAN
SELECT [Id]
FROM dbo.NameLookup WITH (SERIALIZABLE) /* hold that key range for @vName */
WHERE ItemName = N'testA';
--ROLLBACK;
(En la pestaña de consulta (es decir, Sesión) # 2)
EXEC dbo.NameLookup_getset_byName @vName = N'testD';
-- works just fine
EXEC dbo.NameLookup_getset_byName @vName = N'testB';
-- hangs until you either hit "cancel" in this query tab,
-- OR issue a COMMIT or ROLLBACK in query tab #1
Para ser justos, en mi enfoque sugerido, cuando hay una excepción, habrá 4 entradas en el Registro de transacciones que no sucederán en este enfoque de "transacción serializable". PERO, como dije anteriormente, si la excepción ocurre el 1% (o incluso el 5%) del tiempo, eso es mucho menos impactante que el caso mucho más probable de que la SELECCIÓN inicial bloquee temporalmente las operaciones INSERTAR.
Otro problema, aunque menor, con este enfoque de "transacción serializable + cláusula de SALIDA" es que la OUTPUTcláusula (en su uso actual) envía los datos de vuelta como un conjunto de resultados. Un conjunto de resultados requiere más sobrecarga (probablemente en ambos lados: en SQL Server para administrar el cursor interno y en la capa de la aplicación para administrar el objeto DataReader) que un OUTPUTparámetro simple . Dado que solo estamos tratando con un solo valor escalar, y que la suposición es una alta frecuencia de ejecuciones, esa sobrecarga adicional del conjunto de resultados probablemente se suma.
Si bien la OUTPUTcláusula podría usarse de tal manera que devuelva un OUTPUTparámetro, eso requeriría pasos adicionales para crear una tabla o variable de tabla temporal, y luego seleccionar el valor de esa variable de tabla / tabla temporal en el OUTPUTparámetro.
Aclaración adicional: Respuesta a la Respuesta de @ SqlZim (respuesta actualizada) a mi Respuesta a la Respuesta de @ SqlZim (en la respuesta original) a mi declaración sobre concurrencia y desempeño ;-)
Lo siento si esta parte es un poquito larga, pero en este punto solo tenemos los matices de los dos enfoques.
Creo que la forma en que se presenta la información podría conducir a suposiciones falsas sobre la cantidad de bloqueo que uno podría esperar encontrar al usar serializableen el escenario como se presenta en la pregunta original.
Sí, admitiré que soy parcial, aunque para ser justos:
- Es imposible que un humano no sea parcial, al menos en un pequeño grado, y trato de mantenerlo al mínimo,
- El ejemplo dado fue simplista, pero con fines ilustrativos para transmitir el comportamiento sin complicarlo demasiado. No se pretendía implicar una frecuencia excesiva, aunque entiendo que tampoco dije explícitamente lo contrario y podría leerse que implica un problema mayor que el que realmente existe. Trataré de aclarar eso a continuación.
- También incluí un ejemplo de bloqueo de un rango entre dos teclas existentes (el segundo conjunto de bloques "Consulta pestaña 1" y "Consulta pestaña 2").
- Encontré (y fui voluntario) el "costo oculto" de mi enfoque, que son las cuatro entradas adicionales del Registro Tran cada vez que
INSERTfalla debido a una violación de Restricción Única. No he visto eso mencionado en ninguna de las otras respuestas / publicaciones.
Con respecto al enfoque "JFDI" de @ gbn, la publicación "Pragmatismo feo para la victoria" de Michael J. Swart, y el comentario de Aaron Bertrand sobre la publicación de Michael (con respecto a sus pruebas que muestran qué escenarios han disminuido el rendimiento), y su comentario sobre su "adaptación de Michael J La adaptación de Stewart del procedimiento Try Catch JFDI de @ gbn "indicando:
Si está insertando valores nuevos con más frecuencia que seleccionando valores existentes, esto puede ser más eficaz que la versión de @ srutzky. De lo contrario, preferiría la versión de @ srutzky sobre esta.
Con respecto a esa discusión de gbn / Michael / Aaron relacionada con el enfoque "JFDI", sería incorrecto equiparar mi sugerencia al enfoque "JFDI" de gbn. Debido a la naturaleza de la operación "Obtener o insertar", existe una necesidad explícita SELECTde obtener el IDvalor de los registros existentes. Este SELECT actúa como IF EXISTSverificación, lo que hace que este enfoque sea más equiparable a la variación "Check TryCatch" de las pruebas de Aaron. El código reescrito de Michael (y su adaptación final de la adaptación de Michael) también incluye WHERE NOT EXISTSprimero hacer esa misma verificación. Por lo tanto, mi sugerencia (junto con el código final de Michael y su adaptación de su código final) en realidad no llegará al CATCHbloque con tanta frecuencia. Solo podrían ser situaciones donde dos sesiones,ItemNameINSERT...SELECTen el mismo momento exacto, de modo que ambas sesiones reciban un "verdadero" para el WHERE NOT EXISTSmismo momento exacto y, por lo tanto, ambos intenten hacerlo INSERTexactamente en el mismo momento. Esa situación muy específica ocurre con mucha menos frecuencia que seleccionar una existente ItemNameo insertar una nueva ItemNamecuando ningún otro proceso intenta hacerlo en el mismo momento exacto .
CON TODO LO ANTERIOR EN MENTE: ¿Por qué prefiero mi enfoque?
Primero, veamos qué bloqueo tiene lugar en el enfoque "serializable". Como se mencionó anteriormente, el "rango" que se bloquea depende de los valores clave existentes a cada lado de donde encajaría el nuevo valor clave. El comienzo o el final del rango también podría ser el comienzo o el final del índice, respectivamente, si no hay un valor clave existente en esa dirección. Supongamos que tenemos el siguiente índice y claves ( ^representa el comienzo del índice mientras que $representa el final):
Range #: |--- 1 ---|--- 2 ---|--- 3 ---|--- 4 ---|
Key Value: ^ C F J $
Si la sesión 55 intenta insertar un valor clave de:
A, el rango n. ° 1 (de ^a C) está bloqueado: la sesión 56 no puede insertar un valor de B, incluso si es único y válido (todavía). Pero la sesión 56 se puede insertar valores de D, Gy M.
D, luego el rango # 2 (de Ca F) está bloqueado: la sesión 56 no puede insertar un valor de E(todavía). Pero la sesión 56 se puede insertar valores de A, Gy M.
M, luego el rango # 4 (de Ja $) está bloqueado: la sesión 56 no puede insertar un valor de X(todavía). Pero la sesión 56 se puede insertar valores de A, Dy G.
A medida que se agregan más valores clave, los rangos entre los valores clave se vuelven más estrechos, lo que reduce la probabilidad / frecuencia de que se inserten múltiples valores al mismo tiempo que luchan en el mismo rango. Es cierto que este no es un problema importante , y afortunadamente parece ser un problema que en realidad disminuye con el tiempo.
El problema con mi enfoque se describió anteriormente: solo ocurre cuando dos sesiones intentan insertar el mismo valor clave al mismo tiempo. A este respecto, todo se reduce a lo que tiene la mayor probabilidad de que suceda: ¿se intentan dos valores clave diferentes pero cercanos al mismo tiempo, o se intenta el mismo valor clave al mismo tiempo? Supongo que la respuesta radica en la estructura de la aplicación que realiza las inserciones, pero en general, supondría que es más probable que se inserten dos valores diferentes que comparten el mismo rango. Pero la única forma de saber realmente sería probar ambos en el sistema operativo.
A continuación, consideremos dos escenarios y cómo cada enfoque los maneja:
Todas las solicitudes corresponden a valores clave únicos:
En este caso, el CATCHbloque en mi sugerencia nunca se ingresa, por lo tanto, no hay "problema" (es decir, 4 entradas de registro de tran y el tiempo que lleva hacer eso). Pero, en el enfoque "serializable", incluso con todas las inserciones únicas, siempre habrá algún potencial para bloquear otras inserciones en el mismo rango (aunque no por mucho tiempo).
Alta frecuencia de solicitudes para el mismo valor clave al mismo tiempo:
En este caso, un grado muy bajo de unicidad en términos de solicitudes entrantes de valores clave inexistentes, el CATCHbloque en mi sugerencia se ingresará regularmente. El efecto de esto será que cada inserción fallida necesitará revertir automáticamente y escribir las 4 entradas en el Registro de transacciones, que es un pequeño golpe de rendimiento cada vez. Pero la operación general nunca debería fallar (al menos no debido a esto).
(Hubo un problema con la versión anterior del enfoque "actualizado" que le permitía sufrir puntos muertos. Se updlockagregó una pista para solucionar esto y ya no tiene puntos muertos).PERO, en el enfoque "serializable" (incluso la versión actualizada y optimizada), la operación se estancará. ¿Por qué? Debido a que el serializablecomportamiento solo impide INSERToperaciones en el rango que ha sido leído y, por lo tanto, bloqueado; no impide SELECToperaciones en ese rango.
El serializableenfoque, en este caso, parecería no tener una sobrecarga adicional, y podría funcionar un poco mejor de lo que estoy sugiriendo.
Al igual que con muchas / la mayoría de las discusiones sobre el rendimiento, debido a que hay tantos factores que pueden afectar el resultado, la única forma de tener una idea real de cómo funcionará algo es probarlo en el entorno objetivo donde se ejecutará. En ese momento no será una cuestión de opinión :).