En SQL Server, ¿hay alguna forma de verificar si un grupo seleccionado de filas está bloqueado o no?


21

Estamos intentando actualizar / eliminar una gran cantidad de registros en una tabla de filas multimillonaria. Como esta es una tabla popular, hay mucha actividad en diferentes secciones de esta tabla. Cualquier actividad grande de actualización / eliminación se bloquea durante largos períodos de tiempo (ya que está esperando obtener bloqueos en todas las filas o bloqueo de página o bloqueo de tabla), lo que resulta en tiempos de espera o toma varios días para completar la tarea.

Por lo tanto, estamos cambiando el enfoque para eliminar pequeños lotes de filas a la vez. Pero queremos verificar si los seleccionados (digamos 100 o 1000 o 2000 filas) están bloqueados actualmente por un proceso diferente o no.

  • De lo contrario, proceda con la eliminación / actualización.
  • Si están bloqueados, pase al siguiente grupo de registros.
  • Al final, regrese al principio e intente actualizar / eliminar los que quedan fuera.

¿Es esto factible?

Gracias, ToC


2
¿Has mirado READPAST como parte de la declaración de eliminación o NOWAIT (para fallar a todo el grupo)? Uno de estos puede funcionar para usted. msdn.microsoft.com/en-us/library/ms187373.aspx
Sean dice Eliminar Sara Chipps

@SeanGallardy No he considerado esa idea, pero ahora lo haré. ¿Pero hay una manera más fácil de verificar si una fila en particular está bloqueada o no? Gracias.
ToC

3
También puede consultar LOCK_TIMEOUT ( msdn.microsoft.com/en-us/library/ms189470.aspx ). Por ejemplo, esta es la forma en que sp_whoisactive de Adam Machanic se asegura de que el procedimiento no espere demasiado si se bloquea al intentar recopilar un plan de ejecución. Puede establecer un tiempo de espera corto o incluso usar un valor de 0 ("0 significa no esperar en absoluto y devolver un mensaje tan pronto como se encuentre un bloqueo"). Puede combinar esto con un INTENTO / CAPTURA para detectar el error 1222 ( "Tiempo de espera de solicitud de bloqueo excedido") y continúe con el siguiente lote.
Geoff Patterson

@gpatterson Enfoque interesante. Intentaré esto también.
ToC

2
Para responder, no, no hay una manera más fácil de ver si las filas están bloqueadas a menos que se haga algo específicamente en la aplicación. Básicamente, primero puede hacer una selección con HOLDLOCK y XLOCK con un conjunto lock_timeout (que es de lo que se trata NOWAIT en mi comentario original, estableciendo el tiempo de espera en 0). Si no lo entiendes, entonces sabes que algo está bloqueado. No hay nada fácilmente disponible para decir "¿Está la fila X en la Tabla Y usando el índice Z bloqueado por algo?". Podemos ver si la tabla tiene bloqueos o si las páginas / filas / teclas / etc. tienen bloqueos, pero traducir eso en filas específicas en una consulta no sería fácil.
Sean dice Eliminar Sara Chipps

Respuestas:


10

Si entiendo la solicitud correctamente, el objetivo es eliminar lotes de filas, mientras que, al mismo tiempo, se producen operaciones DML en filas en toda la tabla. El objetivo es eliminar un lote; sin embargo, si cualquier fila subyacente contenida dentro del rango definido por dicho lote está bloqueada, entonces debemos omitir ese lote y pasar al siguiente lote. Luego debemos volver a los lotes que no se eliminaron previamente y volver a intentar nuestra lógica de eliminación original. Debemos repetir este ciclo hasta que se eliminen todos los lotes de filas requeridos.

Como se ha mencionado, es razonable usar una sugerencia READPAST y el nivel de aislamiento READ COMMITTED (predeterminado), para omitir los rangos pasados ​​que pueden contener filas bloqueadas. Iré un paso más allá y recomendaré usar el nivel de aislamiento SERIALIZABLE y eliminar eliminaciones.

SQL Server usa bloqueos de rango clave para proteger un rango de filas implícitamente incluido en un conjunto de registros que se lee mediante una instrucción Transact-SQL mientras se usa el nivel de aislamiento de transacción serializable ... encuentre más aquí: https://technet.microsoft.com /en-US/library/ms191272(v=SQL.105).aspx

Con las eliminaciones de mordiscos, nuestro objetivo es aislar un rango de filas y asegurarnos de que no se produzcan cambios en esas filas mientras las eliminamos, es decir, no queremos lecturas o inserciones fantasmas. El nivel de aislamiento serializable está destinado a resolver este problema.

