Как правило, я рекомендую не использовать подсказки по всем стандартным причинам. Однако недавно я обнаружил шаблон, в котором я почти всегда нахожу принудительное циклическое соединение для лучшей производительности. На самом деле, я начинаю использовать и рекомендовать его так сильно, что мне хотелось получить второе мнение, чтобы убедиться, что я ничего не пропустил. Вот типичный сценарий (очень специфический код для генерации примера в конце):
--Case 1: NO HINT
SELECT S.*
INTO #Results
FROM #Driver AS D
JOIN SampleTable AS S ON S.ID = D.ID
--Case 2: LOOP JOIN HINT
SELECT S.*
INTO #Results
FROM #Driver AS D
INNER LOOP JOIN SampleTable AS S ON S.ID = D.ID
SampleTable имеет 1 миллион строк, а его PK - ID.
Временная таблица #Driver имеет только один столбец, ID, без индексов и 50K строк.
То, что я постоянно нахожу, является следующим:
Случай 1: НЕТ
СОВЕТА Индексации на
хэш-соединении SampleTable.
Более высокая продолжительность (среднее значение 333 мс).
Более высокая загрузка ЦП (среднее значение 331 мс
).
Случай 2:
Индекс LOOP JOIN HINT Поиск по SampleTable
Loop Join
Меньшая продолжительность (в среднем 204 мс, на 39% меньше)
Меньшая загрузка ЦП (в среднем 206, 38%)
намного выше логическое считывание (160015, в 34 раза больше)
Во-первых, гораздо более высокие показатели чтения во втором случае меня немного напугали, поскольку снижение показателей чтения часто считается достойным показателем производительности. Но чем больше я думаю о том, что на самом деле происходит, это меня не касается. Вот мое мышление:
SampleTable содержится на 4714 страницах, занимает около 36 МБ. Случай 1 сканирует их все, поэтому мы получаем 4714 операций чтения. Кроме того, он должен выполнить 1 миллион хэшей, которые требуют значительных ресурсов процессора и в конечном итоге пропорционально увеличивают время. Это все хеширование, которое, кажется, увеличивает время в случае 1.
Теперь рассмотрим случай 2. Он не выполняет хеширование, а вместо этого выполняет 50000 отдельных поисков, что приводит к увеличению числа операций чтения. Но насколько дорого считывание для сравнения? Можно сказать, что если это физическое чтение, это может быть довольно дорого. Но имейте в виду, 1) только первое чтение данной страницы может быть физическим, и 2) даже в этом случае в случае 1 возникнет та же или более серьезная проблема, поскольку гарантированно попадет на каждую страницу.
Таким образом, учитывая тот факт, что оба случая должны получить доступ к каждой странице хотя бы один раз, кажется, что вопрос в том, что быстрее, 1 миллион хэшей или около 155 000 операций чтения из памяти? Мои тесты, кажется, говорят о последнем, но SQL Server последовательно выбирает первое.
Вопрос
Итак, вернемся к моему вопросу: должен ли я продолжать использовать эту подсказку LOOP JOIN, когда тестирование показывает результаты такого рода, или я что-то упускаю в своем анализе? Я не решаюсь пойти против оптимизатора SQL Server, но мне кажется, что он переключается на использование хеш-соединения намного раньше, чем в подобных случаях.
Обновление 2014-04-28
Я провел дополнительное тестирование и обнаружил, что результаты, которые я получал выше (на виртуальной машине с 2 процессорами), я не мог воспроизвести в других средах (я пробовал на 2 разных физических машинах с 8 и 12 процессорами). Оптимизатор работал намного лучше в последних случаях до такой степени, что не было такой выраженной проблемы. Я предполагаю, что извлеченный урок, который кажется очевидным в ретроспективе, заключается в том, что среда может существенно повлиять на эффективность работы оптимизатора.
Планы выполнения
План исполнения Вариант 1 План исполнения Вариант 2
Код для создания примера кейса
------------------------------------------------------------
-- 1. Create SampleTable with 1,000,000 rows
------------------------------------------------------------
CREATE TABLE SampleTable
(
ID INT NOT NULL PRIMARY KEY CLUSTERED
, Number1 INT NOT NULL
, Number2 INT NOT NULL
, Number3 INT NOT NULL
, Number4 INT NOT NULL
, Number5 INT NOT NULL
)
--Add 1 million rows
;WITH
Cte0 AS (SELECT 1 AS C UNION ALL SELECT 1), --2 rows
Cte1 AS (SELECT 1 AS C FROM Cte0 AS A, Cte0 AS B),--4 rows
Cte2 AS (SELECT 1 AS C FROM Cte1 AS A ,Cte1 AS B),--16 rows
Cte3 AS (SELECT 1 AS C FROM Cte2 AS A ,Cte2 AS B),--256 rows
Cte4 AS (SELECT 1 AS C FROM Cte3 AS A ,Cte3 AS B),--65536 rows
Cte5 AS (SELECT 1 AS C FROM Cte4 AS A ,Cte2 AS B),--1048576 rows
FinalCte AS (SELECT ROW_NUMBER() OVER (ORDER BY C) AS Number FROM Cte5)
INSERT INTO SampleTable
SELECT Number, Number, Number, Number, Number, Number
FROM FinalCte
WHERE Number <= 1000000
------------------------------------------------------------
-- Create 2 SPs that join from #Driver to SampleTable.
------------------------------------------------------------
GO
IF OBJECT_ID('JoinTest_NoHint') IS NOT NULL DROP PROCEDURE JoinTest_NoHint
GO
CREATE PROC JoinTest_NoHint
AS
SELECT S.*
INTO #Results
FROM #Driver AS D
JOIN SampleTable AS S ON S.ID = D.ID
GO
IF OBJECT_ID('JoinTest_LoopHint') IS NOT NULL DROP PROCEDURE JoinTest_LoopHint
GO
CREATE PROC JoinTest_LoopHint
AS
SELECT S.*
INTO #Results
FROM #Driver AS D
INNER LOOP JOIN SampleTable AS S ON S.ID = D.ID
GO
------------------------------------------------------------
-- Create driver table with 50K rows
------------------------------------------------------------
GO
IF OBJECT_ID('tempdb..#Driver') IS NOT NULL DROP TABLE #Driver
SELECT ID
INTO #Driver
FROM SampleTable
WHERE ID % 20 = 0
------------------------------------------------------------
-- Run each test and run Profiler
------------------------------------------------------------
GO
/*Reg*/ EXEC JoinTest_NoHint
GO
/*Loop*/ EXEC JoinTest_LoopHint
------------------------------------------------------------
-- Results
------------------------------------------------------------
/*
Duration CPU Reads TextData
315 313 4714 /*Reg*/ EXEC JoinTest_NoHint
309 296 4713 /*Reg*/ EXEC JoinTest_NoHint
327 329 4713 /*Reg*/ EXEC JoinTest_NoHint
398 406 4715 /*Reg*/ EXEC JoinTest_NoHint
316 312 4714 /*Reg*/ EXEC JoinTest_NoHint
217 219 160017 /*Loop*/ EXEC JoinTest_LoopHint
211 219 160014 /*Loop*/ EXEC JoinTest_LoopHint
217 219 160013 /*Loop*/ EXEC JoinTest_LoopHint
190 188 160013 /*Loop*/ EXEC JoinTest_LoopHint
187 187 160015 /*Loop*/ EXEC JoinTest_LoopHint
*/
источник
FORCE ORDER
. В нечетном случае я использую подсказку о присоединении, которую я часто добавляюOPTION (FORCE ORDER)
с комментарием, чтобы объяснить, почему.50000 строк, соединенных с таблицей из миллиона строк, кажется много для любой таблицы без индекса.
Трудно сказать вам точно, что делать в этом случае, поскольку он настолько изолирован от проблемы, которую вы на самом деле пытаетесь решить. Я, конечно, надеюсь, что это не общий шаблон в вашем коде, где вы объединяетесь со многими неиндексированными временными таблицами со значительным количеством строк.
Взяв пример только для того, что он говорит, почему бы просто не поместить индекс в #Driver? Действительно ли D.ID уникален? Если это так, то это семантически эквивалентно выражению EXISTS, которое, по крайней мере, сообщит SQL Server, что вы не хотите продолжать поиск в S повторяющихся значений D:
Короче говоря, для этого шаблона я бы не использовал подсказку LOOP. Я бы просто не использовал этот шаблон. Я бы сделал одно из следующих действий в порядке приоритетности, если это возможно:
источник