Declaración de fusión que se estanca

22

Tengo el siguiente procedimiento (SQL Server 2008 R2):

create procedure usp_SaveCompanyUserData
    @companyId bigint,
    @userId bigint,
    @dataTable tt_CoUserdata readonly
as
begin

    set nocount, xact_abort on;

    merge CompanyUser with (holdlock) as r
    using (
        select 
            @companyId as CompanyId, 
            @userId as UserId, 
            MyKey, 
            MyValue
        from @dataTable) as newData
    on r.CompanyId = newData.CompanyId
        and r.UserId = newData.UserId
        and r.MyKey = newData.MyKey
    when not matched then
        insert (CompanyId, UserId, MyKey, MyValue) values
        (@companyId, @userId, newData.MyKey, newData.MyValue);

end;

CompanyId, UserId, MyKey forman la clave compuesta para la tabla de destino. CompanyId es una clave externa para una tabla principal. Además, hay un índice no agrupado en CompanyId asc, UserId asc.

Se llama desde muchos subprocesos diferentes, y constantemente recibo puntos muertos entre diferentes procesos que llaman a esta misma declaración. Comprendí que el "con (bloqueo)" era necesario para evitar errores de inserción / actualización de la condición de carrera.

Supongo que dos hilos diferentes están bloqueando filas (o páginas) en diferentes órdenes cuando están validando las restricciones, y por lo tanto están bloqueadas.

¿Es esta una suposición correcta?

¿Cuál es la mejor manera de resolver esta situación (es decir, sin puntos muertos, impacto mínimo en el rendimiento de subprocesos múltiples)?

Imagen del plan de consulta (Si ve la imagen en una pestaña nueva, es legible. Perdón por el tamaño pequeño).

  • Hay como máximo 28 filas en la @datatable.
  • He rastreado el código, y no puedo ver en ningún lado que comencemos una transacción aquí.
  • La clave externa está configurada para conectarse en cascada solo al eliminar, y no hubo eliminaciones de la tabla primaria.
Sako73
fuente

Respuestas:

12

Bien, después de mirar todo un par de veces, creo que su suposición básica era correcta. Lo que probablemente está sucediendo aquí es que:

  1. La parte MATCH del MERGE verifica el índice en busca de coincidencias, y bloquea las filas / páginas a medida que avanza.

  2. Cuando tiene una fila sin coincidencia, primero intentará insertar la nueva Fila de índice para que solicite un bloqueo de escritura de fila / página ...

Pero si otro usuario también ha llegado al paso 1 en la misma fila / página, entonces el primer usuario será bloqueado de la Actualización, y ...

Si el segundo usuario también necesita insertar en la misma página, entonces están en un punto muerto.

AFAIK, solo hay una manera (simple) de estar 100% seguro de que no puede llegar a un punto muerto con este procedimiento y sería agregar una sugerencia TABLOCKX al MERGE, pero eso probablemente tendría un impacto realmente malo en el rendimiento.

Es posible que agregar una sugerencia TABLOCK sea suficiente para resolver el problema sin tener un gran efecto en su rendimiento.

Finalmente, también puede intentar agregar PAGLOCK, XLOCK o ambos PAGLOCK y XLOCK. De nuevo, eso podría funcionar y el rendimiento podría no ser demasiado horrible. Tendrás que probarlo para ver.

RBarryYoung
fuente
¿Cree que el nivel de aislamiento de la instantánea (versiones de fila) podría ser útil aquí?
Mikael Eriksson
Tal vez. O puede convertir las excepciones de punto muerto en excepciones de concurrencia.
RBarryYoung
2
Especificar la sugerencia TABLOCK en una tabla que es el objetivo de una instrucción INSERT tiene el mismo efecto que especificar la sugerencia TABLOCKX. (Fuente: msdn.microsoft.com/en-us/library/bb510625.aspx )
martes al
31

No habría problema si la variable de tabla solo tuviera un valor. Con varias filas, hay una nueva posibilidad de punto muerto. Suponga que dos procesos concurrentes (A y B) se ejecutan con variables de tabla que contienen (1, 2) y (2, 1) para la misma compañía.

El proceso A lee el destino, no encuentra fila e inserta el valor '1'. Tiene un bloqueo de fila exclusivo en el valor '1'. El proceso B lee el destino, no encuentra fila e inserta el valor '2'. Tiene un bloqueo de fila exclusivo en el valor '2'.

Ahora el proceso A necesita procesar la fila 2, y el proceso B necesita procesar la fila 1. Ninguno de los procesos puede avanzar porque requiere un bloqueo que sea incompatible con el bloqueo exclusivo del otro proceso.

Para evitar puntos muertos con varias filas, las filas deben procesarse (y acceder a las tablas) en el mismo orden cada vez . La variable de tabla en el plan de ejecución que se muestra en la pregunta es un montón, por lo que las filas no tienen un orden intrínseco (es muy probable que se lean en orden de inserción, aunque esto no está garantizado):

Plan existente

La falta de un orden de procesamiento de fila consistente conduce directamente a la oportunidad de punto muerto. Una segunda consideración es que la falta de una garantía de singularidad clave significa que se necesita un carrete de mesa para proporcionar la protección correcta de Halloween. El spool es un spool ansioso, lo que significa que todas las filas se escriben en una tabla de trabajo tempdb antes de volver a leerse y reproducirse para el operador Insertar.