Antes de demostrar mi solución, me gustaría agregar que tampoco recomiendo cambiar el nivel de aislamiento predeterminado de su base de datos a SERIALIZABLE ni recomiendo que mi solución sea la mejor. Simplemente deseo presentarlo y ver a dónde podemos ir desde aquí.

Algunas notas de mantenimiento:

  1. La versión de SQL Server que estoy usando es Microsoft SQL Server 2012 - 11.0.5343.0 (X64)
  2. Mi base de datos de prueba está utilizando el modelo de recuperación COMPLETO

Para comenzar mi experimento, configuraré una base de datos de prueba, una tabla de muestra y llenaré la tabla con 2,000,000 filas.


USE [master];
GO

SET NOCOUNT ON;

IF DATABASEPROPERTYEX (N'test', N'Version') > 0
BEGIN
    ALTER DATABASE [test] SET SINGLE_USER
        WITH ROLLBACK IMMEDIATE;
    DROP DATABASE [test];
END
GO

-- Create the test database
CREATE DATABASE [test];
GO

-- Set the recovery model to FULL
ALTER DATABASE [test] SET RECOVERY FULL;

-- Create a FULL database backup
-- in order to ensure we are in fact using 
-- the FULL recovery model
-- I pipe it to dev null for simplicity
BACKUP DATABASE [test]
TO DISK = N'nul';
GO

USE [test];
GO

-- Create our table
IF OBJECT_ID('dbo.tbl','U') IS NOT NULL
BEGIN
    DROP TABLE dbo.tbl;
END;
CREATE TABLE dbo.tbl
(
      c1 BIGINT IDENTITY (1,1) NOT NULL
    , c2 INT NOT NULL
) ON [PRIMARY];
GO

-- Insert 2,000,000 rows 
INSERT INTO dbo.tbl
    SELECT TOP 2000
        number
    FROM
        master..spt_values
    ORDER BY 
        number
GO 1000

En este punto, necesitaremos uno o más índices sobre los que puedan actuar los mecanismos de bloqueo del nivel de aislamiento SERIALIZABLE.


-- Add a clustered index
CREATE UNIQUE CLUSTERED INDEX CIX_tbl_c1
    ON dbo.tbl (c1);
GO

-- Add a non-clustered index
CREATE NONCLUSTERED INDEX IX_tbl_c2 
    ON dbo.tbl (c2);
GO

Ahora, verifiquemos que se crearon nuestras 2,000,000 filas


SELECT
    COUNT(*)
FROM
    tbl;

ingrese la descripción de la imagen aquí

Entonces, tenemos nuestra base de datos, tabla, índices y filas. Entonces, configuremos el experimento para eliminar eliminaciones. Primero, debemos decidir cuál es la mejor manera de crear un mecanismo típico de eliminación de mordiscos.


DECLARE
      @BatchSize        INT    = 100
    , @LowestValue      BIGINT = 20000
    , @HighestValue     BIGINT = 20010
    , @DeletedRowsCount BIGINT = 0
    , @RowCount         BIGINT = 1;

SET NOCOUNT ON;
GO

WHILE  @DeletedRowsCount <  ( @HighestValue - @LowestValue ) 
BEGIN

    SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
    BEGIN TRANSACTION

        DELETE 
        FROM
            dbo.tbl 
        WHERE
            c1 IN ( 
                    SELECT TOP (@BatchSize)
                        c1
                    FROM
                        dbo.tbl 
                    WHERE 
                        c1 BETWEEN @LowestValue AND @HighestValue
                    ORDER BY 
                        c1
                  );

        SET @RowCount = ROWCOUNT_BIG();

    COMMIT TRANSACTION;

    SET @DeletedRowsCount += @RowCount;
    WAITFOR DELAY '000:00:00.025';
    CHECKPOINT;

END;

Como puede ver, coloqué la transacción explícita dentro del ciclo while. Si desea limitar las descargas de registros, no dude en colocarlo fuera del ciclo. Además, dado que estamos en el modelo de recuperación COMPLETA, es posible que desee crear copias de seguridad del registro de transacciones con mayor frecuencia mientras ejecuta sus operaciones de eliminación de mordiscos, para garantizar que su registro de transacciones no pueda crecer escandalosamente.

Entonces, tengo un par de objetivos con esta configuración. Primero, quiero mis cerraduras de rango de llave; entonces, trato de mantener los lotes lo más pequeños posible. Tampoco quiero impactar negativamente la concurrencia en mi tabla "gigantesca"; Entonces, quiero tomar mis cerraduras y dejarlas tan rápido como pueda. Por lo tanto, le recomiendo que reduzca el tamaño de sus lotes.

Ahora, quiero proporcionar un ejemplo muy breve de esta rutina de eliminación en acción. Debemos abrir una nueva ventana dentro de SSMS y eliminar una fila de nuestra tabla. Haré esto dentro de una transacción implícita usando el nivel de aislamiento predeterminado LEER COMPROMETIDO.


