Производительность T-SQL MERGE в типичном контексте публикации

У меня есть ситуация, когда приложение «издатель» по существу поддерживает модель представления в актуальном состоянии, запрашивая ОЧЕНЬ сложное представление, а затем объединяя результаты в таблицу денормализованной модели представления, используя отдельные операции вставки, обновления и удаления.

Теперь, когда мы обновились до SQL 2008, я решил, что самое время обновить их с помощью инструкции SQL MERGE. Однако после написания запроса стоимость поддерева оператора MERGE составляет 1214,54! По-старому сумма операций Вставить/Обновить/Удалить составляла всего 0,104!!

Я не могу понять, как более простой способ описания той же самой операции может быть настолько дерьмовым. Возможно, вы видите ошибочность моего пути там, где я не вижу.

Немного статистики по таблице: в ней 1,9 миллиона строк, и каждая операция MERGE вставляет, обновляет или удаляет более 100 из них. В моем тестовом примере затронут только 1.

-- This table variable has the EXACT same structure as the published table
-- Yes, I've tried a temp table instead of a table variable, and it makes no difference
declare @tSource table
(
    Key1 uniqueidentifier NOT NULL,
    Key2 int NOT NULL,
    Data1 datetime NOT NULL,
    Data2 datetime,
    Data3 varchar(255) NOT NULL, 
    PRIMARY KEY 
    (
        Key1, 
        Key2
    )
)

-- Fill the temp table with the desired current state of the view model, for
-- only those rows affected by @Key1.  I'm not really concerned about the
-- performance of this.  The result of this; it's already good.  This results
-- in very few rows in the table var, in fact, only 1 in my test case
insert into @tSource
select *
from vw_Source_View with (nolock)
where Key1 = @Key1

-- Now it's time to merge @tSource into TargetTable

;MERGE TargetTable as T
USING tSource S
    on S.Key1 = T.Key1 and S.Key2 = T.Key2

-- Only update if the Data columns do not match
WHEN MATCHED AND T.Data1 <> S.Data1 OR T.Data2 <> S.Data2 OR T.Data3 <> S.Data3 THEN
    UPDATE SET
        T.Data1 = S.Data1,
        T.Data2 = S.Data2,
        T.Data3 = S.Data3

-- Insert when missing in the target
WHEN NOT MATCHED BY TARGET THEN
    INSERT (Key1, Key2, Data1, Data2, Data3)
    VALUES (Key1, Key2, Data1, Data2, Data3)

-- Delete when missing in the source, being careful not to delete the REST
-- of the table by applying the T.Key1 = @id condition
WHEN NOT MATCHED BY SOURCE AND T.Key1 = @id THEN
    DELETE
;

Итак, как это достигает стоимости 1200 поддеревьев? Доступ к данным из самих таблиц кажется достаточно эффективным. На самом деле, 87% стоимости MERGE приходится на операцию сортировки ближе к концу цепочки:

MERGE (0%) ‹- Обновление индекса (12%) ‹- Сортировка (87%) ‹- (...)

И этот вид имеет 0 строк, входящих и исходящих из него. Почему для сортировки 0 строк требуется 87% ресурсов?

ОБНОВЛЕНИЕ

Я разместил фактический (не оценочный) план выполнения только для операции MERGE в Gist.


person David Boike    schedule 13.09.2011    source источник
comment
Можете ли вы опубликовать фактические планы выполнения? Я предполагаю, что это из фактического, а не предполагаемого плана.   -  person JNK    schedule 13.09.2011
comment
Я добавил план выполнения для MERGE в суть. Очевидно, я изменил все имена баз данных, чтобы повысить удовлетворенность работодателей, и я попытался внести те же изменения в план XML. Надеюсь, я ничего не напутал! Во всяком случае, он все еще загружается в SQL Management Studio.   -  person David Boike    schedule 14.09.2011
comment
Это не занимает 87% ресурсов. Затраты, указанные в фактическом плане, по-прежнему являются сметными затратами. Это не приводит их в соответствие с тем, что произошло на самом деле. По какой-то причине он думает, что в итоге отсортирует 3 348 560 строк. Фильтр на [Action1007] IS NOT NULL уменьшает фактическое количество строк до 0, после чего все расчеты не выполняются.   -  person Martin Smith    schedule 14.09.2011
comment
Мартин опередил меня. У вас проблемы со статистикой, я думаю.   -  person JNK    schedule 14.09.2011
comment
@ Мартин, мне показалось, что я видел в пятницу, что вы опубликовали возможное решение, которое я собирался попробовать сегодня утром, но теперь его нет ??   -  person David Boike    schedule 19.09.2011
comment
@David - я понял, что это не сработало, и я не мог его исправить (хотя там были некоторые вещи, которые можно было спасти, и это можно было исправить). Подожди, просто посмотрю.   -  person Martin Smith    schedule 19.09.2011
comment
@David - Восстановлено и (возможно) исправлено!   -  person Martin Smith    schedule 19.09.2011


