Медленное удаление записей при включении триггера
-
16-10-2019 - |
Вопрос
Я думал, что это было решено с помощью ссылки ниже - обходной путь работает - но патч - нет.Работаем со службой поддержки Microsoft для решения.
http://support.microsoft.com/kb/2606883
Итак, у меня есть проблема, которую я хотел бы сообщить в StackOverflow, чтобы узнать, есть ли у кого-нибудь идеи.
Обратите внимание, что это касается SQL Server 2008 R2.
Проблема:Удаление 3000 записей из таблицы с 15000 записями занимает 3-4 минуты при включенном триггере и всего 3-5 секунд при отключенном триггере.
Настройка стола
Две таблицы мы назовем Main и Secondary.Вторичная таблица содержит записи элементов, которые я хочу удалить, поэтому при выполнении удаления я присоединяюсь к вторичной таблице.Перед оператором удаления запускается процесс заполнения вторичной таблицы записями, которые необходимо удалить.
Удалить заявление:
DELETE FROM MAIN
WHERE ID IN (
SELECT Secondary.ValueInt1
FROM Secondary
WHERE SECONDARY.GUID = '9FFD2C8DD3864EA7B78DA22B2ED572D7'
);
В этой таблице много столбцов и около 14 различных индексов NC.Я перепробовал кучу разных вещей, прежде чем понял, что проблема в триггере.
- Включите блокировку страниц (у нас отключена по умолчанию)
- Собранная статистика вручную
- Отключен автосбор статистики
- Подтвержденное состояние и фрагментация индекса
- Удален кластерный индекс из таблицы
- Изучил план выполнения (ничего не отображалось как отсутствующие индексы, а затраты составили 70 процентов на фактическое удаление и около 28 процентов на объединение/объединение записей).
Триггеры
Таблица имеет три триггера (по одному для операций вставки, обновления и удаления).Я изменил код триггера удаления, чтобы он просто возвращался, а затем выбирал его и смотрел, сколько раз он срабатывает.Он срабатывает только один раз за всю операцию (как и ожидалось).
ALTER TRIGGER [dbo].[TR_MAIN_RD] ON [dbo].[MAIN]
AFTER DELETE
AS
SELECT 1
RETURN
Подводя итоги
- При включенном триггере выполнение запроса занимает 3–4 минуты.
- При выключенном триггере выполнение запроса занимает 3–5 секунд.
У кого-нибудь есть идеи, почему?
Также обратите внимание: не собираюсь менять эту архитектуру, добавлять индексы удаления и т. д.как решение.Эта таблица является центральной частью некоторых основных операций с данными, и нам пришлось настроить ее (индексы, блокировку страниц и т. д.), чтобы основные параллельные операции могли работать без взаимоблокировок.
Вот план казни в формате xml (имена изменены в целях защиты невиновных)
<?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>
Решение 2
Ну, вот официальный ответ от Microsoft ... который, я думаю, является основным недостатком дизайна.
14.11.2011 - Официальный ответ изменился. Они не используют журнал транзакций, как указано ранее. Используют внутренний магазин (уровень строки) для копирования измененных данных. Они до сих пор не могут определить, почему это заняло так долго.
Мы решили использовать вместо триггеров вместо после удаления триггеров.
После того, как часть триггера заставляет нас прочитать через журнал транзакций после завершения удалений и создать вставленную/удаленную таблицу триггера. Именно здесь мы проводим огромное количество времени и занимаемся замыслом для последующей части триггера. Вместо триггера предотвратит такое поведение сканирования журнала транзакций и создания вставленной/удаленной таблицы. Кроме того, поскольку было замечено, что вещи намного быстрее, если мы бросаем все столбцы с помощью NVARCHAR (MAX), что имеет смысл из -за того, что они считаются данными LOB. Пожалуйста, попросите Alook в статье ниже для получения дополнительной информации относительно данных в строке:
http://msdn.microsoft.com/en-us/library/ms189087.aspx
Резюме: после триггера требуется сканирование через журнал транзакций после окончания удаления, затем мы должны создать и вставить/удаленную таблицу, которая требует большего использования журнала транзакций и времени.
Таким образом, в качестве плана действий, это то, что мы предлагаем в настоящее время:
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.
Другие советы
Структура управления версиями строк, представленная в SQL Server 2005, используется для поддержки ряда функций, включая новые уровни изоляции транзакций. READ_COMMITTED_SNAPSHOT
и SNAPSHOT
.Даже если ни один из этих уровней изоляции не включен, управление версиями строк по-прежнему используется для AFTER
триггеры (чтобы облегчить генерацию inserted
и deleted
псевдотаблицы), MARS и (в отдельном хранилище версий) онлайн-индексирование.
Как задокументировано, механизм может добавить 14-байтовый постфикс к каждой строке таблицы, версии которой предназначены для любой из этих целей.Такое поведение относительно хорошо известно, как и добавление 14-байтовых данных в каждую строку индекса. восстановлен онлайн с включенным уровнем изоляции управления версиями строк.Даже если уровни изоляции не включены, к только некластеризованные индексы когда перестроен ONLINE
.
Если присутствует триггер AFTER и в противном случае управление версиями привело бы к добавлению 14 байтов на строку, в движке существует оптимизация для избегать это, но где ROW_OVERFLOW
или LOB
распределение не может произойти.На практике это означает, что максимально возможный размер строки должен быть меньше 8060 байт.При расчете максимум возможные размеры строк, движок предполагает, например, что столбец VARCHAR(460) может содержать 460 символов.
Поведение легче всего увидеть с помощью AFTER UPDATE
триггер, хотя тот же принцип применим и к AFTER DELETE
.Следующий скрипт создает таблицу с максимальной длиной строки 8060 байт.Данные умещаются на одной странице, на которой имеется 13 байт свободного места.Существует неактивный триггер, поэтому страница разделяется и добавляется информация о версии:
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;
Скрипт выдает результат, показанный ниже.Одностраничная таблица разбита на две страницы, и максимальная физический длина строки увеличилась с 57 до 71 байта (= +14 байт для информации о версии строки).
DBCC PAGE
показывает, что единственная обновленная строка имеет Record Attributes = NULL_BITMAP VERSIONING_INFO Record Size = 71
, тогда как все остальные строки таблицы имеют Record Attributes = NULL_BITMAP; record Size = 57
.
Тот же сценарий с UPDATE
заменено одной строкой DELETE
производит показанный результат:
DELETE dbo.Example
WHERE ID = 1;
Всего на одну строку меньше (конечно!), но максимальный физический размер строки не увеличился.Информация о версии строк добавляется только к строкам, необходимым для псевдотаблиц триггера, и в конечном итоге эта строка была удалена.Однако разделение страниц остается.Это действие разделения страниц отвечает за низкую производительность, наблюдаемую при наличии триггера.Если определение Padding2
столбец изменен с varchar(8000)
к varchar(7999)
, страница больше не разбивается.
Также посмотрите это Сообщение блога автор SQL Server MVP Дмитрий Короткевич, в котором также обсуждается влияние на фрагментацию.
Согласно плану, все идет правильно. Вы можете попробовать написать удаление как соединение вместо того, что даст вам другой план.
DELETE m
FROM MAIN m
JOIN Secondary s ON m.ID = s.ValueInt1
AND s.SetTMGUID = '9DDD2C8DD3864EA7B78DA22B2ED572D7'
Я не уверен, сколько это поможет. Когда удаление работает с триггерами на таблице, какой тип ожидания для сеанса делает удаление?