Разгадка магических тормозов медленного выполнения запроса

Недавно я написал заметку, в которой описал магическое выполнение запроса, которое поставило меня в ступор http://www.flenov.info/blog/show/Magicheskaya-problema-proizvoditelynosti. В этой заметке я не раскрою все тайны тормозов, потому что я так и не могу понять, почему тогда простое добавление перехода на новую строку меняло план выполнения, а реальное изменение запроса типа добавления and 1=1 или другие модификации оставляли запрос медленным. Даже OPTION (recompile) не влияла. Именно символ новой строки менял план выполнения. Скажу только, что на следующий день этот трюк не работал и запрос оставался медленным даже после добавления новой строки.

Итак, краткая история. Если просто выполнять запрос в SQL Server Management Studio, то он выполняется быстро:

declare @OffSet int = 0
declare @PageSize int = 50
declare @appServerTimeStamp datetime = '2018-05-04 07:14:08.193'
declare @CurrentSessionUserId int =53792

SELECT 
...
...
	ОгромнаяТаблица1
	Join ОкромнаяТаблица2 on …. and userID = @CurrentSessionUserId
...
...
...
...
 
OFFSET @OffSet ROWS

Но .NET запускает параметризированные запросы через хранимую процедуру sp_executesql:

exec sp_executesql N'
SELECT 
...
...
	ОгромнаяТаблица1
	Join ОкромнаяТаблица2 on …. and userID = @CurrentSessionUserId
...
...
...
...
 
OFFSET @OffSet ROWS
FETCH NEXT @PageSize ROWS ONLY  OPTION(RECOMPILE)',
  N'@OffSet int,
  @PageSize int,
  @appServerTimeStamp 
  @CurrentSessionUserId int',
  @OffSet=0,@PageSize=50,
  @appServerTimeStamp='2018-05-04 07:14:08.193',
  @CurrentSessionUserId=53792'

Я весь запрос не могу приводить, и он реально займет наверно 5 листов в Word, но где-то в середине есть связь двух огромных таблиц (точнее это даже две вьюшки), которые фильтруются по ID пользователя. В зависимости от ID пользователя результат этой связки может возвращать 10 записей, а может 1,000. И когда мы выполняем процедуру sp_executesql, то SQL сервер не знает, какой пользователь будет в переменной и сколько он записей вернет, поэтому выбирает самый ужасный план выполнения и сохраняет его.

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

Запрос был оптимизирован введением локальной переменной:

exec sp_executesql N'
declare @useidlocal int = @CurrentSessionUserId

SELECT 
...
...
	ОгромнаяТаблица1
	Join ОкромнаяТаблица2 on …. and userID = @useidlocal
...
...
...
...
 
OFFSET @OffSet ROWS
FETCH NEXT @PageSize ROWS ONLY  OPTION(RECOMPILE)',
  N'@OffSet int,
  @PageSize int,
  @appServerTimeStamp 
  @CurrentSessionUserId int',
  @OffSet=0,@PageSize=50,
  @appServerTimeStamp='2018-05-04 07:14:08.193',
  @CurrentSessionUserId=53792'

Я два дня гуглил, видел большое количество статей в стиле «какого черта sp_executesql медленнее» и видел много рекомендаций, как решить проблему, но ни одна из них не подходила. Только введением локальной переменной, которая используется в фильтрации огромных таблиц запрос заработал стабильно быстро. Видимо сервер по-разному обрабатывает переменные, которые поступают извне и из текста SQL. Вот это для меня не объяснимо.


Понравилось? Кликни Лайк, чтобы я знал, какой контент более интересен читателям. Заметку пока еще никто не лайкал и ты можешь быть первым


Комментарии

Андрей

28 Мая 2018

Видел что-то похожее, но при использовании скалярных функций при фильтрации больших объемов данных. Если значение функции присвоить локалной переменной и использовать ее, запрос сильно ускоряется. Если пойти еще дальше и в динамический запрос подставить значение, получается еще быстрей.
Возможно sp_executesql оценивает входящие параметры по аналогии со скалярными функциями.


Добавить Комментарий

Еще что-нибудь

Хотите найти еще что-то интересное почитать? Можно попробовать отфильтровать заметки на блоге по категориям.

О блоге

Программист, автор нескольких книг серии глазами хакера и просто блогер. Интересуюсь безопасностью, хотя хакером себя не считаю

Обратная связь

Без проблем вступаю в неразборчивые разговоры по e-mail. Стараюсь отвечать на письма всех читателей вне зависимости от страны проживания, вероисповедания, на русском или английском языке.

Пишите мне