Повільне видалення записів, коли активовано тригер


17

Думав, що це було вирішено за посиланням нижче - робота навколо роботи - але патч не робить. Робота з підтримкою Microsoft для вирішення.

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

Гаразд, у мене є проблема, яку я хотів викинути до StackOverflow, щоб побачити, чи є хтось ідея.

Зауважте, що це стосується SQL Server 2008 R2

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

Налаштування таблиці

Дві таблиці ми будемо називати Основними та Вторинними. Вторинний містить записи елементів, які я хочу видалити, тому при виконанні видалення я приєднуюся до таблиці вторинних. Процес запускається до оператора видалення для заповнення вторинної таблиці записами, які потрібно видалити.

Видалити заяву:

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>

Відповіді:


12

Рамка версійних версій, представлена ​​в SQL Server 2005, використовується для підтримки низки функцій, включаючи нові рівні ізоляції транзакцій READ_COMMITTED_SNAPSHOTта SNAPSHOT. Навіть коли жоден з цих рівнів ізоляції не ввімкнено, версія версій все ще використовується для AFTERтригерів (для полегшення генерації insertedта deletedпсевдо таблиць), MARS та (в окремому магазині версій) інтернет-індексації.

Як задокументовано , двигун може додати 14-байтовий постфікс до кожного рядка таблиці, який розроблений для будь-якої з цих цілей. Така поведінка відносно добре відома, як і додавання 14-байтових даних до кожного ряду індексу, який перебудовується в Інтернеті з увімкненим рівнем ізоляції версійних версій. Навіть там, де рівні ізоляції не ввімкнено, додатковий байт додається до некластеризованих індексів лише під час перебудови ONLINE.

Якщо присутній тригер ПІСЛЯ, а версія в іншому випадку додала б 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), сторінка більше не розбивається.

Також дивіться цю публікацію в блозі MVP SQL Server Дмитра Короткевича, де також обговорюється вплив на фрагментацію.


1
Ах, я запитав це питання на SO колись тому і ніколи не отримав остаточної відповіді.
Мартін Сміт

5

Ну ось офіційна відповідь від Microsoft ... що, на мою думку, є головним недоліком дизайну.

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

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

ПІСЛЯ частини тригера змушує нас читати журнал транзакцій після завершення видалення та складання тригера, вставленого / видаленого таблиці. Тут ми витрачаємо величезну кількість часу і розробляємо ПІСЛЯ частину спускового гачка. Тригер INSTEAD OF запобігає подібній поведінці сканування журналу транзакцій та побудови вставленої / видаленої таблиці. Крім того, як було помічено, що все відбувається набагато швидше, якщо ми скинемо всі стовпці з nvarchar (max), що має сенс через те, що він вважається даними LOB. Будь ласка, перегляньте статтю нижче для отримання додаткової інформації щодо 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.

2

За планом все йде правильно. Ви можете спробувати написати видалення як ПРИЄДНАЙТЕ замість ІН, що дасть вам інший план.

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

Я не впевнений, наскільки це допоможе. Коли видалення виконується за допомогою тригерів на таблиці, який тип очікування сеансу виконує видалення?

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.