在SQL Server中,在以下情况下是否应强制执行LOOP JOIN?


15

通常,出于所有标准原因,我建议不要使用连接提示。但是,最近,我发现了一种模式,在该模式下,我几乎总是找到强制循环联接以提高性能。实际上,我已经开始使用并推荐它太多了,以至于我想征询他人的意见,以确保我不会错过任何东西。这是一个代表性的场景(最后生成示例的非常具体的代码):

--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 方案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

*/

Answers:


13

SampleTable包含在4714页上,大约需要36MB。情况1扫描了它们全部,这就是为什么我们得到4714次读取的原因。此外,它必须执行一百万个散列,这些散列占用大量CPU,最终会成比例地增加时间。在情况1中,所有这些散列似乎增加了时间。

哈希联接(建立哈希表,这也是一个阻塞操作)需要一定的启动成本,但是哈希联接最终在SQL Server支持的三种物理联接类型中具有最低的理论每行开销,这两种IO和CPU的条款。散列连接实际上以相对较小的构建输入和较大的探测输入来实现。也就是说,在所有情况下,物理连接类型都不是“更好”的。

现在考虑情况2。它没有进行任何哈希处理,而是进行了50000个单独的查找,这就是驱动读取的原因。但是,相对而言,读物的价格是多少?也许有人会说,如果这些是物理读物,那可能会非常昂贵。但是请记住1)只有给定页面的第一次读取才是物理的,2)即使如此,情况1也会有相同或更严重的问题,因为可以保证击中每个页面。

每个搜索都需要将b树导航到根,这与单个哈希探针相比在计算上昂贵。另外,与散列连接的探针侧扫描输入的顺序访问模式相比,嵌套循环连接内侧的常规IO模式是随机的。根据基础物理IO子系统的不同,顺序读取可能比随机读取更快。同样,SQL Server预读机制与顺序IO更好地配合使用,发出更大的读取次数。

因此,考虑到这两种情况都必须至少访问每个页面一次的事实,似乎是一个问题,即更快的速度,100万个哈希或大约155000次读取内存的问题?我的测试似乎说后者,但是SQL Server始终选择前者。

SQL Server查询优化器进行了许多假设。一个是查询对页面的第一次访问将导致物理IO(“冷缓存假设”)。对以后的读取来自同一查询已读入内存的页面的可能性进行了建模,但这仅是有根据的猜测。

优化器模型以这种方式工作的原因是通常最好针对最坏情况进行优化(需要物理IO)。并行性和内存中运行的东西可以弥补许多缺点。如果优化程序假设所有数据都在内存中,则该查询计划可能会执行得很差,如果该假设被证明是无效的。

使用冷缓存假设生成的计划可能不会像假设使用暖缓存时那样好,但是在最坏情况下的性能通常会更好。

当测试显示此类结果时,我应该继续施加此LOOP JOIN提示,还是我的分析中遗漏了一些东西?我犹豫要不要使用SQL Server的优化程序,但是感觉像在这种情况下,它比使用哈希连接要早得多。

您应该非常小心,因为两个原因。首先,联接提示还以无提示的方式强制物理联接顺序与查询的书面顺序相匹配(就像您也已指定一样OPTION (FORCE ORDER)。这严重限制了优化器可用的替代方案,并且可能并不总是您想要的。)OPTION (LOOP JOIN)强制嵌套循环联接查询,但不强制执行书面联接顺序。

其次,您假设数据集大小将保持很小,并且大多数逻辑读取将来自缓存。如果这些假设无效(也许随着时间的推移),性能将下降。内置的查询优化器非常擅长于应对不断变化的情况。消除自由是您应该认真考虑的事情。

总体而言,除非有令人信服的理由强制循环联接,否则我将避免这种情况。默认计划通常非常接近于最佳计划,并且在情况不断变化的情况下往往更具弹性。


谢谢保罗。出色的详细分析。根据我所做的一些进一步测试,我认为正在发生的情况是,当临时表的大小在5K到100K之间时,对于该特定示例,优化程序的有根据的猜测一直存在。考虑到我们的要求保证临时表将小于50K,这对我来说似乎很安全。我很好奇,您是否仍会避免任何一种连接提示知道这一点?
JohnnyM 2014年

1
@JohnnyM提示存在是有原因的。可以在有充分理由的地方使用它们。就是说,由于隐含的原因,我很少使用连接提示FORCE ORDER。在很奇怪的情况下,我确实使用了连接提示,我经常添加OPTION (FORCE ORDER)注释以解释原因。
保罗·怀特

0

对于没有索引的任何表,连接到一百万行表的50,000行似乎很多。

在这种情况下,很难告诉您确切的操作,因为它与实际要解决的问题非常隔离。我当然希望这不是代码中的常规模式,在这种模式下,您将与许多具有大量行的未索引临时表联接。

仅以其含义为例,为什么不只在#Driver上添加索引?D.ID真的唯一吗?如果是这样,则在语义上等效于EXISTS语句,该语句至少会让SQL Server知道您不想继续在S中搜索D的重复值:

SELECT S.*
INTO #Results
FROM SampleTable S
WHERE EXISTS (SELECT * #Driver D WHERE S.ID = D.ID);

简而言之,对于这种模式,我不会使用LOOP提示。我根本不会使用这种模式。如果可行,我将按照优先顺序执行以下操作之一:

  • 如果可能的话,请为#Driver使用CTE而不是临时表
  • 如果ID是唯一的,请在ID的#Driver上使用唯一的非聚集索引(假设这是您唯一使用#Driver的时间,并且您不希望表本身有任何数据-如果确实需要该表中的数据,则可以最好将其设为聚集索引)
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.