Por lo general, recomiendo no usar sugerencias de combinación por todas las razones estándar. Recientemente, sin embargo, he encontrado un patrón en el que casi siempre encuentro una unión de bucle forzado para un mejor rendimiento. De hecho, estoy empezando a usarlo y recomendarlo tanto que quería obtener una segunda opinión para asegurarme de que no me falta algo. Aquí hay un escenario representativo (el código muy específico para generar un ejemplo está al 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 tiene 1 millón de filas y su PK es ID.
La tabla temporal #Driver tiene solo una columna, ID, sin índices y 50,000 filas.
Lo que siempre encuentro es lo siguiente:
Caso 1: SIN SUGERENCIA
Escaneo de índice en SampleTable
Hash Join
Mayor duración (promedio 333 ms)
CPU más alta (promedio 331 ms)
Lecturas lógicas más bajas (4714)
Caso 2: SUGERENCIA SUGERENCIA
Indice Buscar en la tabla Tabla
Loop Join
Duración más baja (promedio 204ms, 39% menos)
CPU más baja (promedio 206, 38% menos)
Lecturas lógicas mucho más altas (160015, 34X más)
Al principio, las lecturas mucho más altas del segundo caso me asustaron un poco porque reducir las lecturas a menudo se considera una medida decente del rendimiento. Pero cuanto más pienso en lo que realmente está sucediendo, no me preocupa. Aquí está mi pensamiento:
SampleTable está contenido en 4714 páginas, ocupando aproximadamente 36MB. El caso 1 los escanea a todos, razón por la cual obtenemos 4714 lecturas. Además, debe realizar 1 millón de hashes, que son intensivos en CPU, y que finalmente aumentan el tiempo proporcionalmente. Es todo este hashing lo que parece aumentar el tiempo en el caso 1.
Ahora considere el caso 2. No está haciendo ningún hashing, sino que está haciendo 50000 búsquedas separadas, que es lo que está impulsando las lecturas. Pero, ¿qué tan caras son las lecturas comparativamente? Se podría decir que si se trata de lecturas físicas, podría ser bastante costoso. Pero tenga en cuenta 1) solo la primera lectura de una página determinada podría ser física, y 2) aun así, el caso 1 tendría el mismo problema o un problema peor, ya que se garantiza que llegará a cada página.
Entonces, teniendo en cuenta el hecho de que ambos casos tienen que acceder a cada página al menos una vez, parece ser una cuestión de cuál es más rápido, ¿1 millón de hashes o aproximadamente 155000 lecturas contra la memoria? Mis pruebas parecen decir lo último, pero SQL Server elige consistentemente lo primero.
Pregunta
Volviendo a mi pregunta: ¿debo seguir forzando esta sugerencia de LOOP JOIN cuando las pruebas muestran este tipo de resultados, o me falta algo en mi análisis? Dudo en ir en contra del optimizador de SQL Server, pero parece que cambia a usar un hash join mucho antes de lo que debería en casos como estos.
Actualizar 28-04-2014
Hice algunas pruebas más y descubrí que los resultados que estaba obteniendo arriba (en una VM con 2 CPU) no podía replicar en otros entornos (probé en 2 máquinas físicas diferentes con 8 y 12 CPU). El optimizador funcionó mucho mejor en los últimos casos hasta el punto en que no hubo un problema tan pronunciado. Supongo que la lección aprendida, que parece obvia en retrospectiva, es que el entorno puede afectar significativamente qué tan bien funciona el optimizador.
Planes de ejecucion
Plan de ejecución Caso 1 Plan de ejecución Caso 2
Código para generar caso de muestra
------------------------------------------------------------
-- 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
*/