Как оптимизировать запрос T-SQL с использованием плана выполнения

15

У меня есть SQL-запрос, который я провел последние два дня, пытаясь оптимизировать, используя метод проб и ошибок и план выполнения, но безрезультатно. Пожалуйста, прости меня за это, но я опубликую весь план выполнения здесь. Я приложил усилия, чтобы сделать имена таблиц и столбцов в запросе и плане выполнения общими для краткости и для защиты IP моей компании. План выполнения можно открыть с помощью SQL Sentry Plan Explorer .

Я сделал довольно много T-SQL, но использование планов выполнения для оптимизации моего запроса - это новая область для меня, и я действительно пытался понять, как это сделать. Поэтому, если бы кто-нибудь мог помочь мне с этим и объяснить, как этот план выполнения может быть расшифрован, чтобы найти в запросе способы его оптимизации, я был бы вечно благодарен. У меня есть еще много запросов для оптимизации - мне просто нужен трамплин, чтобы помочь мне с этим первым.

Это запрос:

DECLARE @Param0 DATETIME     = '2013-07-29';
DECLARE @Param1 INT          = CONVERT(INT, CONVERT(VARCHAR, @Param0, 112))
DECLARE @Param2 VARCHAR(50)  = 'ABC';
DECLARE @Param3 VARCHAR(100) = 'DEF';
DECLARE @Param4 VARCHAR(50)  = 'XYZ';
DECLARE @Param5 VARCHAR(100) = NULL;
DECLARE @Param6 VARCHAR(50)  = 'Text3';

SET NOCOUNT ON

DECLARE @MyTableVar TABLE
(
    B_Var1_PK int,
    Job_Var1 varchar(512),
    Job_Var2 varchar(50)
)

INSERT INTO @MyTableVar (B_Var1_PK, Job_Var1, Job_Var2) 
SELECT B_Var1_PK, Job_Var1, Job_Var2 FROM [fn_GetJobs] (@Param1, @Param2, @Param3, @Param4, @Param6);

CREATE TABLE #TempTable
(
    TTVar1_PK INT PRIMARY KEY,
    TTVar2_LK VARCHAR(100),
    TTVar3_LK VARCHAR(50),
    TTVar4_LK INT,
    TTVar5 VARCHAR(20)
);

INSERT INTO #TempTable
SELECT DISTINCT
    T.T1_PK,
    T.T1_Var1_LK,
    T.T1_Var2_LK,
    MAX(T.T1_Var3_LK),
    T.T1_Var4_LK
FROM
    MyTable1 T
    INNER JOIN feeds.MyTable2 A ON A.T2_Var1 = T.T1_Var4_LK
    INNER JOIN @MyTableVar B ON B.Job_Var2 = A.T2_Var2 AND B.Job_Var1 = A.T2_Var3
GROUP BY T.T1_PK, T.T1_Var1_LK, T.T1_Var2_LK, T.T1_Var4_LK

-- This is the slow statement...
SELECT 
    CASE E.E_Var1_LK
        WHEN 'Text1' THEN T.TTVar2_LK + '_' + F.F_Var1
        WHEN 'Text2' THEN T.TTVar2_LK + '_' + F.F_Var2
        WHEN 'Text3' THEN T.TTVar2_LK
    END,
    T.TTVar4_LK,
    T.TTVar3_LK,
    CASE E.E_Var1_LK
        WHEN 'Text1' THEN F.F_Var1
        WHEN 'Text2' THEN F.F_Var2
        WHEN 'Text3' THEN T.TTVar5
    END,
    A.A_Var3_FK_LK,
    C.C_Var1_PK,
    SUM(CONVERT(DECIMAL(18,4), A.A_Var1) + CONVERT(DECIMAL(18,4), A.A_Var2))
FROM #TempTable T
    INNER JOIN TableA (NOLOCK) A ON A.A_Var4_FK_LK  = T.TTVar1_PK
    INNER JOIN @MyTableVar     B ON B.B_Var1_PK     = A.Job
    INNER JOIN TableC (NOLOCK) C ON C.C_Var2_PK     = A.A_Var5_FK_LK
    INNER JOIN TableD (NOLOCK) D ON D.D_Var1_PK     = A.A_Var6_FK_LK
    INNER JOIN TableE (NOLOCK) E ON E.E_Var1_PK     = A.A_Var7_FK_LK  
    LEFT OUTER JOIN feeds.TableF (NOLOCK) F ON F.F_Var1 = T.TTVar5
WHERE A.A_Var8_FK_LK = @Param1
GROUP BY
    CASE E.E_Var1_LK
        WHEN 'Text1' THEN T.TTVar2_LK + '_' + F.F_Var1
        WHEN 'Text2' THEN T.TTVar2_LK + '_' + F.F_Var2
        WHEN 'Text3' THEN T.TTVar2_LK
    END,
    T.TTVar4_LK,
    T.TTVar3_LK,
    CASE E.E_Var1_LK 
        WHEN 'Text1' THEN F.F_Var1
        WHEN 'Text2' THEN F.F_Var2
        WHEN 'Text3' THEN T.TTVar5
    END,
    A.A_Var3_FK_LK, 
    C.C_Var1_PK


