INSERTOS masivos que bloquean SELECT

14

Tengo un problema con una gran cantidad de INSERTOS que están bloqueando mis operaciones SELECT.

Esquema

Tengo una mesa como esta:

CREATE TABLE [InverterData](
    [InverterID] [bigint] NOT NULL,
    [TimeStamp] [datetime] NOT NULL,    
    [ValueA] [decimal](18, 2) NULL,
    [ValueB] [decimal](18, 2) NULL
    CONSTRAINT [PrimaryKey_e149e28f-5754-4229-be01-65fafeebce16] PRIMARY KEY CLUSTERED 
    (
        [TimeStamp] DESC,
        [InverterID] ASC
    ) WITH (PAD_INDEX = OFF, STATISTICS_NORECOMPUTE = OFF
    , IGNORE_DUP_KEY = OFF, ALLOW_ROW_LOCKS = ON
    , ALLOW_PAGE_LOCKS = ON)
)

También tengo este pequeño procedimiento auxiliar, que me permite insertar o actualizar (actualizar en conflicto) con el comando MERGE:

CREATE PROCEDURE [InsertOrUpdateInverterData]
    @InverterID bigint, @TimeStamp datetime
    , @ValueA decimal(18,2), @ValueB decimal(18,2)
AS
BEGIN
    MERGE [InverterData] AS TARGET
        USING (VALUES (@InverterID, @TimeStamp, @ValueA, @ValueB))
        AS SOURCE ([InverterID], [TimeStamp], [ValueA], [ValueB])
        ON TARGET.[InverterID] = @InverterID AND TARGET.[TimeStamp] = @TimeStamp
    WHEN MATCHED THEN
        UPDATE
        SET [ValueA] = SOURCE.[ValueA], [ValueB] = SOURCE.[ValueB]              
    WHEN NOT MATCHED THEN
        INSERT ([InverterID], [TimeStamp], [ValueA], [ValueB]) 
        VALUES (SOURCE.[InverterID], SOURCE.[TimeStamp], SOURCE.[ValueA], SOURCE.[ValueB]);
END

Uso

Ahora he ejecutado instancias de servicio en varios servidores que realizan actualizaciones masivas llamando [InsertOrUpdateInverterData]rápidamente al procedimiento.

También hay un sitio web que selecciona consultas en la [InverterData]tabla.

Problema

Si selecciono consultas en la [InverterData]tabla, se realizan en diferentes intervalos de tiempo, dependiendo del uso de INSERT de mis instancias de servicio. Si detengo todas las instancias de servicio, SELECT es extremadamente rápido, si la instancia realiza una inserción rápida, los SELECT se vuelven muy lentos o incluso se cancela el tiempo de espera.

Intentos

He hecho algunos SELECT en la [sys.dm_tran_locks]tabla para encontrar procesos de bloqueo, como este

SELECT
tl.request_session_id,
wt.blocking_session_id,
OBJECT_NAME(p.OBJECT_ID) BlockedObjectName,
h1.TEXT AS RequestingText,
h2.TEXT AS BlockingText,
tl.request_mode

FROM sys.dm_tran_locks AS tl

INNER JOIN sys.dm_os_waiting_tasks AS wt ON tl.lock_owner_address = wt.resource_address
INNER JOIN sys.partitions AS p ON p.hobt_id = tl.resource_associated_entity_id
INNER JOIN sys.dm_exec_connections ec1 ON ec1.session_id = tl.request_session_id
INNER JOIN sys.dm_exec_connections ec2 ON ec2.session_id = wt.blocking_session_id
CROSS APPLY sys.dm_exec_sql_text(ec1.most_recent_sql_handle) AS h1
CROSS APPLY sys.dm_exec_sql_text(ec2.most_recent_sql_handle) AS h2

Este es el resultado:

ingrese la descripción de la imagen aquí

S = Compartido. La sesión de retención tiene acceso compartido al recurso.

Pregunta

¿Por qué los SELECT están bloqueados por el [InsertOrUpdateInverterData]procedimiento que solo usa comandos MERGE?

¿Tengo que usar algún tipo de transacción con modo de aislamiento definido dentro [InsertOrUpdateInverterData]?

Actualización 1 (relacionada con la pregunta de @Paul)

Base en los informes internos del servidor MS-SQL sobre la [InsertOrUpdateInverterData]siguiente estadística:

  • Tiempo promedio de CPU: 0.12ms
  • Procesos de lectura promedio: 5.76 por / s
  • Procesos de escritura promedio: 0.4 por / s