Ответы (1)


Стоимость поддерева следует воспринимать с большой долей скептицизма (особенно, когда у вас есть огромные ошибки кардинальности). Выход SET STATISTICS IO ON; SET STATISTICS TIME ON; является лучшим индикатором фактической производительности.

Сортировка по нулевой строке не занимает 87% ресурсов. Эта задача в вашем плане связана с оценкой статистики. Затраты, указанные в фактическом плане, по-прежнему являются сметными затратами. Это не приводит их в соответствие с тем, что произошло на самом деле.

В плане есть точка, где фильтр уменьшает 1 911 721 строку до 0, но предполагаемое количество строк в будущем составляет 1 860 310 строк. После этого все затраты являются фиктивными, кульминацией которых является 87%-ная стоимость, оцениваемая в 3 348 560 строк.

Ошибку оценки количества элементов можно воспроизвести вне оператора Merge, взглянув на оценочный план для Full Outer Join с эквивалентными предикатами (дает ту же оценку 1 860 310 строк).

SELECT * 
FROM TargetTable T
FULL OUTER JOIN  @tSource S
    ON S.Key1 = T.Key1 and S.Key2 = T.Key2
WHERE 
CASE WHEN S.Key1 IS NOT NULL 
     /*Matched by Source*/
     THEN CASE WHEN T.Key1 IS NOT NULL  
               /*Matched by Target*/
               THEN CASE WHEN  [T].[Data1]<>S.[Data1] OR 
                               [T].[Data2]<>S.[Data2] OR 
                               [T].[Data3]<>S.[Data3]
                         THEN (1) 
                     END 
                /*Not Matched by Target*/     
                ELSE (4) 
           END 
       /*Not Matched by Source*/     
      ELSE CASE WHEN  [T].[Key1]=@id 
                THEN (3) 
            END 
END IS NOT NULL

Тем не менее, план до самого фильтра выглядит довольно неоптимальным. Он выполняет полное сканирование кластеризованного индекса, когда, возможно, вам нужен план с двумя поисками диапазона кластеризованного индекса. Один для извлечения одной строки, соответствующей первичному ключу, из соединения в источнике, а другой для извлечения диапазона T.Key1 = @id (хотя, может быть, это сделано для того, чтобы избежать необходимости сортировки в кластерном порядке ключей позже?)

Исходный план

Возможно, вы могли бы попробовать это переписать и посмотреть, работает ли он лучше или хуже.

;WITH FilteredTarget AS
(
SELECT T.*
FROM TargetTable  AS T WITH (FORCESEEK)
JOIN @tSource S
    ON (T.Key1 = S.Key1
    AND S.Key2 = T.Key2)
    OR T.Key1 = @id
)
MERGE FilteredTarget AS T
USING @tSource S
ON (T.Key1 = S.Key1
   AND S.Key2 = T.Key2)


-- Only update if the Data columns do not match
WHEN MATCHED AND S.Key1 = T.Key1 AND S.Key2 = T.Key2 AND 
                                         (T.Data1 <> S.Data1 OR
                                          T.Data2 <> S.Data2 OR 
                                          T.Data3 <> S.Data3) THEN
  UPDATE SET T.Data1 = S.Data1,
             T.Data2 = S.Data2,
             T.Data3 = S.Data3

-- Note from original poster: This extra "safety clause" turned out not to
-- affect the behavior or the execution plan, so I removed it and it works
-- just as well without, but if you find yourself in a similar situation
-- you might want to give it a try.
-- WHEN MATCHED AND (S.Key1 <> T.Key1 OR S.Key2 <> T.Key2) AND T.Key1 = @id THEN
--   DELETE

-- Insert when missing in the target
WHEN NOT MATCHED BY TARGET THEN
    INSERT (Key1, Key2, Data1, Data2, Data3)
    VALUES (Key1, Key2, Data1, Data2, Data3)

WHEN NOT MATCHED BY SOURCE AND T.Key1 = @id THEN
    DELETE;
person Martin Smith    schedule 15.09.2011
comment
Это сработало отлично! План выполнения 0,0638 в операторе слияния, и это подтверждается тем фактом, что время выполнения теперь составляет в среднем около 1,4 мс по сравнению с более чем секундой ранее. Кроме того, оказывается, что дополнительное предложение WHEN MATCHED-DELETE не влияет на поведение или план выполнения, поэтому я удалил его и отредактировал ваш ответ здесь, чтобы показать это. Большое спасибо! - person David Boike; 19.09.2011