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:
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
fuente
([TimeStamp] DESC, [InverterID] ASC)
ve como una opción extraña para el índice agrupado. Me refiero a laDESC
parte.Respuestas:
Primero, aunque no está relacionado con la pregunta principal, su
MERGE
declaració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: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
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
MERGE
instrucció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: 1
en 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.
fuente
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
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
para limpiar la mesa de ensayo.
fuente