Медленное удаление записей при включенном триггере

17

Думал, что это было решено с помощью ссылки ниже - обходной путь работает - но патч нет. Работа с поддержкой Microsoft, чтобы решить.

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

Итак, у меня есть проблема, которую я хотел выбросить в StackOverflow, чтобы узнать, есть ли у кого-то идея.

Обратите внимание, что это с SQL Server 2008 R2

Проблема: удаление 3000 записей из таблицы с 15000 записями занимает 3-4 минуты, когда триггер включен, и только 3-5 секунд, когда триггер отключен.

Настройка таблицы

Две таблицы мы назовем Главной и Вторичной. Secondary содержит записи элементов, которые я хочу удалить, поэтому при выполнении удаления я присоединяюсь к таблице Secondary. Процесс выполняется до оператора delete, чтобы заполнить вторичную таблицу записями, которые необходимо удалить.

Удалить заявление:

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

Эта таблица имеет много столбцов и около 14 различных индексов NC. Я попробовал кучу разных вещей, прежде чем решил, что проблема в триггере.

  • Включите блокировку страницы (мы отключили по умолчанию)
  • Собранная статистика вручную
  • Отключен автоматический сбор статистики
  • Проверенный индекс здоровья и фрагментации
  • Удалил кластерный индекс из таблицы
  • Изучил план выполнения (ничего не показывалось как отсутствующие индексы, и стоимость составила 70 процентов к фактическому удалению с приблизительно 28 процентами для объединения / слияния записей

Триггеры

Таблица имеет 3 триггера (по одному для операций вставки, обновления и удаления). Я изменил код для триггера удаления, чтобы он просто возвращался, а затем выбирал один, чтобы увидеть, сколько раз он срабатывает. Он срабатывает только один раз в течение всей операции (как и ожидалось).

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>
tsells
источник

Ответы:

12

Платформа управления версиями строк, представленная в SQL Server 2005, используется для поддержки ряда функций, включая новые уровни изоляции транзакций READ_COMMITTED_SNAPSHOTи SNAPSHOT. Даже тогда , когда ни один из этих уровней изоляций включены, строка-версии все еще используется для AFTERтриггеров (для облегчения генерации insertedи deletedпсевдо-таблиц), МАРС, и (в отдельном хранилище версий) онлайн индексации.

Как задокументировано , механизм может добавить 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 Дмитрия Короткевича, в котором также обсуждается влияние на фрагментацию.

Пол Уайт восстановил Монику
источник
1
Ах, я задал вопрос об этом на ТАК некоторое время назад и никогда не получал окончательного ответа.
Мартин Смит
5

Ну, вот официальный ответ от Microsoft ... который я считаю основным недостатком дизайна.

14.11.2011 - Официальный ответ изменился. Они не используют журнал транзакций, как указано ранее. Они используют внутреннее хранилище (уровень строки) для копирования измененных данных. Они до сих пор не могут определить, почему это так долго.

Мы решили использовать вместо триггеров вместо триггеров после удаления.

Часть ПОСЛЕ триггера заставляет нас прочитать журнал транзакций после завершения удаления и построить таблицу вставленных / удаленных триггеров. Именно здесь мы проводим огромное количество времени и спроектированы для ПОСЛЕ части триггера. Триггер INSTEAD OF предотвратит такое поведение при сканировании журнала транзакций и создании вставленной / удаленной таблицы. Кроме того, как было отмечено, все происходит намного быстрее, если мы отбрасываем все столбцы с помощью nvarchar (max), что имеет смысл в связи с тем, что это считается данными больших объектов. Пожалуйста, ознакомьтесь с приведенной ниже статьей для получения дополнительной информации о данных In-Row:

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.
tsells
источник
2

По плану все идет правильно. Вы можете попытаться записать удаление как JOIN вместо IN, что даст вам другой план.

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

Однако я не уверен, насколько это поможет. Когда удаление выполняется с помощью триггеров в таблице, каков тип ожидания сеанса, выполняющего удаление?

mrdenny
источник