Производительность T-SQL MERGE в типичном контексте публикации - PullRequest
1 голос
/ 13 сентября 2011

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

Теперь, когда мы обновились до 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%) <- (...) </p>

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

UPDATE

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

1 Ответ

2 голосов
/ 15 сентября 2011

Затраты на поддерево должны приниматься с большим количеством соли (особенно если у вас есть огромные ошибки в количестве элементов).Выход 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

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

Original Plan

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

;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;
...