En règle générale, je déconseille d'utiliser des conseils de jointure pour toutes les raisons standard. Récemment, cependant, j'ai trouvé un modèle où je trouve presque toujours une jointure de boucle forcée pour mieux fonctionner. En fait, je commence à l'utiliser et je le recommande tellement que je voulais obtenir un deuxième avis pour m'assurer de ne rien manquer. Voici un scénario représentatif (le code très spécifique pour générer un exemple est à la fin):
--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 a 1 million de lignes et son PK est ID.
La table temporaire #Driver a une seule colonne, un ID, aucun index et 50K lignes.
Ce que je trouve constamment est le suivant:
Cas 1: AUCUN
INDICE Balayage d'index sur la table de
hachage SampleTable Join
Durée plus élevée (moyenne 333 ms)
Processeur supérieur (moyenne 331 ms)
Lectures logiques inférieures (4714)
Cas 2: LOOP JOIN HINT
Index Seek on SampleTable
Loop Join
Durée inférieure (204 ms, 39% de moins)
CPU plus faible (206, 38% de moins)
Lectures logiques beaucoup plus élevées (160015, 34 fois plus)
Au début, les lectures beaucoup plus élevées du deuxième cas m'ont un peu effrayé car la baisse des lectures est souvent considérée comme une mesure décente des performances. Mais plus je pense à ce qui se passe réellement, cela ne me concerne pas. Voici ma pensée:
SampleTable est contenu sur 4714 pages, prenant environ 36 Mo. Le cas 1 les analyse tous, c'est pourquoi nous obtenons 4714 lectures. De plus, il doit effectuer 1 million de hachages, qui sont gourmands en ressources CPU et qui, finalement, augmentent le temps proportionnellement. C'est tout ce hachage qui semble faire gagner du temps dans le cas 1.
Considérons maintenant le cas 2. Il ne fait aucun hachage, mais au lieu de cela, il effectue 50000 recherches distinctes, ce qui stimule les lectures. Mais combien coûtent les lectures comparativement? On pourrait dire que s'il s'agit de lectures physiques, cela pourrait être assez coûteux. Mais gardez à l'esprit 1) seule la première lecture d'une page donnée peut être physique, et 2) même ainsi, le cas 1 aurait le même problème ou pire car il est garanti de frapper chaque page.
Donc, compte tenu du fait que les deux cas doivent accéder à chaque page au moins une fois, il semble que ce soit une question plus rapide, 1 million de hachages ou environ 155 000 lectures par rapport à la mémoire? Mes tests semblent dire le dernier, mais SQL Server choisit systématiquement le premier.
Question
Revenons donc à ma question: devrais-je continuer à forcer cette indication LOOP JOIN lorsque le test montre ce type de résultats, ou manque-t-il quelque chose dans mon analyse? J'hésite à aller contre l'optimiseur de SQL Server, mais il semble qu'il passe à l'utilisation d'une jointure de hachage beaucoup plus tôt qu'il ne le devrait dans des cas comme ceux-ci.
Mise à jour 2014-04-28
J'ai fait quelques tests supplémentaires et j'ai découvert que les résultats que j'obtenais ci-dessus (sur une machine virtuelle avec 2 processeurs) ne pouvaient pas être répliqués dans d'autres environnements (j'ai essayé sur 2 machines physiques différentes avec 8 et 12 processeurs). L'optimiseur a fait beaucoup mieux dans ces derniers cas au point où il n'y avait pas de problème aussi prononcé. Je suppose que la leçon apprise, qui semble évidente rétrospectivement, est que l'environnement peut affecter de manière significative le fonctionnement de l'optimiseur.
Plans d'exécution
Plan d'exécution, cas 1 Plan d'exécution, cas 2
Code pour générer un exemple de cas
------------------------------------------------------------
-- 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
*/
la source
FORCE ORDER
. À l'occasion étrange, j'utilise un indice de jointure, j'ajoute souventOPTION (FORCE ORDER)
un commentaire pour expliquer pourquoi.50 000 lignes jointes à une table d'un million de lignes semblent être beaucoup pour toute table sans index.
Il est difficile de vous dire exactement quoi faire dans ce cas, car il est tellement isolé du problème que vous essayez réellement de le résoudre. J'espère certainement que ce n'est pas un modèle général dans votre code où vous vous joignez à de nombreuses tables temporaires non indexées avec des quantités importantes de lignes.
Prenant l'exemple juste pour ce qu'il dit, pourquoi ne pas simplement mettre un index sur #Driver? D.ID est-il vraiment unique? Si tel est le cas, c'est sémantiquement équivalent à une instruction EXISTS, qui indiquera au moins à SQL Server que vous ne voulez pas continuer à rechercher dans S les valeurs en double de D:
En bref, pour ce modèle, je n'utiliserais pas un indice LOOP. Je n'utiliserais tout simplement pas ce modèle. Je ferais l'une des actions suivantes, par ordre de priorité si possible:
la source