Eliminación lenta de registros cuando un activador está habilitado

17

Pensé que esto se resolvió con el siguiente enlace, el trabajo alrededor funciona, pero el parche no. Trabajando con el soporte de Microsoft para resolver.

http://support.microsoft.com/kb/2606883

Ok, tengo un problema que quería lanzar a StackOverflow para ver si alguien tiene una idea.

Tenga en cuenta que esto es con SQL Server 2008 R2

Problema: la eliminación de 3000 registros de una tabla con 15000 registros demora de 3 a 4 minutos cuando se activa un activador y solo de 3 a 5 segundos cuando el activador está desactivado.

Configuración de la mesa

Dos tablas que llamaremos Principal y Secundaria. Secundaria contiene registros de los elementos que deseo eliminar, por lo que cuando realizo la eliminación, me uno a la tabla Secundaria. Se ejecuta un proceso antes de la declaración de eliminación para completar la tabla secundaria con los registros que se eliminarán.

Eliminar declaración:

DELETE FROM MAIN 
WHERE ID IN (
   SELECT Secondary.ValueInt1 
   FROM Secondary 
   WHERE SECONDARY.GUID = '9FFD2C8DD3864EA7B78DA22B2ED572D7'
);

Esta tabla tiene muchas columnas y alrededor de 14 índices NC diferentes. Probé un montón de cosas diferentes antes de determinar que el desencadenante era el problema.

  • Activa el bloqueo de página (lo hemos desactivado por defecto)
  • Estadísticas recopiladas manualmente
  • Desactivado auto recopilación de estadísticas
  • Índice verificado de salud y fragmentación
  • Quitó el índice agrupado de la tabla
  • Se examinó el plan de ejecución (nada se mostraba como índices faltantes y el costo era del 70 por ciento hacia la eliminación real con aproximadamente el 28 por ciento para la combinación / fusión de los registros

Disparadores

La tabla tiene 3 disparadores (uno para cada operación de inserción, actualización y eliminación). Modifiqué el código para que el disparador de eliminación simplemente regresara, luego seleccioné uno para ver cuántas veces se disparó. Solo se dispara una vez durante toda la operación (como se esperaba).

ALTER TRIGGER [dbo].[TR_MAIN_RD] ON [dbo].[MAIN]
            AFTER DELETE
            AS  
                SELECT 1
                RETURN

Recordar

  • Con Trigger activado: la declaración tarda de 3 a 4 minutos en completarse
  • Con Trigger desactivado: la declaración tarda de 3 a 5 segundos en completarse

Alguien tiene alguna idea de por qué?

También tenga en cuenta que no busca cambiar esta arquitectura, agregar índices de eliminación, etc. como solución. Esta tabla es la pieza central de algunas operaciones de datos importantes y tuvimos que ajustarla y ajustarla (índices, bloqueo de página, etc.) para permitir que las principales operaciones de concurrencia funcionen sin puntos muertos.

Aquí está el plan de ejecución xml (los nombres fueron cambiados para proteger a los inocentes)

<?xml version="1.0" encoding="utf-16"?>
<ShowPlanXML xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema" Version="1.1" Build="10.50.1790.0" xmlns="http://schemas.microsoft.com/sqlserver/2004/07/showplan">
  <BatchSequence>
    <Batch>
      <Statements>
        <StmtSimple StatementCompId="1" StatementEstRows="185.624" StatementId="1" StatementOptmLevel="FULL" StatementOptmEarlyAbortReason="GoodEnoughPlanFound" StatementSubTreeCost="0.42706" StatementText="DELETE FROM MAIN WHERE ID IN (SELECT Secondary.ValueInt1 FROM Secondary WHERE Secondary.SetTMGUID = '9DDD2C8DD3864EA7B78DA22B2ED572D7')" StatementType="DELETE" QueryHash="0xAEA68D887C4092A1" QueryPlanHash="0x78164F2EEF16B857">
          <StatementSetOptions ANSI_NULLS="true" ANSI_PADDING="true" ANSI_WARNINGS="true" ARITHABORT="false" CONCAT_NULL_YIELDS_NULL="true" NUMERIC_ROUNDABORT="false" QUOTED_IDENTIFIER="true" />
          <QueryPlan CachedPlanSize="48" CompileTime="20" CompileCPU="20" CompileMemory="520">
            <RelOp AvgRowSize="9" EstimateCPU="0.00259874" EstimateIO="0.296614" EstimateRebinds="0" EstimateRewinds="0" EstimateRows="185.624" LogicalOp="Delete" NodeId="0" Parallel="false" PhysicalOp="Clustered Index Delete" EstimatedTotalSubtreeCost="0.42706">
              <OutputList />
              <Update WithUnorderedPrefetch="true" DMLRequestSort="false">
                <Object Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Index="[IX_MAIN_02]" IndexKind="Clustered" />
                <Object Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Index="[PK_MAIN_ID]" IndexKind="NonClustered" />
                <Object Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Index="[UK_MAIN_01]" IndexKind="NonClustered" />
                <Object Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Index="[IX_MAIN_03]" IndexKind="NonClustered" />
                <Object Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Index="[IX_MAIN_04]" IndexKind="NonClustered" />
                <Object Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Index="[IX_MAIN_05]" IndexKind="NonClustered" />
                <Object Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Index="[IX_MAIN_06]" IndexKind="NonClustered" />
                <Object Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Index="[IX_MAIN_07]" IndexKind="NonClustered" />
                <Object Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Index="[IX_MAIN_08]" IndexKind="NonClustered" />
                <Object Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Index="[IX_MAIN_09]" IndexKind="NonClustered" />
                <Object Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Index="[IX_MAIN_10]" IndexKind="NonClustered" />
                <Object Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Index="[IX_MAIN_11]" IndexKind="NonClustered" />
                <Object Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Index="[UK_MAIN_12]" IndexKind="NonClustered" />
                <Object Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Index="[IX_MAIN_13]" IndexKind="NonClustered" />
                <RelOp AvgRowSize="15" EstimateCPU="1.85624E-05" EstimateIO="0" EstimateRebinds="0" EstimateRewinds="0" EstimateRows="185.624" LogicalOp="Top" NodeId="2" Parallel="false" PhysicalOp="Top" EstimatedTotalSubtreeCost="0.127848">
                  <OutputList>
                    <ColumnReference Column="Uniq1002" />
                    <ColumnReference Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Column="RelationshipID" />
                  </OutputList>
                  <Top RowCount="true" IsPercent="false" WithTies="false">
                    <TopExpression>
                      <ScalarOperator ScalarString="(0)">
                        <Const ConstValue="(0)" />
                      </ScalarOperator>
                    </TopExpression>
                    <RelOp AvgRowSize="15" EstimateCPU="0.0458347" EstimateIO="0" EstimateRebinds="0" EstimateRewinds="0" EstimateRows="185.624" LogicalOp="Left Semi Join" NodeId="3" Parallel="false" PhysicalOp="Merge Join" EstimatedTotalSubtreeCost="0.12783">
                      <OutputList>
                        <ColumnReference Column="Uniq1002" />
                        <ColumnReference Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Column="RelationshipID" />
                      </OutputList>
                      <Merge ManyToMany="false">
                        <InnerSideJoinColumns>
                          <ColumnReference Database="[MyDatabase]" Schema="[dbo]" Table="[Secondary]" Column="ValueInt1" />
                        </InnerSideJoinColumns>
                        <OuterSideJoinColumns>
                          <ColumnReference Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Column="ID" />
                        </OuterSideJoinColumns>
                        <Residual>
                          <ScalarOperator ScalarString="[MyDatabase].[dbo].[MAIN].[ID]=[MyDatabase].[dbo].[Secondary].[ValueInt1]">
                            <Compare CompareOp="EQ">
                              <ScalarOperator>
                                <Identifier>
                                  <ColumnReference Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Column="ID" />
                                </Identifier>
                              </ScalarOperator>
                              <ScalarOperator>
                                <Identifier>
                                  <ColumnReference Database="[MyDatabase]" Schema="[dbo]" Table="[Secondary]" Column="ValueInt1" />
                                </Identifier>
                              </ScalarOperator>
                            </Compare>
                          </ScalarOperator>
                        </Residual>
                        <RelOp AvgRowSize="19" EstimateCPU="0.0174567" EstimateIO="0.0305324" EstimateRebinds="0" EstimateRewinds="0" EstimateRows="15727" LogicalOp="Index Scan" NodeId="4" Parallel="false" PhysicalOp="Index Scan" EstimatedTotalSubtreeCost="0.0479891" TableCardinality="15727">
                          <OutputList>
                            <ColumnReference Column="Uniq1002" />
                            <ColumnReference Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Column="ID" />
                            <ColumnReference Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Column="RelationshipID" />
                          </OutputList>
                          <IndexScan Ordered="true" ScanDirection="FORWARD" ForcedIndex="false" ForceSeek="false" NoExpandHint="false">
                            <DefinedValues>
                              <DefinedValue>
                                <ColumnReference Column="Uniq1002" />
                              </DefinedValue>
                              <DefinedValue>
                                <ColumnReference Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Column="ID" />
                              </DefinedValue>
                              <DefinedValue>
                                <ColumnReference Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Column="RelationshipID" />
                              </DefinedValue>
                            </DefinedValues>
                            <Object Database="[MyDatabase]" Schema="[dbo]" Table="[MAIN]" Index="[PK_MAIN_ID]" IndexKind="NonClustered" />
                          </IndexScan>
                        </RelOp>
                        <RelOp AvgRowSize="11" EstimateCPU="0.00392288" EstimateIO="0.03008" EstimateRebinds="0" EstimateRewinds="0" EstimateRows="3423.53" LogicalOp="Index Seek" NodeId="5" Parallel="false" PhysicalOp="Index Seek" EstimatedTotalSubtreeCost="0.0340029" TableCardinality="171775">
                          <OutputList>
                            <ColumnReference Database="[MyDatabase]" Schema="[dbo]" Table="[Secondary]" Column="ValueInt1" />
                          </OutputList>
                          <IndexScan Ordered="true" ScanDirection="FORWARD" ForcedIndex="false" ForceSeek="false" NoExpandHint="false">
                            <DefinedValues>
                              <DefinedValue>
                                <ColumnReference Database="[MyDatabase]" Schema="[dbo]" Table="[Secondary]" Column="ValueInt1" />
                              </DefinedValue>
                            </DefinedValues>
                            <Object Database="[MyDatabase]" Schema="[dbo]" Table="[Secondary]" Index="[IX_Secondary_01]" IndexKind="NonClustered" />
                            <SeekPredicates>
                              <SeekPredicateNew>
                                <SeekKeys>
                                  <Prefix ScanType="EQ">
                                    <RangeColumns>
                                      <ColumnReference Database="[MyDatabase]" Schema="[dbo]" Table="[Secondary]" Column="SetTMGUID" />
                                    </RangeColumns>
                                    <RangeExpressions>
                                      <ScalarOperator ScalarString="'9DDD2C8DD3864EA7B78DA22B2ED572D7'">
                                        <Const ConstValue="'9DDD2C8DD3864EA7B78DA22B2ED572D7'" />
                                      </ScalarOperator>
                                    </RangeExpressions>
                                  </Prefix>
                                </SeekKeys>
                              </SeekPredicateNew>
                            </SeekPredicates>
                          </IndexScan>
                        </RelOp>
                      </Merge>
                    </RelOp>
                  </Top>
                </RelOp>
              </Update>
            </RelOp>
          </QueryPlan>
        </StmtSimple>
      </Statements>
    </Batch>
  </BatchSequence>
</ShowPlanXML>
tsells
fuente

Respuestas:

12

El marco de versiones de fila introducido en SQL Server 2005 se utiliza para admitir una serie de características, incluidos los nuevos niveles de aislamiento de transacciones READ_COMMITTED_SNAPSHOTy SNAPSHOT. Incluso cuando ninguno de estos niveles de aislamiento está habilitado, el control de versiones de fila todavía se usa para los AFTERdesencadenantes (para facilitar la generación de las insertedy deletedpseudo-tablas), MARS y (en una tienda de versiones separada) indexación en línea.

Como se documenta , el motor puede agregar un postfix de 14 bytes a cada fila de una tabla que se versiona para cualquiera de estos propósitos. Este comportamiento es relativamente conocido, al igual que la adición de los datos de 14 bytes a cada fila de un índice que se reconstruye en línea con un nivel de aislamiento de versiones de fila habilitado. Incluso cuando los niveles de aislamiento no están habilitados, se agrega un byte adicional a los índices no agrupados solo cuando se reconstruye ONLINE.

Cuando un gatillo después de que se presente y versiones de lo contrario añadir 14 bytes por fila, existe una optimización dentro del motor para evitar esto, pero donde un ROW_OVERFLOWo LOBno puede ocurrir la asignación. En la práctica, esto significa que el tamaño máximo posible de una fila debe ser inferior a 8060 bytes. Al calcular el tamaño máximo de fila posible, el motor supone, por ejemplo, que una columna VARCHAR (460) podría contener 460 caracteres.

El comportamiento es más fácil de ver con un AFTER UPDATEdisparador, aunque se aplica el mismo principio AFTER DELETE. El siguiente script crea una tabla con una longitud máxima de fila de 8060 bytes. Los datos se ajustan en una sola página, con 13 bytes de espacio libre en esa página. Existe un disparador no operativo, por lo que la página se divide y se agrega información de versión:

USE Sandpit;
GO
CREATE TABLE dbo.Example
(
    ID          integer NOT NULL IDENTITY(1,1),
    Value       integer NOT NULL,
    Padding1    char(42) NULL,
    Padding2    varchar(8000) NULL,

    CONSTRAINT PK_Example_ID
    PRIMARY KEY CLUSTERED (ID)
);
GO
WITH
    N1 AS (SELECT 1 AS n UNION ALL SELECT 1),
    N2 AS (SELECT L.n FROM N1 AS L CROSS JOIN N1 AS R),
    N3 AS (SELECT L.n FROM N2 AS L CROSS JOIN N2 AS R),
    N4 AS (SELECT L.n FROM N3 AS L CROSS JOIN N3 AS R)
INSERT TOP (137) dbo.Example
    (Value)
SELECT
    ROW_NUMBER() OVER (ORDER BY (SELECT 0))
FROM N4;
GO
ALTER INDEX PK_Example_ID 
ON dbo.Example 
REBUILD WITH (FILLFACTOR = 100);
GO
SELECT
    ddips.index_type_desc,
    ddips.alloc_unit_type_desc,
    ddips.index_level,
    ddips.page_count,
    ddips.record_count,
    ddips.max_record_size_in_bytes
FROM sys.dm_db_index_physical_stats(DB_ID(), OBJECT_ID(N'dbo.Example', N'U'), 1, 1, 'DETAILED') AS ddips
WHERE
    ddips.index_level = 0;
GO
CREATE TRIGGER ExampleTrigger
ON dbo.Example
AFTER DELETE, UPDATE
AS RETURN;
GO
UPDATE dbo.Example
SET Value = -Value
WHERE ID = 1;
GO
SELECT
    ddips.index_type_desc,
    ddips.alloc_unit_type_desc,
    ddips.index_level,
    ddips.page_count,
    ddips.record_count,
    ddips.max_record_size_in_bytes
FROM sys.dm_db_index_physical_stats(DB_ID(), OBJECT_ID(N'dbo.Example', N'U'), 1, 1, 'DETAILED') AS ddips
WHERE
    ddips.index_level = 0;
GO
DROP TABLE dbo.Example;

El script produce el resultado que se muestra a continuación. La tabla de una sola página se divide en dos páginas, y la longitud física máxima de la fila ha aumentado de 57 a 71 bytes (= +14 bytes para la información de versión de la fila).

Ejemplo de actualización

DBCC PAGEmuestra que la fila actualizada única tiene Record Attributes = NULL_BITMAP VERSIONING_INFO Record Size = 71, mientras que todas las demás filas de la tabla tienen Record Attributes = NULL_BITMAP; record Size = 57.

El mismo script, con el UPDATEreemplazado por una sola fila DELETEproduce el resultado que se muestra:

DELETE dbo.Example
WHERE ID = 1;

Eliminar ejemplo

Hay una fila menos en total (¡por supuesto!), Pero el tamaño máximo de fila física no ha aumentado. La información de versiones de fila solo se agrega a las filas necesarias para las pseudo-tablas de activación, y esa fila finalmente se eliminó. Sin embargo, la división de la página permanece. Esta actividad de división de páginas es responsable del lento rendimiento observado cuando el desencadenante estaba presente. Si la definición de la Padding2columna se cambia de varchar(8000)a varchar(7999), la página ya no se divide.

Consulte también esta publicación de blog del MVP de SQL Server, Dmitri Korotkevitch, que también analiza el impacto en la fragmentación.

Paul White reinstala a Monica
fuente
1
Ah, hice una pregunta sobre esto en SO hace algún tiempo y nunca obtuve una respuesta definitiva.
Martin Smith
5

Bueno, aquí está la respuesta oficial de Microsoft ... que creo que es un gran defecto de diseño.

14/11/2011 - La respuesta oficial ha cambiado. No están utilizando el registro de transacciones como se indicó anteriormente. Están utilizando el almacén interno (nivel de fila) para copiar los datos modificados. Todavía no pueden determinar por qué lleva tanto tiempo.

Decidimos utilizar desencadenantes en lugar de en lugar de desencadenantes después de eliminar.

La parte DESPUÉS del desencadenante hace que tengamos que leer el registro de transacciones después de que se completen las eliminaciones y compile la tabla insertada / eliminada del desencadenante. Aquí es donde pasamos la gran cantidad de tiempo y es por diseño para la parte DESPUÉS del disparador. INSTEAD OF trigger evitaría este comportamiento de escanear el registro de transacciones y construir una tabla insertada / eliminada. Además, como se observó, las cosas son mucho más rápidas si eliminamos todas las columnas con nvarchar (max), lo que tiene sentido debido al hecho de que se considera datos LOB. Consulte el siguiente artículo para obtener más información sobre los datos en fila:

http://msdn.microsoft.com/en-us/library/ms189087.aspx

Resumen: DESPUÉS de que el disparador requiera escanear nuevamente el registro de transacciones después de que finalice la eliminación, entonces tenemos que construir e insertar / eliminar la tabla que requiere un mayor uso del registro de transacciones y el tiempo.

Entonces, como plan de acción, esto es lo que sugerimos en este momento:

A) Limit the number of rows deleted in each transaction or
B) Increase timeout settings or
C) Don't use AFTER trigger or trigger at all or
D) Limit usage of nvarchar(max) datatypes.
tsells
fuente
2

Según el plan, todo va correctamente. Puede intentar escribir la eliminación como una UNIÓN en lugar de una IN que le dará un plan diferente.

DELETE m
FROM MAIN m
JOIN Secondary s ON m.ID = s.ValueInt1
AND s.SetTMGUID = '9DDD2C8DD3864EA7B78DA22B2ED572D7'

Sin embargo, no estoy seguro de cuánto ayudará. Cuando la eliminación se ejecuta con los desencadenantes en la tabla, ¿cuál es el tipo de espera para la sesión que realiza la eliminación?

mrdenny
fuente