IF OBJECT_ID(N'tempdb..#TempTable') IS NOT NULL
BEGIN
    DROP TABLE #TempTable
END
IF OBJECT_ID(N'tempdb..#TempTable') IS NOT NULL
BEGIN
    DROP TABLE #TempTable
END

Я обнаружил, что третье утверждение (прокомментированное как медленное) - это та часть, которая занимает больше всего времени. Два заявления перед возвращением почти мгновенно.

План выполнения доступен в виде XML по этой ссылке .

Лучше щелкнуть правой кнопкой мыши и сохранить, а затем открыть в SQL Sentry Plan Explorer или в другом программном обеспечении для просмотра, а не в браузере.

Если вам нужна дополнительная информация о таблицах или данных, пожалуйста, не стесняйтесь спрашивать.

Нео
источник
2
Ваша статистика далеко Когда вы в последний раз де-фрагментировали индексы или обновляли статистику? Кроме того, я бы попытался использовать временную таблицу вместо табличной переменной @MyTableVar, поскольку оптимизатор действительно не может использовать статистику для табличных переменных.
Адам Хейнс
Спасибо за ваш ответ, Адам. Изменение @MyTableVar на временную таблицу не имеет никакого эффекта, но это всего лишь небольшое количество строк (что видно из плана выполнения). Что в плане выполнения показывает, что моя статистика далеко? Указывает ли это, какие индексы следует реорганизовать или перестроить, и какие таблицы должны обновлять статистику?
Neo
3
Это хеш-соединение в правом нижнем углу содержит приблизительно 24 000 строк во входных данных сборки, но фактически составляет 3 285 620, поэтому вполне может быть, что это произойдет tempdb. т. е. оценки для строк, полученных в результате соединения TableAи @MyTableVarявляются далеко. Кроме того, число строк, входящих в сортировку, намного больше, чем предполагалось, поэтому они также могут пролиться.
Мартин Смит

Ответы:

22

Прежде чем перейти к основному ответу, необходимо обновить две части программного обеспечения.

Требуемые обновления программного обеспечения

Первый - это SQL Server. Вы используете SQL Server 2008 с пакетом обновления 1 (сборка 2531). Вы должны быть исправлены как минимум до текущего пакета обновления (SQL Server 2008 с пакетом обновления 3 (SP3) - сборка 5500). Самая последняя сборка SQL Server 2008 на момент написания этой статьи - Пакет обновления 3, Накопительное обновление 12 (сборка 5844).

Вторым программным обеспечением является SQL Sentry Plan Explorer . В последних версиях появились новые важные функции и исправления, в том числе возможность напрямую загружать план запросов для экспертного анализа (не нужно никуда вставлять XML!)

Анализ плана запроса

Оценка кардинальности для табличной переменной является абсолютно правильной благодаря перекомпиляции на уровне операторов:

оценка табличной переменной

К сожалению, переменные таблицы не поддерживают статистику распределения, поэтому оптимизатор знает только о шести строках; он ничего не знает о значениях, которые могут быть в этих шести строках. Эта информация имеет решающее значение, учитывая, что следующая операция представляет собой соединение с другой таблицей. Оценка количества элементов этого соединения основана на сомнительном предположении оптимизатора:

оценка первого соединения

С этого момента план, выбранный оптимизатором, основан на неверной информации, поэтому неудивительно, что производительность настолько низкая. В частности, память, выделенная для сортировок и хеш-таблиц для хеш-соединений, будет слишком мала. Во время выполнения переполненные операции сортировки и хеширования будут распространяться на физический диск tempdb.

SQL Server 2008 не выделяет это в планах выполнения; Вы можете отслеживать разливы с помощью расширенных событий или предупреждений сортировки профилировщика и предупреждений о хеше . Память зарезервирована для сортировок и хэшей, основанных на оценках количества элементов перед запуском выполнения, и не может быть увеличена во время выполнения независимо от того, сколько резервной памяти может иметь ваш SQL Server. Поэтому точные оценки числа строк имеют решающее значение для любого плана выполнения, который включает операции, занимающие память рабочей области.

Ваш запрос также параметризован. Следует рассмотреть возможность добавления OPTION (RECOMPILE)к запросу, если различные значения параметров влияют на план запроса. Вероятно, вам все равно стоит рассмотреть возможность его использования, чтобы оптимизатор мог видеть значение @Param1во время компиляции. Если ничего другого, это может помочь оптимизатору вывести более разумную оценку для поиска индекса, показанного выше, учитывая, что таблица очень большая и секционированная. Это также может включить удаление статических разделов.