DELETE FROM
    dbo.tbl
WHERE
    c1 = 20005;

¿Esta fila se eliminó realmente?


SELECT
    c1
FROM
    dbo.tbl
WHERE
    c1 BETWEEN 20000 AND 20010;

Sí, fue eliminado.

Prueba de fila eliminada

Ahora, para ver nuestros bloqueos, abramos una nueva ventana dentro de SSMS y agreguemos un fragmento de código o dos. Estoy usando el sp_whoisactive de Adam Mechanic, que se puede encontrar aquí: sp_whoisactive


SELECT
    DB_NAME(resource_database_id) AS DatabaseName
  , resource_type
  , request_mode
FROM
    sys.dm_tran_locks
WHERE
    DB_NAME(resource_database_id) = 'test'
    AND resource_type = 'KEY'
ORDER BY
    request_mode;

-- Our insert
sp_lock 55;

-- Our deletions
sp_lock 52;

-- Our active sessions
sp_whoisactive;

Ahora estamos listos para comenzar. En una nueva ventana de SSMS, comencemos una transacción explícita que intentará volver a insertar la fila que eliminamos. Al mismo tiempo, activaremos nuestra operación de eliminación de mordiscos.

El código de inserción:


BEGIN TRANSACTION

    SET IDENTITY_INSERT dbo.tbl ON;

    INSERT  INTO dbo.tbl
            ( c1 , c2 )
    VALUES
            ( 20005 , 1 );

    SET IDENTITY_INSERT dbo.tbl OFF;

--COMMIT TRANSACTION;

Comencemos ambas operaciones comenzando con la inserción y seguidas por nuestras eliminaciones. Podemos ver las cerraduras de la gama de llaves y las cerraduras exclusivas.

Gama y cerraduras exclusivas

El inserto generó estos bloqueos:

Cerraduras de inserción

La eliminación / selección mordisqueada mantiene estos bloqueos:

ingrese la descripción de la imagen aquí

Nuestra inserción está bloqueando nuestra eliminación como se esperaba:

Insertar bloques Eliminar

Ahora, confirmemos la transacción de inserción y veamos qué sucede.

Confirmar la eliminación

Y como se esperaba, todas las transacciones se completan. Ahora, debemos verificar si la inserción fue fantasma o si la operación de eliminación también la eliminó.


SELECT
    c1
FROM
    dbo.tbl
WHERE
    c1 BETWEEN 20000 AND 20015;

De hecho, el inserto fue eliminado; entonces, no se permitió ningún inserto fantasma.

Sin inserción fantasma

Entonces, en conclusión, creo que la verdadera intención de este ejercicio no es tratar de rastrear cada fila, página o bloqueo a nivel de tabla e intentar determinar si un elemento de un lote está bloqueado y, por lo tanto, requeriría nuestra operación de eliminación para Espere. Esa pudo haber sido la intención de los interrogadores; sin embargo, esa tarea es hercúlea y básicamente poco práctica, si no imposible. El objetivo real es garantizar que no surjan fenómenos no deseados una vez que hayamos aislado el rango de nuestro lote con bloqueos propios y luego precedamos a eliminar el lote. El nivel de aislamiento SERIALIZABLE logra este objetivo. La clave es mantener sus bocaditos pequeños, su registro de transacciones bajo control y eliminar los fenómenos no deseados.

Si desea velocidad, no cree tablas gigantescas que no puedan dividirse y, por lo tanto, no pueda utilizar el cambio de partición para obtener los resultados más rápidos. La clave para acelerar es el particionamiento y el paralelismo; la clave del sufrimiento son los mordiscos y el bloqueo en vivo.

Por favor déjame saber lo que piensa.

Creé algunos ejemplos adicionales del nivel de aislamiento SERIALIZABLE en acción. Deben estar disponibles en los enlaces a continuación.

Eliminar operación

Insertar operación

Operaciones de igualdad: bloqueos de rango clave en los siguientes valores clave

Operaciones de igualdad: recuperación de datos existentes de Singleton

Operaciones de igualdad: recuperación de datos no existentes de Singleton

Operaciones de desigualdad: bloqueos de rango clave en rango y valores clave siguientes


9

Por lo tanto, estamos cambiando el enfoque para eliminar pequeños lotes de filas a la vez.

Esta es una muy buena idea para eliminar en pequeños lotes o fragmentos cuidadosos . Me gustaría añadir una pequeña waitfor delay '00:00:05'y dependiendo del modelo de recuperación de la base de datos - Si FULL, a continuación, hacer una log backupy si SIMPLEluego hacer una manual CHECKPOINTpara evitar la hinchazón de registro de transacciones - entre lotes.