Redefiniendo la TYPEvariable de la tabla para incluir un clúster PRIMARY KEY:

DROP TYPE dbo.CoUserData;

CREATE TYPE dbo.CoUserData
AS TABLE
(
    MyKey   integer NOT NULL PRIMARY KEY CLUSTERED,
    MyValue integer NOT NULL
);

El plan de ejecución ahora muestra un escaneo del índice agrupado y la garantía de unicidad significa que el optimizador puede eliminar de forma segura el Table Spool:

Con clave primaria

En las pruebas con 5000 iteraciones de la MERGEdeclaración en 128 subprocesos, no se produjeron puntos muertos con la variable de tabla agrupada. Debo enfatizar que esto es solo en base a la observación; la variable de tabla en clúster también podría ( técnicamente ) producir sus filas en una variedad de órdenes, pero las posibilidades de un orden consistente son mucho mayores. El comportamiento observado tendría que volver a probarse para cada nueva actualización acumulativa, service pack o nueva versión de SQL Server, por supuesto.

En caso de que la definición de la variable de la tabla no se pueda cambiar, hay otra alternativa:

MERGE dbo.CompanyUser AS R
USING 
    (SELECT DISTINCT MyKey, MyValue FROM @DataTable) AS NewData ON
    R.CompanyId = @CompanyID
    AND R.UserID = @UserID
    AND R.MyKey = NewData.MyKey
WHEN NOT MATCHED THEN 
    INSERT 
        (CompanyID, UserID, MyKey, MyValue) 
    VALUES
        (@CompanyID, @UserID, NewData.MyKey, NewData.MyValue)
OPTION (ORDER GROUP);

Esto también logra la eliminación del carrete (y la consistencia del orden de las filas) a costa de introducir un tipo explícito:

Plan de ordenación

Este plan tampoco produjo puntos muertos utilizando la misma prueba. Guión de reproducción a continuación:

CREATE TYPE dbo.CoUserData
AS TABLE
(
    MyKey   integer NOT NULL /* PRIMARY KEY */,
    MyValue integer NOT NULL
);
GO
CREATE TABLE dbo.Company
(
    CompanyID   integer NOT NULL

    CONSTRAINT PK_Company
        PRIMARY KEY (CompanyID)
);
GO
CREATE TABLE dbo.CompanyUser
(
    CompanyID   integer NOT NULL,
    UserID      integer NOT NULL,
    MyKey       integer NOT NULL,
    MyValue     integer NOT NULL

    CONSTRAINT PK_CompanyUser
        PRIMARY KEY CLUSTERED
            (CompanyID, UserID, MyKey),

    FOREIGN KEY (CompanyID)
        REFERENCES dbo.Company (CompanyID),
);
GO
CREATE NONCLUSTERED INDEX nc1
ON dbo.CompanyUser (CompanyID, UserID);
GO
INSERT dbo.Company (CompanyID) VALUES (1);
GO
DECLARE 
    @DataTable AS dbo.CoUserData,
    @CompanyID integer = 1,
    @UserID integer = 1;

INSERT @DataTable
SELECT TOP (10)
    V.MyKey,
    V.MyValue
FROM
(
    VALUES
        (1, 1),
        (2, 2),
        (3, 3),
        (4, 4),
        (5, 5),
        (6, 6),
        (7, 7),
        (8, 8),
        (9, 9)
) AS V (MyKey, MyValue)
ORDER BY NEWID();

BEGIN TRANSACTION;

    -- Test MERGE statement here

ROLLBACK TRANSACTION;
Paul White dice GoFundMonica
fuente
8

Creo que SQL_Kiwi proporcionó un muy buen análisis. Si necesita resolver el problema en la base de datos, debe seguir su sugerencia. Por supuesto, debe volver a probar que todavía funciona para usted cada vez que actualice, aplique un paquete de servicio o agregue / cambie un índice o una vista indizada.

Hay otras tres alternativas:

  1. Puede serializar sus insertos para que no choquen: puede invocar sp_getapplock al comienzo de su transacción y adquirir un bloqueo exclusivo antes de ejecutar su MERGE. Por supuesto, aún necesita hacer una prueba de esfuerzo.

  2. Puede hacer que un hilo maneje todas sus inserciones, de modo que su servidor de aplicaciones maneje la concurrencia.

  3. Puede volver a intentarlo automáticamente después de puntos muertos: este puede ser el enfoque más lento si la concurrencia es alta.

De cualquier manera, solo usted puede determinar el impacto de su solución en el rendimiento.

Por lo general, no tenemos puntos muertos en nuestro sistema, aunque tenemos mucho potencial para tenerlos. En 2011 cometimos un error en una implementación y tuvimos media docena de puntos muertos en unas pocas horas, todos siguiendo el mismo escenario. Lo arreglé pronto y eso fue todo el punto muerto del año.

Estamos utilizando principalmente el enfoque 1 en nuestro sistema. Funciona muy bien para nosotros.

Alaska
fuente
-1

Otro enfoque posible: he descubierto que Merge a veces presenta problemas de bloqueo y rendimiento, puede valer la pena jugar con la opción de consulta Opción (MaxDop x)

En el pasado oscuro y distante, SQL Server tenía una opción de bloqueo de nivel de inserción de fila, pero esto parece haber muerto, sin embargo, un PK agrupado con una identidad debería hacer que las inserciones se ejecuten sin problemas.

Ed Green
fuente