2011-04-06 32 views
8

Estoy probando un proceso que elimina muchos, muchos registros a la vez. No puede TRUNCATE TABLE, porque hay registros allí que deben permanecer.SQL Server Tiempo de espera de bloqueo excedido Eliminación de registros en un bucle

Debido al volumen, he roto la eliminación en un bucle similar a esto:

-- Do not block if records are locked. 
SET LOCK_TIMEOUT 0 
-- This process should be chosen as a deadlock victim in the case of a deadlock. 
SET DEADLOCK_PRIORITY LOW 
SET NOCOUNT ON 

DECLARE @Count 
SET @Count = 1 
WHILE @Count > 0 
BEGIN TRY 
    BEGIN TRANSACTION -- added per comment below 

    DELETE TOP (1000) FROM MyTable WITH (ROWLOCK, READPAST) WHERE MyField = SomeValue 
    SET @Count == @@ROWCOUNT 

    COMMIT 
END TRY 
BEGIN CATCH 
    exec sp_lock -- added to display the open locks after the timeout 
    exec sp_who2 -- shows the active processes 

    IF @@TRANCOUNT > 0 
     ROLLBACK 
    RETURN -- ignoring this error for brevity 
END CATCH 

MiTabla es una tabla agrupada. MyField está en la primera columna del índice agrupado. Indica una agrupación lógica de registros, por lo que MyField = SomeValue a menudo selecciona muchos registros. No me importa en qué orden se eliminan siempre que se procese un grupo a la vez. No hay otros índices en esta tabla.

He añadido la sugerencia ROWLOCK para tratar de evitar las escaladas de bloqueo que hemos visto en la producción. Agregué la sugerencia READPAST para evitar borrar registros bloqueados por otros procesos. Eso nunca debería suceder, pero estoy tratando de estar seguro.

Problema: a veces este ciclo llega a un tiempo de espera de bloqueo 1222 "Excedido el tiempo de espera de solicitud de bloqueo excedido" cuando es el único en ejecución.

Estoy seguro de que no hay otra actividad en este sistema mientras estoy probando este proceso, porque es mi propio cuadro de desarrollador, nadie más está conectado, no hay otros procesos ejecutándose en él y el generador de perfiles no muestra actividad.

Puedo volver a ejecutar el mismo script un segundo más tarde y se retoma donde lo dejó, borrando felizmente registros-- hasta el siguiente tiempo de espera de bloqueo.

He intentado un BEGIN TRY/BEGIN CATCH para ignorar el error 1222 y volver a intentar la eliminación, pero falla de nuevo inmediatamente con el mismo error de tiempo de espera de bloqueo. También falla nuevamente si agrego un pequeño retraso antes de volver a intentar.

Supongo que los tiempos de espera de bloqueo se deben a algo así como una división de página, pero no estoy seguro de por qué esto entraría en conflicto con la iteración actual del ciclo. La declaración de eliminación anterior ya debería haberse completado, y pensé que eso significaba que las divisiones de página también habían finalizado.

¿Por qué el ciclo DELETE golpea un tiempo de espera de bloqueo contra sí mismo?

¿Hay alguna manera en que el proceso pueda evitar este tiempo de espera de bloqueo o detectar que es seguro reanudar?

Esto es en SQL Server 2005.

- EDITAR -

he añadido el evento Lock: Tiempo de espera para el generador de perfiles. Es el tiempo de espera en un PageLock durante el borrado:

Event Class: Lock:Timeout 
TextData: 1:15634 (one example of several) 
Mode:  7 - IU 
Type:  6 - PAGE 

DBCC página Informes de estas páginas se encuentran fuera del rango de la base de datos maestra (ID 1).

- EDIT 2 -

he añadido un BEGIN TRY/BEGIN CATCH y corrió un exec sp_lock en el bloque catch.Esto es lo que vi:

spid dbid ObjId  IndId Type Resource Mode Status 
19 2 1401108082 1  PAG 1:52841 X GRANT (tempdb.dbo.MyTable) 
19 2 1401108082 0  TAB   IX GRANT (tempdb.dbo.MyTable) 
Me 2 1401108082 0  TAB   IX GRANT (tempdb.dbo.MyTable) 
Me 1 1115151018 0  TAB   IS GRANT (master..spt_values) (?) 