Pero queremos verificar si los seleccionados (digamos 100 o 1000 o 2000 filas) están bloqueados actualmente por un proceso diferente o no.

Lo que está diciendo no es totalmente posible fuera de la caja (teniendo en cuenta sus 3 puntos). Si la sugerencia anterior small batches + waitfor delayno funciona (siempre que realice las pruebas adecuadas), puede usar el query HINT.

No lo use NOLOCK- vea kb / 308886 , Problemas de coherencia de lectura del servidor SQL por Itzik Ben-Gan , poniendo NOLOCK en todas partes - Por Aaron Bertrand y SQL Server NOLOCK Hint y otras ideas pobres .

READPASTSugerencia ayudará en su escenario. La esencia de la READPASTpista es: si hay un bloqueo de nivel de fila, el servidor SQL no lo leerá.

Especifica que el Motor de base de datos no lee las filas que están bloqueadas por otras transacciones. Cuando READPASTse especifica, los bloqueos de nivel de fila se omiten. Es decir, el Motor de base de datos omite las filas en lugar de bloquear la transacción actual hasta que se liberan los bloqueos.

Durante mis pruebas limitadas, encontré un rendimiento realmente bueno cuando utilicé DELETE from schema.tableName with (READPAST, READCOMMITTEDLOCK)y configuré el nivel de aislamiento de la sesión de consulta para READ COMMITTEDusar, SET TRANSACTION ISOLATION LEVEL READ COMMITTEDque de todos modos es el nivel de aislamiento predeterminado.


2

Resumiendo otros enfoques originalmente ofrecidos en los comentarios a la pregunta.


  1. Úselo NOWAITsi el comportamiento deseado es fallar todo el fragmento tan pronto como se encuentre un bloqueo incompatible.

    De la NOWAITdocumentación :

    Indica al Motor de base de datos que devuelva un mensaje tan pronto como se encuentre un bloqueo en la tabla. NOWAITes equivalente a especificar SET LOCK_TIMEOUT 0para una tabla específica. La NOWAITsugerencia no funciona cuando TABLOCKtambién se incluye la sugerencia. Para finalizar una consulta sin esperar cuando se utiliza la TABLOCKsugerencia, en su SETLOCK_TIMEOUT 0;lugar , prólogo de la consulta .

  2. Úselo SET LOCK_TIMEOUTpara lograr un resultado similar, pero con un tiempo de espera configurable:

    De la SET LOCK_TIMEOUTdocumentación

    Especifica el número de milisegundos que una instrucción espera a que se libere un bloqueo.

    Cuando una espera para un bloqueo excede el valor de tiempo de espera, se devuelve un error. Un valor de 0 significa no esperar en absoluto y devolver un mensaje tan pronto como se encuentre un bloqueo.


0

Para suponer que tenemos 2 consultas paralelas:

conectar / sesión 1: bloqueará la fila = 777

SELECT * FROM your_table WITH(UPDLOCK,READPAST) WHERE id = 777

conectar / sesión 2: ignorará la fila bloqueada = 777

SELECT * FROM your_table WITH(UPDLOCK,READPAST) WHERE id = 777

O conecte / sesión 2: lanzará una excepción

DECLARE @id integer;
SELECT @id = id FROM your_table WITH(UPDLOCK,READPAST) WHERE id = 777;
IF @id is NULL
  THROW 51000, 'Hi, a record is locked or does not exist.', 1;

-1

Intenta filtrar algo como esto: puede ser complicado si quieres ser muy, muy específico. Busque en BOL la descripción de sys.dm_tran_locks

SELECT 
tl.request_session_id,
tl.resource_type,
tl.resource_associated_entity_id,
db_name(tl.resource_database_id) 'Database',
CASE 
    WHEN tl.resource_type = 'object' THEN object_name(tl.resource_associated_entity_id, tl.resource_database_id)
    ELSE NULL
END 'LockedObject',
tl.resource_database_id,
tl.resource_description,
tl.request_mode,
tl.request_type,
tl.request_status FROM [sys].[dm_tran_locks] tl WHERE resource_database_id <> 2order by tl.request_session_id

Simplemente curioso, ¿por qué el voto negativo?
rottengeek

-11

Puede usar NoLOCK mientras está eliminando y si las filas están bloqueadas, no se eliminarán. No es ideal, pero puede hacer el truco para usted.

DELETE TA FROM dbo.TableA TA WITH (NOLOCK) WHERE Condition = True

77
Si intento eso en mi máquina local, me sale Msg 1065, Level 15, State 1, Line 15 The NOLOCK and READUNCOMMITTED lock hints are not allowed for target tables of INSERT, UPDATE, DELETE or MERGE statements., en desuso desde 2005
Tom V - Equipo Mónica
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.