Query_hash и Query_plan_hash, что в этих числах? Часть вторая.

Добрый день коллеги.

Мы продолжаем разговор о использовании query_hash и query_plan_hash для оптимизации использования процедурного кэша и уменьшения нагрузки на процессор. В предыдущем блоге мы выполнили постановку задачи и вышли на использование этих свойств запроса (https://blogs.technet.com/b/sqlruteam/archive/2014/11/09/sql_5f00_server_5f00_query_5f00_hash_5f00_and_5f00_query_5f00_plan_5f00_hash_5f00_part_5f00_1.aspx).

Для более глубокого понимания описываемых свойств рассмотрим упрощенную архитектуру системы компиляции запросов (кода Transact-SQL).

Ниже приведена блок-схема. Для упрощения часть ее компонентов пропущена.

Последовательность превращения кода, написанного на Transact-SQL, в план выполнения следующая.

1. Текст запроса (функция, триггер, хранимая процедура) попадает на вход Parser-а. Задача которого проста - выполнить синтаксический контроль введенного текста и нормализацию текста (удалить ненужные пробелы и форматировать во внутренний (нормальный) формат. На этом этапе из кода запроса рассчитывается sql_handle (внутренний идентификатор однозначно идентифицирующий этот код).  sql_handle рассчитывается исходя из исходного (ненормализованного) текста запроса и поэтому такой же запрос, но с дополнительным символом "пробел" или оператором GO в одном и том же тексте, будет трактоваться как новый запрос, и для него будет рассчитан свой sql_handle, и создана еще одна запись в процедурном кэше.

Ниже приведены два абсолютно одинаковых запроса, отличающиеся наличием лишнего символа "пробел". Обратите внимание на то, что sql_handle у них разные, а вот query_hash и query_paln_hash одинаковы.

select *
from [Person].[Person] pp
inner join [Sales].[SalesPerson] ssp
on pp.BusinessEntityID=ssp.BusinessEntityID
where FirstName= 'Michael'

go

select *
from [Person].[Person] pp
inner join [Sales].[SalesPerson] ssp
on pp.BusinessEntityID=ssp.BusinessEntityID
where FirstName= 'Michael'

2. Нормализованный текст запроса попадает на Algebrizer. Его задача - превратить все ваши "измышления" на языке T-SQL в дерево разбора (Bound Tree), описывающее алгоритм выполнения запроса (кстати для View на этом все и заканчивается и дальнейшая обработка (оптимизация) не производится). Также, на данном этапе производится извлечение из кода запроса литералов. Цель этой операции -  обеспечить возможность использования Auto Parameterization (Автопараметризации) для уменьшения количества планов выполнения. Ниже приведенные запросы будут иметь разные sql_handle (поскольку побайтово тексты запросов разные), но одинаковые query_hash и query_plan_hash поскольку дерево разбора и алгоритмы их выполнения одинаковы.

select *
from [Person].[Person] pp
inner join [Sales].[SalesPerson] ssp
on pp.BusinessEntityID=ssp.BusinessEntityID
where pp.BusinessEntityID = 274

go

select *
from [Person].[Person] pp
inner join [Sales].[SalesPerson] ssp
on pp.BusinessEntityID=ssp.BusinessEntityID
where pp.BusinessEntityID = 275

 3. На этапе оптимизации решаются более сложные задачи.

Первая из них - это отыскание тривиального плана. Часть запросов достаточно просты и нет необходимости их оптимизировать, например, простейшие операторы INSERT, UPDATE, DELETE, SELECT не использующие никаких условий (предикатов). Для таких запросов хранятся готовые планы выполнения.

Вторая задача - это стандартизация.  Это сложная задача по приведению разных конструкций, дающих один и тот же результат, к одной стандартной конструкции. На этом этапе сервер преобразует код T-SQL в некоторые стандартные (предварительно заготовленные разработчиками) формы деревьев, для того чтобы уменьшить возможное количество кандидатов на планы выполнения.  
Например: 
- соединение таблиц через оператор JOIN ... ON это одно и то же, что соединение через запятую и оператор WHERE,
- использование операторов WHERE ... BETWEEN ... AND ... дает тот же результат, что и операторы WHERE ...  >=... и ...<=,
- использование оператор WHERE ... IN(....) дает тот же результат, что и операторы WHERE ... OR ...
- и т.д.

select *
from [Person].[Person] pp
inner join [Sales].[SalesPerson] ssp
on pp.BusinessEntityID=ssp.BusinessEntityID
where FirstName= 'Michael'

go

select *
from [Person].[Person] pp, [Sales].[SalesPerson] ssp
where pp.BusinessEntityID=ssp.BusinessEntityID and FirstName= 'Michael'

   

Обратите внимание, что несмотря на то, что тексты запросов абсолютно разные, query_hash (структуры деревьев разбора) также разные, а query_plan_hash одинаковы, благодаря встроенному "интеллекту" оптимизатора, "понимающему", что (по алгоритму выполнения) это один и тот же запрос.

Еще пример с операторами DISTINCT и GROUP BY.

select distinct FirstName
from [Person].[Person] pp
inner join [Sales].[SalesPerson] ssp
on pp.BusinessEntityID=ssp.BusinessEntityID

go

select FirstName
from [Person].[Person] pp
inner join [Sales].[SalesPerson] ssp
on pp.BusinessEntityID=ssp.BusinessEntityID
group by FirstName

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

Итак мы рассмотрели принципы работы системы компиляции запросов и связь их с query_hash и query_plan_hash. В следующих статьях мы сформулируем ряд рекомендаций по оптимизации использования процедурного кэша на основе полученных знаний.

Александр Каленик, Senior Premier Field Engineer (PFE), MSFT (Russia)