7. SELECT * FROM [Products] as p
SQL Server Execution Times:
CPU time = 204 ms, elapsed time = 12409 ms.
SELECT p.[ID], p.[Name]
FROM [Products] as p
SQL Server Execution Times:
CPU time = 141 ms, elapsed time = 3547 ms.
11. SELECT p.[Name], p.[Price]
FROM [Products] AS p
WHERE p.[Price] BETWEEN 10 and 100
SQL Server Execution Times:
CPU time = 62 ms, elapsed time = 21 ms.
(1794 row(s) affected)
12. Missing index
/*
Missing Index Details from SQLQuery2.sql
The Query Processor estimates that implementing the following index could
improve the query cost by 93.0671%.
*/
/*
USE [demo]
GO
CREATE NONCLUSTERED INDEX [<Name of Missing Index, sysname,>]
ON [dbo].[Products] ([Price])
INCLUDE ([Name])
GO
*/
15. SELECT p.[Name], p.[Price]
FROM [Products] AS p
WHERE p.[Price] BETWEEN 10 and 100
SQL Server Execution Times:
CPU time = 0 ms, elapsed time = 13 ms.
(1794 row(s) affected)
16.
17. SELECT DISTINCT
с.[ID], c.[FirstName], c.[LastName]
FROM [OrderItems] AS oi
LEFT JOIN [Orders] AS o
ON o.[ID] = oi.[OrderId]
LEFT JOIN [Customers] AS c
ON o.[CustomerId] = c.[ID]
WHERE oi.[ProductID] = 123
21. SELECT DISTINCT с.[ID], c.[FirstName], c.[LastName]
FROM [OrderItems] AS oi
LEFT JOIN [Orders] AS o ON o.[ID] = oi.[OrderId]
LEFT JOIN [Customers] AS c ON o.[CustomerId] = c.[ID]
WHERE oi.[ProductID] = 123
Без индекса по ProductId:
C индексом:
SQL Server Execution Times:
CPU time = 47 ms, elapsed time = 45 ms.
(3 row(s) affected)
SQL Server Execution Times:
CPU time = 0 ms, elapsed time = 2 ms.
(3 row(s) affected)
22. SELECT DISTINCT
c.[ID], c.[FirstName], c.[LastName]
FROM [Customers] as c
JOIN Orders as o on o.[CustomerId] = c.[ID]
WHERE o.[Date] >= '12.01.2000'
AND o.[Date]< '01.01.2001'
24. /*
Missing Index Details from SQLQuery5.sql
The Query Processor estimates that implementing the following
index could improve the query cost by 67.9358%.
*/
/*
USE [demo]
GO
CREATE NONCLUSTERED INDEX [<Name of Missing Index,
sysname,>]
ON [dbo].[Orders] ([Date])
INCLUDE ([CustomerId])
GO
*/
26. С индексом:
SQL Server Execution Times:
CPU time = 0 ms, elapsed time = 13 ms.
(843 row(s) affected)
Без индекса:
SQL Server Execution Times:
CPU time = 16 ms, elapsed time = 81 ms.
(843 row(s) affected)
27. SELECT DISTINCT p.[ID], p.[Name]
FROM [Products] AS p
JOIN [OrderItems] AS oi ON oi.[ProductId]=p.[ID]
JOIN [Orders] as o on o.[ID] = oi.[OrderId]
WHERE o.[Date] >= '12.01.2000'
AND o.[Date]< '01.01.2001'
AND p.Price BETWEEN 50 and 100
28. План выполнения
SQL Server Execution Times:
CPU time = 16 ms, elapsed time = 14 ms.
(30 row(s) affected)
29. SELECT DISTINCT p.[ID], p.[Name]
FROM [Products] AS p
JOIN [OrderItems] AS oi ON oi.[ProductId]=p.[ID]
JOIN [Orders] as o on o.[ID] = oi.[OrderId]
WHERE o.[Date] >= '12.01.2000'
AND o.[Date]< '01.01.2001'
30. План выполнения
SQL Server Execution Times:
CPU time = 329 ms, elapsed time = 243 ms.
(4305 row(s) affected)
Ядро SQL Server Database Engine может показывать, каким образом оно переходит к таблицам и использует индексы для доступа к данным или их обработки для запроса или другой инструкции DM, например для обновления. Это называется выводом плана выполнения. Для проведения анализа медленно выполняемого запроса полезно изучить план выполнения запроса, чтобы определить причину проблемы.
База наполнялась фиктивными данными с помощью программы SQL Data Generator от компании Red Gate
(http://www.red-gate.com/products/sql_data_generator/index.htm)
Обратить внимание на использование статистики для базы данных
Статистика используется оптимизатором запросов для построения наиболее эффективного плана выполнения запроса
В нашем случае статистика будет отключена.
Ожидаемый план выполнения показывается сразу же.
Реальный план выполнения – только после завершения выполнения запроса.
Замечания:
- Стараться избегать подобных запросов, т.е. Выбирать те данные которые действительно необходимы в каждом конкретном случае.
Каждый узел древовидной структуры представлен в виде значка, указывающего логический и физический оператор, используемый для выполнения этой части запроса или инструкции.
Каждый узел связан со своим родительским узлом. Дочерние узлы одного родительского узла отображаются в одном столбце. Однако все узлы в одном столбце не обязательно имеют общий родительский узел. Правила со стрелками на конце соединяют каждый узел с его родителем.
Операторы показаны в виде символов, связанных с определенным родительским узлом.
Ширина стрелки пропорциональна количеству строк. Если имеются данные о фактическом количестве строк, используются эти данные. В противном случае используется ориентировочное количество строк.
Если запрос содержит несколько инструкций, показывается несколько планов выполнения запроса.
Для графического представления плана выполнения запроса студия использует SHOWPLAN_XML
Оператор Clustered Index Scan сканирует кластеризованный индекс, заданный в столбце Аргумент плана выполнения запроса. При наличии необязательного предиката WHERE:(), возвращаются только строки, удовлетворяющие предикату. Если столбец Argument содержит предложение ORDERED, обработчик запросов требует, чтобы выходные данные строк были возвращены в порядке, в соответствии с которым они были отсортированы в кластеризованном индексе. Если предложение ORDERED отсутствует, подсистема хранилища выполняет поиск в индексе оптимальным способом, без обязательной сортировки выходных данных.
Clustered Index Scan является логическим и физическим оператором.
Студия предлагает создать индекс
Нажимаем правую клавишу мыши и выбираем «Missing Index Details…»
Полный текст предложения о создании индекса
Создаем рекомендованный индекс с именем idx_price_name
С дополнительным полем Name
Index Seek – наиболее благоприятный вариант при выборке данных
Оператор Clustered Index Seek использует поисковые возможности индексов для получения строк из кластеризованного индекса. Столбец Аргумент содержит имя используемого кластеризованного индекса и предикат SEEK:() SEEK:(). Подсистема хранилища использует индекс для обработки только тех строк, которые удовлетворяют данному предикату SEEK:(). SEEK:(). Он также может содержать предикат WHERE:(), который подсистема хранилища применяет для всех строк, которые удовлетворяют предикату SEEK:(), но предикат WHERE:() необязателен и не использует индексы для завершения процесса.
OBJECT:([demo].[dbo].[Products].[idx_price_name] AS [p]), SEEK:([p].[Price] >= CONVERT_IMPLICIT(decimal(18,2),[@1],0) AND [p].[Price] <= CONVERT_IMPLICIT(decimal(18,2),[@2],0)) ORDERED FORWARD
Подробная информация по плану выполнения запроса
Немного «усложненный» запрос. Суть его в следующем:
«Выбираем всех потребителей, которые приобрели конкретный продукт.»
Выполним запрос, для того чтобы постореть на временные затраты
У каждой операции есть свой вес(%)
Оператор Hash Match строит хэш-таблицу при помощи вычисления хэш-значения для каждой строки из своих входных данных. Предикат HASH:() со списком столбцов, использованных для создания хэш-значения, отображается в столбце Argument. Затем для каждой тестовой строки (если возможно) он вычисляет хэш-значение (с использованием той же хэш-функции) и осуществляет поиск совпадений по хэш-таблице. Если наличествует остаточный предикат (определенный посредством RESIDUAL:() в столбце Argument), строки должны удовлетворять также и этому предикату, чтобы рассматриваться в качестве совпадающих.
Оператор Merge Join выполняет внутреннее соединение, левое внешнее соединение, левое полусоединение, левое антиполусоединение, правое внешнее соединение, правое полусоединение, правое антиполусоединение, а также логические операции соединения.
Студия рекомендует создать индекс.
Создаем рекомендованный индекс с именем idx_ProductId
OrderId помещаем в дополнительное поле
Проверяем ожидаемый план выполнения запроса после создания рекомендованного индекса
Мы видим, что план выполнения радикально изменился
Сканирование индекса заменилось на поиск по индексу (стоимость/вес операции стал 11%)
Соеденения с помощью хеширования заменено на соединение с помощью Вложенных циклов
Оператор Nested Loops выполняет логические операции внутреннего соединения, левого внешнего соединения, левого полусоединения и антилевого полусоединения.
Соединения вложенных циклов выполняют поиск во внутренней таблице для каждой строки внешней таблицы, обычно используя индекс. Опираясь на предполагаемые затраты, сервер Microsoft SQL Server решает вопрос о необходимости сортировки внешней таблицы, чтобы улучшить район поиска по индексу во внутренней таблице.
В большинстве случаев Nested Loops предпочтительнее, чем Hash Match
Помотрим, каковы временные затраты на выполнение запроса
Еще один схожий запрос –
Ищем клиентов, которые купили что-нибудь в декабре 2000 г.
Мы снова видим рекомендации по созданию индекса
Создаем рекомендуемый индекс
И опять выполняем запрос
IndexScan с весом в 68% заменился на IndexSeek с долей 6%
Сравним выполнение запроса с индексом и без него
Совсем коротко о том, как меняется план выполнения в зависимости от условия
Сначала выполним поск товаров купленных в декабре 2000г в ценовом диапазоне от 50 до 100 денежных единиц
Убираем ценовое ограничение
Мы видим, что план запроса изменился радикально.
Над операторами, выполняющимися параллельно, отображается значок параллельного процесса.
И нет никаких рекомендаций по индексам.
Также следует обратить внимание на толщину стрелок, соединяющих пиктограммы процессов
Время выполнения увеличилось также радикально.
Pinalkumar Dave is a Microsoft SQL Server MVP and a Mentor for Solid Quality India. He has written over 1300 articles on the subject on his blog athttp://blog.sqlauthority.com.
В заключение надо сказать, что индексами надо пользоваться весьма умеренно.
Индексы замедляют вставку записей, их периодически необходимо дефрагментировать, проверять целосность...
Бывают случаи (довольно редко), когда приходится в запросе указывать какой именно индекс надо использовать при построении плана запроса.