SPID 19 es un TASK MANAGER de SQL Server. ¿Por qué uno de estos administradores de tareas estaría adquiriendo bloqueos en MyTable?

+0

¿Ha intentado trazar los diversos eventos de bloqueo en SQL Trace para ver si se puede deshacer lo que está pasando? –

+0

Acabo de hacer, gracias por mencionar eso. Agregué la información de tiempo de espera de bloqueo anterior. No estoy seguro de qué está bloqueado exactamente. –

+0

Otra edición: agregó algo de información de sp_lock inmediatamente después del tiempo de espera de bloqueo. –

Respuesta

6

He encontrado la respuesta: mi eliminación en bucle está en conflicto con el proceso de limpieza de fantasmas.

Usando la sugerencia de Nicholas, agregué un BEGIN TRANSACTION y un COMMIT. Envolví el bucle de eliminación en un BEGIN TRY/BEGIN CATCH. En el BEGIN CATCH, justo antes de ROLLBACK, ejecuté sp_lock y sp_who2. (He añadido los cambios de código en la pregunta anterior.)

Cuando mi proceso bloqueado, vi el siguiente resultado:

spid dbid ObjId  IndId Type Resource       Mode  Status 
------ ------ ----------- ------ ---- -------------------------------- -------- ------ 
20  2  1401108082 0  TAB         IX  GRANT 
20  2  1401108082 1  PAG 1:102368       X  GRANT 

SPID Status  Login HostName BlkBy DBName Command  CPUTime DiskIO 
---- ---------- ----- -------- ----- ------ ------------- ------- ------ 
20 BACKGROUND sa .  .  tempdb GHOST CLEANUP 31  0 

Para referencia futura, cuando SQL Server elimina los registros, se pone un poco en ellas simplemente marcarlos como "registros fantasma". Cada pocos minutos, se ejecuta un proceso interno llamado limpieza de fantasmas para reclamar páginas de registros que se han eliminado por completo (es decir, todos los registros son registros fantasmas).

The ghost cleanup process was discussed on ServerFault in this question.

Here is Paul S. Randal's explanation of the ghost cleanup process.

It is possible to disable the ghost cleanup process with a trace flag. Pero yo no tenía que hacerlo en este caso.

Terminé agregando un tiempo de espera de espera de bloqueo de 100 ms. Esto ocasiona tiempos de espera de bloqueo ocasionales en el proceso de limpieza del registro fantasma, pero eso es aceptable. También agregué un bucle que reintenta el tiempo de espera de bloqueo hasta 5 veces. Con estos dos cambios, mi proceso ahora generalmente se completa. Ahora solo se agota el tiempo de espera si hay un proceso muy largo que genera gran cantidad de datos que adquieren bloqueos de tabla o página en los datos que mi proceso necesita para limpiar.

EDITAR 2016-07-20

El código final es el siguiente:

-- Do not block long if records are locked. 
SET LOCK_TIMEOUT 100 

-- This process volunteers to be a deadlock victim in the case of a deadlock. 
SET DEADLOCK_PRIORITY LOW 

DECLARE @Error BIT 
SET @Error = 0 

DECLARE @ErrMsg VARCHAR(1000) 
DECLARE @DeletedCount INT 
SELECT @DeletedCount = 0 

DECLARE @LockTimeoutCount INT 
SET @LockTimeoutCount = 0 

DECLARE @ContinueDeleting BIT, 
    @LastDeleteSuccessful BIT 

SET @ContinueDeleting = 1 
SET @LastDeleteSuccessful = 1 