Попробуйте запрос снова с временной таблицей вместо табличной переменной и OPTION (RECOMPILE) . Вам также следует попытаться материализовать результат первого объединения в другую временную таблицу и выполнить остальную часть запроса для этого. Количество строк не так велико (3 285 620), поэтому это должно быть достаточно быстро. После этого оптимизатор будет иметь точную оценку количества элементов и статистику распределения для результата объединения. Если повезет, остальная часть плана хорошо встанет на свои места.

Исходя из свойств, показанных в плане, материализационный запрос будет:

SELECT
    A.A_Var7_FK_LK,
    A.A_Var4_FK_LK,
    A.A_Var6_FK_LK, 
    A.A_Var5_FK_LK,
    A.A_Var1,
    A.A_Var2,
    A.A_Var3_FK_LK
INTO #AnotherTempTable
FROM @MyTableVar AS B
JOIN TableA AS A
    ON A.Job = B.B_Var1_PK
WHERE
    A_Var8_FK_LK = @Param1;

Вы также можете INSERTдобавить в предопределенную временную таблицу (правильные типы данных не показаны в плане, поэтому я не могу выполнить эту часть). Новая временная таблица может иметь или не иметь преимущества от кластеризованных и некластеризованных индексов.

Пол Уайт восстановил Монику
источник
Большое спасибо за этот подробный ответ. Извините, что на ответ ушла неделя - я работаю над этим каждый день, перемежаясь с другой работой. Я реализовал ваши предложения, материализовав соединение с TableA в #AnotherTempTable. Похоже, это оказало наилучшее влияние - другие предложения (использование временной таблицы вместо табличной переменной для @MyTableVar и использование OPTION (RECOMPILE)не имели большого эффекта или вообще никакого). «Anonymize» и «Post to SQLPerformance.com» опции в SQL Sentry Plan Explorer великолепны - я только что их использовал: answers.sqlperformance.com/questions/1087
Neo
-6

Я заметил, что на @MyTableVar должен быть PK, и согласен с тем, что #MyTableVar часто лучше работает (особенно с большим количеством строк).

Условие в предложении where

   WHERE A.A_Var8_FK_LK = @Param1

должен быть перемещен во внутреннее соединение A AND. По моему опыту оптимизатор не достаточно умен, чтобы сделать это (извините, я не смотрю на план), и это может иметь огромное значение.

Если эти изменения не показывают улучшения, я бы затем создал еще одну временную таблицу из A и всех вещей, к которым он присоединяется к ограниченному (хорошо?) A.A_Var8_FK_LK = @ Param1, если эта группировка имеет логический смысл для вас.

Затем создайте кластеризованный индекс для этой временной таблицы (до или после создания) для следующего условия соединения.

Затем объедините этот результат с несколькими оставшимися таблицами (F и T).

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

След может показать разливы tempdb, которые могут иметь или не иметь сильное влияние.

Другой альтернативный подход - по крайней мере, более быстрый, чтобы попробовать - состоит в том, чтобы упорядочить таблицы от наименьшего числа строк (A) к наибольшему, а затем начать добавлять слияние, хэш и цикл к объединениям. При наличии подсказок порядок соединения фиксируется, как указано. Другие пользователи разумно избегают такого подхода, потому что он может повредить в долгосрочной перспективе, если относительное количество строк резко изменится. Желательно минимальное количество подсказок.

Если вы делаете многие из них, возможно, стоит попробовать коммерческий оптимизатор (или пробную версию), и он по-прежнему является хорошим опытом обучения.

crokusek
источник
Да, это так. Это гарантирует, что строки, возвращаемые A, ограничены ограничением. В противном случае оптимизатор может присоединиться первым и применить ограничение позже. Я занимаюсь этим ежедневно.
Crokusek
4
@crokusek Вы просто не правы. Оптимизатор SQL-сервера хорошо знает, что запросы эквивалентны (независимо от того, находится ли условие в WHERE или в условии ON), когда это соединение INNER.
ypercubeᵀᴹ
6
Вам может пригодиться серия Пола Уайта об Оптимизаторе запросов .
Мартин Смит
Это ужасная привычка. Может быть, это будет в данном конкретном случае (где есть одно ограничение), но я приехал из страны, где несколько разработчиков используют условия AND в предложении where. SQL Server делает не последовательно «шаг» их обратно присоединиться к вам.
Crokusek
Согласитесь, неверно для внешних (и правых соединений). Но когда в предложении where есть только выражения AND'd, и каждый термин соответствует исключительно определенному внутреннему соединению, этот термин можно безопасно и уверенно перенести в положение «on» в качестве оптимизации и лучшей практики (imo). Является ли это «истинным» условием соединения или просто фиксированным ограничением, является вторичным по отношению к значительному увеличению производительности. Эта ссылка для тривиального случая. В реальной жизни существует множество условий, в которых используются функции convert () и математика, что делает их лучшими кандидатами для извлечения передового опыта.
crokusek