通常,出于所有标准原因,我建议不要使用连接提示。但是,最近,我发现了一种模式,在该模式下,我几乎总是找到强制循环联接以提高性能。实际上,我已经开始使用并推荐它太多了,以至于我想征询他人的意见,以确保我不会错过任何东西。这是一个代表性的场景(最后生成示例的非常具体的代码):
--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有100万行,其PK为ID。
临时表#Driver只有一列,ID,没有索引和5万行。
我一直发现以下内容:
情况1:
SampleTable
Hash Join
上的NO HINT 索引扫描持续时间较长(平均333毫秒)
较高的CPU(平均331毫秒)
较低的逻辑读取(4714)
情况2:
在
SampleTable
Loop Join 上寻求LOOP JOIN HINT 索引
持续时间越短(平均204ms,少39%)
CPU 越少(平均206,少38%)
逻辑读取多得多(160015,多34X)
首先,第二种情况下的较高读取值使我有些害怕,因为降低读取值通常被认为是性能的一个不错的衡量标准。但是我对实际发生的事情的思考越多,它与我无关。这是我的想法:
SampleTable包含在4714页上,大约需要36MB。情况1扫描了它们全部,这就是为什么我们得到4714次读取的原因。此外,它必须执行一百万个散列,这些散列占用大量CPU,最终会成比例地增加时间。在情况1中,所有这些散列似乎增加了时间。
现在考虑情况2。它没有进行任何哈希处理,而是进行了50000个单独的查找,这就是驱动读取的原因。但是,这些读本的价格相比有多贵?也许有人会说,如果这些是物理读物,那可能会非常昂贵。但是请记住1)只有给定页面的第一次读取才是物理的,2)即使如此,情况1也会有相同或更严重的问题,因为可以保证击中每个页面。
因此,考虑到这两种情况都必须至少访问每个页面一次的事实,似乎是一个问题,即更快的速度,100万个哈希或大约155000次读取内存的问题?我的测试似乎说后者,但是SQL Server始终选择前者。
题
回到我的问题:当测试显示这些结果时,我应该继续施加这个LOOP JOIN提示吗?还是我的分析中遗漏了一些东西?我犹豫要不要使用SQL Server的优化程序,但是感觉像在这种情况下,它比使用哈希连接要早得多。
更新2014-04-28
我进行了更多测试,发现结果超过了我的要求(在带有2个CPU的VM上),我无法在其他环境中复制(我在2个不同的具有8和12个CPU的物理机上进行了尝试)。在后一种情况下,优化器的性能要好得多,以至于没有这种明显的问题。我想,从回顾中可以明显看出,所汲取的教训是环境会极大地影响优化器的工作效果。
执行计划
执行计划案例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
*/