Normalmente, eu recomendo não usar dicas de junção por todos os motivos padrão. Recentemente, no entanto, encontrei um padrão em que quase sempre encontro uma junção de loop forçado para ter um desempenho melhor. Na verdade, estou começando a usá-lo e recomendá-lo tanto que desejei obter uma segunda opinião para garantir que não estou perdendo algo. Aqui está um cenário representativo (código muito específico para gerar um exemplo está no final):
--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 possui 1 milhão de linhas e seu PK é ID.
A tabela temporária #Driver possui apenas uma coluna, ID, sem índices e 50 mil linhas.
O que eu sempre acho é o seguinte:
Caso 1: NO HINT
Verificação de índice na
junção de hash de SampleTable
Maior duração (média de 333ms)
Maior CPU (média de 331ms)
Leituras lógicas mais baixas (4714)
Caso 2: LOOP JOIN HINT
Procura de índice na
junção do loop SampleTable
Duração mais baixa (média 204ms, 39% menos)
CPU mais baixa (média 206, 38% menos)
Leituras lógicas muito mais altas (160015, 34X mais)
A princípio, as leituras muito mais altas do segundo caso me assustaram um pouco, porque diminuir as leituras é frequentemente considerado uma medida decente de desempenho. Mas quanto mais penso no que realmente está acontecendo, isso não me preocupa. Aqui está o meu pensamento:
SampleTable está contido em 4714 páginas, ocupando cerca de 36 MB. O caso 1 examina todos e é por isso que obtemos 4714 leituras. Além disso, ele deve executar 1 milhão de hashes, que consomem muita CPU e, finalmente, aumentam o tempo proporcionalmente. É todo esse hash que parece aumentar o tempo no caso 1.
Agora considere o caso 2. Ele não está fazendo nenhum hash, mas está fazendo 50000 buscas separadas, que é o que está impulsionando as leituras. Mas quão caras são as leituras comparativamente? Pode-se dizer que, se essas são leituras físicas, pode ser bastante caro. Mas lembre-se de que 1) apenas a primeira leitura de uma determinada página pode ser física e 2) mesmo assim, o caso 1 teria o mesmo ou pior problema, pois é garantido que ele atinge todas as páginas.
Portanto, considerando o fato de que ambos os casos precisam acessar cada página pelo menos uma vez, parece ser uma questão mais rápida: 1 milhão de hashes ou cerca de 155000 leituras contra a memória? Meus testes parecem dizer o último, mas o SQL Server escolhe consistentemente o primeiro.
Questão
Então, voltando à minha pergunta: devo continuar forçando essa dica de LOOP JOIN ao testar esses tipos de resultados ou estou perdendo alguma coisa em minha análise? Eu hesito em ir contra o otimizador do SQL Server, mas parece que ele muda para o uso de uma junção de hash muito mais cedo do que deveria em casos como esses.
Atualização 2014-04-28
Fiz mais alguns testes e descobri que os resultados que eu estava obtendo acima (em uma VM com 2 CPUs) não conseguiam replicar em outros ambientes (tentei em 2 máquinas físicas diferentes com 8 e 12 CPUs). O otimizador se saiu muito melhor nos últimos casos, a ponto de não haver um problema tão pronunciado. Acho que a lição aprendida, que parece óbvia em retrospecto, é que o ambiente pode afetar significativamente o desempenho do otimizador.
Planos de Execução
Caso do plano de execução 1 Caso do plano de execução 2
Código para gerar exemplo de caso
------------------------------------------------------------
-- 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
*/
fonte
FORCE ORDER
. Na ocasião ímpar em que eu uso uma dica de junção, costumo adicionarOPTION (FORCE ORDER)
um comentário para explicar o porquê.50.000 linhas unidas em uma tabela de milhões de linhas parecem muito para qualquer tabela sem um índice.
É difícil dizer exatamente o que fazer nesse caso, pois é tão isolado do problema que você está realmente tentando resolver. Eu certamente espero que não seja um padrão geral no seu código em que você esteja se juntando a muitas tabelas temporárias não indexadas com quantidades significativas de linhas.
Tomando o exemplo apenas pelo que diz, por que não colocar um índice no #Driver? O D.ID é verdadeiramente único? Nesse caso, é semanticamente equivalente a uma instrução EXISTS, que permitirá ao SQL Server saber que você não deseja continuar pesquisando S por valores duplicados de D:
Em resumo, para esse padrão, eu não usaria uma dica de LOOP. Eu simplesmente não usaria esse padrão. Eu faria um dos seguintes, em ordem de prioridade, se possível:
fonte