¡Basándome en esto, parece que el comando MERGE está ocupado principalmente con operaciones de lectura que bloquearán la tabla! (?)

Actualización 2 (relacionada con la pregunta de @Paul)

La [InverterData]tabla tiene las siguientes estadísticas de almacenamiento:

  • Espacio de datos: 26,901.86 MB
  • Recuento de filas: 131,827,749
  • Particionado: verdadero
  • Recuento de particiones: 62

Aquí está el conjunto de resultados sp_WhoIsActive completo (casi) :

SELECT mando

  • dd hh: mm: ss.mss: 00 00: 01: 01.930
  • session_id: 73
  • wait_info: (12629ms) LCK_M_S
  • CPU: 198
  • block_session_id: 146
  • lecturas: 99,368
  • escribe: 0
  • estado: suspendido
  • open_tran_count: 0

[InsertOrUpdateInverterData]Comando de bloqueo

  • dd hh: mm: ss.mss: 00 00: 00: 00.330
  • session_id: 146
  • wait_info: NULL
  • CPU: 3.972
  • block_session_id: NULL
  • lecturas: 376,95
  • escribe: 126
  • estado: durmiendo
  • open_tran_count: 1
Steffen Mangold
fuente
Los ([TimeStamp] DESC, [InverterID] ASC)ve como una opción extraña para el índice agrupado. Me refiero a la DESCparte.
ypercubeᵀᴹ
Entiendo su punto: el índice agrupado DESC que inserta datos forzaría la reconstrucción de la tabla en lugar de agregarse al final ... performance dog; bloquearía la mesa mientras ocurre la reconstrucción ... sí. por Jove, lo tienes. La estructura es causa de bloqueo más que bloqueos.
Alocyte

Respuestas:

12

Primero, aunque no está relacionado con la pregunta principal, su MERGEdeclaración está potencialmente en riesgo de errores debido a una condición de carrera . El problema, en pocas palabras, es que es posible que múltiples hilos concurrentes concluyan que la fila de destino no existe, lo que resulta en intentos de inserción colisionantes. La causa raíz es que no es posible tomar un bloqueo compartido o de actualización en una fila que no existe. La solución es agregar una pista:

MERGE [dbo].[InverterData] WITH (SERIALIZABLE) AS [TARGET]

La sugerencia de nivel de aislamiento serializable asegura que el rango de clave donde iría la fila está bloqueado. Tiene un índice único para admitir el bloqueo de rango, por lo que esta sugerencia no tendrá un efecto adverso en el bloqueo, simplemente obtendrá protección contra esta posible condición de carrera.

Pregunta principal

¿Por qué están SELECTsbloqueados por el procedimiento [InsertOrUpdateInverterData] que solo usa MERGEcomandos?

Bajo el nivel de aislamiento predeterminado de lectura de bloqueo predeterminado , los bloqueos compartidos (S) se toman cuando se leen datos y, por lo general (aunque no siempre) se liberan poco después de que se completa la lectura. Algunos bloqueos compartidos se mantienen hasta el final de la declaración.

Una MERGEinstrucción modifica los datos, por lo que adquirirá S o bloqueos de actualización (U) al ubicar los datos para cambiar, que se convierten en bloqueos exclusivos (X) justo antes de realizar la modificación real. Los bloqueos U y X deben mantenerse hasta el final de la transacción.

Esto es cierto en todos los niveles de aislamiento, excepto el aislamiento de instantáneas 'optimista' (SI) que no debe confundirse con la lectura de versiones confirmadas, también conocido como aislamiento de instantáneas de lectura confirmada (RCSI).

Nada en su pregunta muestra una sesión esperando que un bloqueo S sea bloqueado por una sesión que tiene un bloqueo U. Estas cerraduras son compatibles . Es casi seguro que cualquier bloqueo es causado por el bloqueo en un bloqueo X sostenido. Esto puede ser un poco difícil de capturar cuando se toman, convierten y liberan una gran cantidad de bloqueos a corto plazo en un corto intervalo de tiempo.

El open_tran_count: 1en el comando InsertOrUpdateInverterData vale la pena investigar. Aunque el comando no se ejecutó por mucho tiempo, debe verificar que no tiene una transacción que contenga (en la aplicación o procedimiento almacenado de nivel superior) que sea innecesariamente larga. La mejor práctica es mantener las transacciones lo más cortas posible. Esto puede no ser nada, pero definitivamente deberías comprobarlo.

