Тайна производительности запросов SQL Server

Сегодня у меня было слишком много совещаний, но я думаю, что мои мозги все еще на месте. Пытаясь улучшить производительность некоторого запроса, я столкнулся со следующей загадкой (имена таблиц и поля перефразированы):

SELECT X.ADId FROM
(
    SELECT DISTINCT A.ADId
    FROM P WITH (NOLOCK)
    INNER JOIN A WITH (NOLOCK) ON (P.ID = A.PId)
    INNER JOIN dbo.fn_A(16) AS VD ON (VD.DId = A.ADId)
    LEFT JOIN DPR ON (LDID = A.ADId)
    WHERE ((A.ADId = 1) OR ((HDId IS NOT NULL) AND (HDId = 1))) AND
           (P.PS NOT IN(5,7)) AND (A.ASP IN (2, 3))
) X
WHERE (dbo.fn_B(X.ADId, 16) = 1)

Как вы увидите, содержимое внутреннего запроса в основном не имеет значения. Первоначально весь смысл заключался в том, что я хотел избежать вызова fn_B() для каждой записи, потому что они содержали повторяющиеся значения для ADId, поэтому я сделал SELECT DISTINCT внутренне, а затем отфильтровал отдельные записи. Звучит разумно, верно?

Здесь начинается загадка...

Внутренний запрос возвращает NO RECORDS (для указанных параметров). Если я закомментирую «ГДЕ fn_B() = 1», тогда запрос будет выполнен в нулевое время (и не даст результатов). Если я его снова включу, то запрос занимает 6-10 секунд, снова не возвращая результатов.

Кажется, это превосходит здравый смысл или, по крайней мере, МОЙ здравый смысл в SQL :-) Если внутренний запрос не возвращает данных, то внешние условия никогда не должны оцениваться, верно?

Конечно, я нашел время, чтобы проверить фактические планы выполнения, сохранил их и очень тщательно сравнил. Они на 99% идентичны, в них нет ничего необычного, по крайней мере, так я думаю.

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

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

Любые идеи?

Спасибо за ваше время :-)

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


person Dimitrios Staikos    schedule 03.05.2012    source источник


Ответы (2)


Обратите внимание: оптимизатор не читает запрос так, как это делаете вы. Даже когда вы считаете, что должен иметь место определенный порядок или что сокращение может иметь наибольший смысл, оптимизатор все равно может оценивать CTE/подзапросы в неожиданном для вас порядке. Обходной путь, который вы можете попробовать, — выбрать первый запрос в таблице #temp, а затем запустить функциональный фильтр в таблице #temp. Это должно заставить порядок оценки, даже если он совершенно неинтуитивен и гораздо менее элегантен.

ИЗМЕНИТЬ

Кроме того, хотя он может работать медленнее, мне любопытно, что произойдет, если вы запустите запрос без NOLOCK или вместо этого в RCSI. Различная семантика блокировки может сбивать с толку оптимизатора.

person Aaron Bertrand    schedule 03.05.2012
comment
Я думаю, что большинство людей уже знают, что оптимизатор странным образом перестраивает вещи. НО иногда запросы пишутся таким образом, что за происходящее отвечает программист. Если я сделаю два SELECT DISCTINCT, а затем JOIN к ним, я примерно уверен, что произойдет. В любом случае, я собираюсь попробовать сегодня некоторые вызовы, в которых внутренний запрос фактически приносит данные, или где я заменяю fn_B() фиктивной функцией, просто чтобы посмотреть, как изменится поведение. - person Dimitrios Staikos; 04.05.2012
comment
Вы бы так подумали, но есть много исключений. Оптимизатор не идеален. Вы только сегодня читали сообщение Хьюго в блоге и отчет об ошибке? sqlblog.com/blogs/hugo_kornelis/archive/2012/05/04/ Я видел несколько случаев, когда единственным способом добиться ожидаемого от оптимизатора поведения было разбить запрос на отдельные запросы. Это просто идея, которую вы можете попробовать, как я предложил выше. - person Aaron Bertrand; 04.05.2012
comment
Дополнительная информация. Я запускаю внутренний запрос с параметрами, которые возвращают 6 строк. Нулевое время. Добавьте ГДЕ ==> 30 сек. Я сделал 6 явных вызовов fn(B) с этими идентификаторами, всего 0 сек. Я поместил все это в профилировщик, и вот что дает... SQL Server начинает лавину сканирования таблиц, на ОДИНАКОВЫХ 5 таблицах, снова и снова и снова (примерно 100 000 записей в журнале профилировщика), а затем выполняет запрос. Все эти таблицы появляются внутри функции fn_B(), которая никогда не вызывается в исходном примере. Удаление NOLOCK ничего не изменило. Итак, я начинаю понимать, что здесь что-то сбивает с толку SQL-сервер. - person Dimitrios Staikos; 04.05.2012
comment
Я не сомневаюсь, что в конце концов я/мы найдем способ обойти это. Я хочу сказать, что это выглядит как крайне ненормальное поведение, поэтому мне лично нужно понять, почему это происходит. - person Dimitrios Staikos; 04.05.2012

Мы передали вопрос в службу поддержки Microsoft для SQL Server R2 (я должен прокомментировать их удивительное время отклика и общие процедуры обслуживания). Мы дали им копию нашей БД, которая воспроизводит проблему, и наше обходное решение, они воспроизвели его сами, и через пару дней мы получили ответ:

Я проанализировал оба плана выполнения и хотел бы спросить, приемлемо ли обходное решение для использования в производстве? Основная причина этого заключается в том, что у функции нет статистики, как у индексов. И этот недостаток данных заставляет оптимизатора иногда выбирать не очень хороший план выполнения. Если вы уже нашли обходной путь, лучше реализовать его. Изменения индекса, которые мы пробовали, не улучшали выполнение.

Это довольно дипломатический способ сказать: «Да, оптимизатор все испортил в вашем запросе, поэтому, пожалуйста, используйте обходной путь». Хочешь назвать это багом, называй багом, это не имеет значения.

Для справки, обходной путь заключался в том, чтобы поместить вызов fn_B() в список SELECT запроса на один уровень выше SELECT DISTINCT, а затем отфильтровать его результат по условию WHERE. Немного странно, но это работает.

person Dimitrios Staikos    schedule 10.05.2012