Eliminación lenta de registros cuando se habilita un disparador
-
16-10-2019 - |
Pregunta
Pensé que esto se resolvió con el siguiente enlace, el trabajo en torno a funciona, pero el parche no. Trabajando con 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: Eliminar 3000 registros de una tabla con 15000 registros lleva 3-4 minutos cuando un disparador está habilitado y solo 3-5 segundos cuando el disparador está deshabilitado.
Configuración de mesa
Dos tablas llamaremos principal y secundaria. Secundario contiene registros de elementos que quiero eliminar, así que cuando realice el eliminación, me uniré a la tabla secundaria. Un proceso se ejecuta antes de la instrucción Eliminar para llenar la tabla secundaria con los registros que se eliminarán.
Declaración de eliminación:
DELETE FROM MAIN
WHERE ID IN (
SELECT Secondary.ValueInt1
FROM Secondary
WHERE SECONDARY.GUID = '9FFD2C8DD3864EA7B78DA22B2ED572D7'
);
Esta tabla tiene muchas columnas y aproximadamente 14 índices de NC diferentes. Probé un montón de cosas diferentes antes de determinar que el desencadenante era el problema.
- Encienda el bloqueo de la página (hemos apagado de forma predeterminada)
- Estadísticas reunidas manualmente
- Recopilación de autos discapacitados de estadísticas
- Salud y fragmentación del índice verificado
- Dejó caer el índice agrupado de la tabla
- Examinó el plan de ejecución (nada que muestra los índices faltantes y el costo fue del 70 por ciento hacia la eliminación real con aproximadamente el 28 por ciento para la unión / fusión de los registros
Desencadenantes
La tabla tiene 3 desencadenantes (uno para insertar, actualizar y eliminar operaciones). Modifiqué el código para el disparador de eliminación para que solo regrese, luego seleccione uno para ver cuántas veces se dispara. Solo 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 disparador encendido - La declaración tarda 3-4 minutos en completarse
- Con disparador apagado - la declaración tarda 3-5 segundos en completarse
¿Alguien tiene alguna idea de por qué?
Tenga en cuenta también: no buscar cambiar esta arquitectura, agregar los índices de eliminación, etc. como solución. Esta tabla es la pieza central para algunas operaciones de datos importantes y tuvimos que ajustarla y ajustarla (índices, bloqueo de páginas, etc.) para permitir que las operaciones de concurrencia importantes funcionen sin punto muerto.
Aquí está el plan de ejecución XML (los nombres se cambiaron para proteger al inocente)
<?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>
Solución 2
Bueno, aquí está la respuesta oficial de Microsoft ... que creo que es un defecto de diseño importante.
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 cambiados. Todavía no pueden determinar por qué se ha llevado tanto tiempo.
Decidimos usar en lugar de desencadenantes en lugar de después de eliminar los desencadenantes.
La parte posterior del desencadenante hace que tengamos que leer el registro de transacciones después de que las eliminaciones se completen y construyan la tabla insertada/eliminada del activador. Aquí es donde pasamos la gran cantidad de tiempo y es por diseño para la parte posterior del gatillo. En lugar de disparar, evitaría este comportamiento de escanear el registro de transacciones y construir una tabla insertada/eliminada. Además, como se observó que 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. Tenga a Alook en el artículo a continuación para obtener más información sobre datos en la fila:
http://msdn.microsoft.com/en-us/library/ms189087.aspx
Resumen: después del disparador requiere escanear a través del registro de transacciones después de los acabados de eliminación, entonces tenemos que construir e insertar/eliminar tabla que requiere más 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.
Otros consejos
El marco de versión 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_SNAPSHOT
y SNAPSHOT
. Incluso cuando ninguno de estos niveles de aislamiento está habilitado, la versión de fila todavía se usa para AFTER
desencadenantes (para facilitar la generación de la inserted
y deleted
Pseudo-mesas), Marte y (en una tienda de versiones separada) indexación en línea.
Como documentado, el motor puede agregar un postfix de 14 bytes a cada fila de una tabla que esté versión para cualquiera de estos fines. Este comportamiento es relativamente conocido, al igual que la adición de los datos de 14 bytes a cada fila de un índice que es reconstruido en línea con un nivel de aislamiento de inversión de filas habilitado. Incluso donde los niveles de aislamiento no están habilitados, se agrega un byte adicional a Índices no agrupados solamente Cuando se reconstruye ONLINE
.
Cuando está presente un desencadenante posterior, y los versiones agregarían 14 bytes por fila, existe una optimización dentro del motor a evitar esto, pero donde un ROW_OVERFLOW
o LOB
La asignación no puede ocurrir. En la práctica, esto significa que el tamaño máximo posible de una fila debe ser inferior a 8060 bytes. En calcular máximo Posibles tamaños de fila, 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 UPDATE
disparador, aunque el mismo principio se aplica a AFTER DELETE
. El siguiente script crea una tabla con una longitud máxima en la 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 de no opción, por lo que la página está dividida y se agregó información de versiones:
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 la salida que se muestra a continuación. La tabla de una sola página se divide en dos páginas y el máximo físico La longitud de la fila ha aumentado de 57 a 71 bytes (= +14 bytes para la información de versión de fila).
DBCC PAGE
muestra que la fila actualizada única tiene Record Attributes = NULL_BITMAP VERSIONING_INFO Record Size = 71
, mientras que todas las demás filas en la mesa tienen Record Attributes = NULL_BITMAP; record Size = 57
.
El mismo guión, con el UPDATE
reemplazado por una sola fila DELETE
produce la salida que se muestra:
DELETE dbo.Example
WHERE ID = 1;
Hay una fila menos en total (¡por supuesto!), Pero el tamaño máximo de la 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ó. La división de la página permanece, sin embargo. Esta actividad de división de página es responsable del rendimiento lento observado cuando el disparador estaba presente. Si la definición del Padding2
La columna se cambia de varchar(8000)
a varchar(7999)
, la página ya no se divide.
También vea esto entrada en el blog por SQL Server MVP Dmitri Korotkevitch, que también analiza el impacto en la fragmentación.
Según el plan, todo va correctamente. Puede intentar escribir el eliminación como un unión en lugar de un en el 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 el eliminación se ejecuta con los desencadenantes en la mesa, ¿cuál es el tipo de espera para la sesión que realiza la eliminación?