WHILE @ContinueDeleting = 1 
BEGIN 
    DECLARE @RowCount INT 
    SET @RowCount = 0 

    BEGIN TRY 

     BEGIN TRANSACTION 

     -- The READPAST below attempts to skip over locked records. 
     -- However, it might still cause a lock wait error (1222) if a page or index is locked, because the delete has to modify indexes. 
     -- The threshold for row lock escalation to table locks is around 5,000 records, 
     -- so keep the deleted number smaller than this limit in case we are deleting a large chunk of data. 
     -- Table name, field, and value are all set dynamically in the actual script. 
     SET @SQL = N'DELETE TOP (1000) MyTable WITH(ROWLOCK, READPAST) WHERE MyField = SomeValue' 
     EXEC sp_executesql @SQL, N'@ProcGuid uniqueidentifier', @ProcGUID 

     SET @RowCount = @@ROWCOUNT 

     COMMIT 

     SET @LastDeleteSuccessful = 1 

     SET @DeletedCount = @DeletedCount + @RowCount 
     IF @RowCount = 0 
     BEGIN 
      SET @ContinueDeleting = 0 
     END 

    END TRY 
    BEGIN CATCH 

     IF @@TRANCOUNT > 0 
      ROLLBACK 

     IF Error_Number() = 1222 -- Lock timeout 
     BEGIN 

      IF @LastDeleteSuccessful = 1 
      BEGIN 
       -- If we hit a lock timeout, and we had already deleted something successfully, try again. 
       SET @LastDeleteSuccessful = 0 
      END 
      ELSE 
      BEGIN 
       -- The last delete failed, too. Give up for now. The job will run again shortly. 
       SET @ContinueDeleting = 0 
      END 
     END 
     ELSE -- On anything other than a lock timeout, report an error. 
     BEGIN  
      SET @ErrMsg = 'An error occurred cleaning up data. Table: MyTable Column: MyColumn Value: SomeValue. Message: ' + ERROR_MESSAGE() + ' Error Number: ' + CONVERT(VARCHAR(20), ERROR_NUMBER()) + ' Line: ' + CONVERT(VARCHAR(20), ERROR_LINE()) 
      PRINT @ErrMsg -- this error message will be included in the SQL Server job history 
      SET @Error = 1 
      SET @ContinueDeleting = 0 
     END 

    END CATCH 

END 

IF @Error <> 0 
    RAISERROR('Not all data could be cleaned up. See previous messages.', 16, 1) 
+0

¿Podría publicar su solución de producción después de la corrección? –

+0

@RonnieOverby Agregué una solución de muestra. Nuestro código de producción real es más complejo que esto, porque limpia varias tablas diferentes a través de SQL dinámico. Este código anterior no incluye ese equipaje adicional. –

+0

Impresionante. Gracias por tomarte el tiempo para hacer eso. –

4

Usted u otra persona que use la conexión está configurando el tiempo de espera de bloqueo en un valor distinto al predeterminado. Vea http://msdn.microsoft.com/en-US/library/ms189470(v=SQL.90).aspx para más detalles.

El tiempo de bloqueo predeterminado es -1 milisegundos, lo que significa "Esperar para siempre".

Las sugerencias de filas son agradables, pero son un olor a código y deben evitarse. Deje que SQL Server haga su trabajo. Tiene más información que tú sobre el sistema en general.

Para empezar, no puede controlar el tamaño de bloqueo: la escalada de bloqueos se produce automáticamente, en función del número de bloqueos pendientes. Comienza con bloqueos de fila. Si acumula demasiados bloqueos de fila, SQL Server escalará al bloqueo de página. Adquirir demasiados bloqueos de página y escala a bloqueos de tabla. Consulte http://msdn.microsoft.com/en-us/library/ms184286(v=SQL.90).aspx para obtener detalles de escalamiento de bloqueo. Sin embargo, hay un par de indicadores de seguimiento que puede establecer que evitarán la escalada de bloqueo: sin embargo, eso degradará el rendimiento del SQL Server.

Otra cosa: debe ajustar la declaración DELETE en una transacción, especialmente en un procedimiento almacenado.

DECLARE @Count INT 
SET @Count = 1 
WHILE @Count > 0 
    BEGIN 
    BEGIN TRANSACTION 
    DELETE TOP (1000) FROM MyTable WITH (ROWLOCK, READPAST) WHERE MyField = SomeValue 
    SET @Count = @@ROWCOUNT 
    COMMIT TRANSACTION 
    END 

Esto deja clara su intención y asegura que los bloqueos se liberen cuando deberían.

+1

SQL no escalará un bloqueo de fila a un bloqueo de página; se escalará directamente a un bloqueo de tabla. http://www.sqlskills.com/BLOGS/PAUL/post/A-SQL-Server-DBA-myth-a-day-(2330)-lock-escalation.aspx –

+0

Tiene razón en que el código establece LOCK_TIMEOUT en 0 . Acabo de incluir eso arriba; perdón por no mencionarlo antes. –

+0

Envolver esto en una transacción ha ayudado a identificar los bloqueos abiertos en el momento del tiempo de espera de bloqueo. Ver las ediciones anteriores. –

Cuestiones relacionadas