Solucion potencial

Como Kin sugirió en un comentario, podría buscar habilitar un nivel de aislamiento de versiones de fila (RCSI o SI) en esta base de datos. RCSI es el más utilizado, ya que generalmente no requiere tantos cambios de aplicación. Una vez habilitado, el nivel de aislamiento de lectura predeterminado utiliza versiones de fila en lugar de tomar bloqueos S para las lecturas, por lo que se reduce o elimina el bloqueo SX. Algunas operaciones (por ejemplo, comprobaciones de clave externa) aún adquieren bloqueos S bajo RCSI.

Sin embargo, tenga en cuenta que las versiones de fila consumen espacio tempdb, en términos generales, proporcional a la tasa de actividad de cambio y la duración de las transacciones. Deberá probar su implementación a fondo bajo carga para comprender y planificar el impacto de RCSI (o SI) en su caso.

Si desea localizar su uso de versiones, en lugar de habilitarlo para toda la carga de trabajo, SI podría ser una mejor opción. Al usar SI para las transacciones de lectura, evitará la disputa entre lectores y escritores, a costa de que los lectores vean la versión de la fila antes de que comience cualquier modificación concurrente (más correctamente, la operación de lectura bajo SI siempre verá el estado comprometido de la fila en el momento en que comenzó la transacción SI). El uso de SI para las transacciones de escritura tiene poco o ningún beneficio, ya que los bloqueos de escritura aún se tomarán, y deberá manejar cualquier conflicto de escritura. A menos que eso sea lo que quieras :)

Nota: A diferencia de RCSI (que una vez habilitado se aplica a todas las transacciones que se ejecutan en lectura confirmada), SI se debe solicitar explícitamente mediante SET TRANSACTION ISOLATION SNAPSHOT;.

Los comportamientos sutiles que dependen de que los lectores bloqueen a los escritores (¡incluso en el código de activación!) Hacen que las pruebas sean esenciales. Vea mi serie de artículos vinculados y los Libros en línea para más detalles. Si decide utilizar RCSI, asegúrese de revisar las modificaciones de datos en Leer aislamiento de instantáneas confirmadas en particular.

Finalmente, debe asegurarse de que su instancia esté parcheada con SQL Server 2008 Service Pack 4.

Paul White 9
fuente
0

Humildemente, no usaría fusionar. Iría con IF Exists (UPDATE) ELSE (INSERT): tiene una clave agrupada con las dos columnas que está utilizando para identificar las filas, por lo que es una prueba fácil.

Menciona inserciones MASIVAS y, sin embargo, hace 1 por 1 ... ¿pensó en agrupar los datos en una tabla de etapas y usar el poder del conjunto de datos SQL POWER OVERWHELMING para hacer más de 1 actualización / inserción a la vez? Al igual que tener una prueba de rutina para el contenido en la tabla de etapas, y tomar el top 10000 a la vez en lugar de 1 a la vez ...

Haría algo como esto en mi actualización

DECLARE @Set TABLE (StagingKey, ID,DATE)
INSERT INTO @Set
UPDATE Staging 
SET InProgress = 1
OUTPUT StagingKey, Staging.ID, Staging.Date
WHERE InProgress = 0
AND StagingID IN (SELECT TOP (100000) StagingKey FROM Staging WHERE inProgress = 0 ORDER BY StagingKey ASC ) --FIFO

DECLARE @Temp 
INSERT INTO @TEMP 
UPDATE [DEST] SET Value = Staging.Value [whatever]
OUTPUT INSERTED.ID, DATE [row identifiers]
FROM [DEST] 
JOIN [STAGING]
JOIN [@SET]; 
INSERT INTO @TEMP 
INSERT [DEST] 
SELECT
OUTPUT INSERT.ID, DATE [row identifiers] 
FROM [STAGING] 
JOIN [@SET] 
LEFT JOIN [DEST]

UPDATE Staging
SET inProgress = NULL
FROM Staging 
JOIN @set
ON @Set.Key = Staging.Key
JOIN @temp
ON @temp.id = @set.ID
AND @temp.date = @set.Date

Probablemente podría ejecutar múltiples trabajos haciendo estallar los lotes de actualización, y necesitaría un trabajo separado ejecutando una eliminación lenta

while exists (inProgress is null) 
delete top (100) from staging where inProgress is null 

para limpiar la mesa de ensayo.

Alocito
fuente