ITPub博客

首页 > Linux操作系统 > Linux操作系统 > MS SQL 查询联接运算系列--嵌套循环联接(Nested Loops Join)

MS SQL 查询联接运算系列--嵌套循环联接(Nested Loops Join)

原创 Linux操作系统 作者:bigholy 时间:2008-11-16 21:41:17 0 删除 编辑

SQL Server支持三种物理联接运算:嵌套循环联接合并联接哈希联接.在先前的例子中我们已经看到了嵌套循环联接,在以下部分,我们将详细地介绍每一种联接运算的工作原理,另外也对每一个运算支持的逻辑联接类型作一解释,最后讨论每一种联接类型的性能.

嵌套循环联接(Nested Loops Join)

嵌套循环联接是最简单的,最基本的联接算法.它将从一个表(外表)与另一个表(内表)的每一行进行比较,查找满足联接谓词的记录行.

下面我们使用伪代码来描述该算法:

for each row R1 in the outer table

     for each row R2 in the inner table

           if R1 joins with R2

           return (R1, R2)

嵌套循环联接的查询开销=outer表*inner表,由于其查询开销随着input表的增加而增加,实际上,查询优化器试图通过减少要处理的inner行来降低开销.

考虑以下的查询:

Select O.[OrderId]

FROM [Customers] C JOIN [Orders] O ON C.[CustomerId] = O.[CustomerId]

Where C.[City] = N'London'

当执行该查询时,我们得到以下查询计划:

Rows Executes

46 1 |--Nested Loops(Inner Join, OUTER REFERENCES:([C].[CustomerID]))

6 1 |--Index Seek(OBJECT:([Customers].[City] AS [C]),

SEEK:([C].[City]=N'London') orDERED FORWARD)

46 6 |--Index Seek(OBJECT:([Orders].[CustomerID] AS [O]),

SEEK:([O].[CustomerID]=[C].[CustomerID]) orDERED FORWARD)

与其他计划不同,该计划使用了SET STATISTICS PROFILE ON开关,这样可以看到每一个运算执行的行数.在本计划中,outer表是Customers,inner表是Orders,因而,依据嵌套循环联接算法,SQL Server首先从Customers表开始查找,每次读取一个Customer,然后在Orders表上执行索引查找.由于Customers表有6行,在Orders表上执行的索引查找次数为6次.注意到Orders表上的索引查找取决于CustomerID(源自Customers表).SQL Server需要在Orders表上重复执行6次索引查找.而CustomerID具有不同的值,这样,执行6次索引查找将返回不同的记录行.

我们把CustomerID称为相关参数,若嵌套循环联接具有相关参数,可以在其计划中的OUTER REFERENCES找到.通常我们参考具有索引查找的嵌套循环联接类型,该联接类型取决于索引联接的相关参数.索引联接可能是嵌套循环联接中最常见的类型.其实,我们在SQL Server 2005中所见到的书签查询就是这样一种非聚集索引与基表间的索引联接.

上面提到的示例均是SQL Server提高嵌套循环联接性能的两个重要技术:相关参数和基于联接的innter内相关参数的索引查找.而另外一个性能优化是在联接的inner内使用了lazy spool,Lazy spool将联接的inner内的结果进行缓存,以便可以重用.在和具有许多重复值的相关参数上Lazy spool尤其很有用.通过使用了lazy spool,SQL Server无需对联接的inner内的表进行重新计算.

并非所有的嵌套循环联接都有关联参数,不使用相关参数的嵌套循环联接的简单方法是使用cross联接.Cross联接将一个表的所有行与另一个表的所有行进行匹配,要实现cross联接,需要将inner表的每一行与outer表的每一行进行扫描和联接.Inner表的行集并不会发生变化,这主要取决于我们处理的outer表的行数.

在一些情况下,可能没有合适的索引或满足索引查找的联接谓词,查询优化器可能使用无相关参数生成一个查询计划.判断满足索引查找的联接谓词与判断满足索引查找的其他谓词的规则是相同的,例如,在以下的查询中:

Select E1.[EmployeeId], COUNT(*)

FROM [Employees] E1 JOIN [Employees] E2

ON E1.[HireDate] < E2.[HireDate]

GROUP BY E1.[EmployeeId]

在HireDate列上并没有索引存在,这样该查询将产生一个简单的嵌套循环联接(无任何相关的参数和索引查找)

|--Compute Scalar(DEFINE:([Expr1004]=CONVERT_IMPLICIT(int,[Expr1007],0)))

|--Stream Aggregate(GROUP BY:([E1].[EmployeeID]) DEFINE:([Expr1007]=Count(*)))

|--Nested Loops(Inner Join, Where:([E1].[HireDate]<[E2].[HireDate]))

|--Clustered Index Scan(OBJECT:([Employees].[PK_Employees] AS [E1]))

|--Clustered Index Scan(OBJECT:([Employees].[PK_Employees] AS [E2]))

下面我来使用CROSS APPLY来重写上面的查询:

Select E1.[EmployeeId], ECnt.Cnt

FROM [Employees] E1 CROSS APPLY

(

Select COUNT(*) Cnt

FROM [Employees] E2

Where E1.[HireDate] < E2.[HireDate]

) ECnt

虽然这两个查询结果是相同的,对于使用CROSS APPLY的查询计划使用相关能数的嵌套循环联接.

|--Nested Loops(Inner Join, OUTER REFERENCES:([E1].[HireDate]))

|--Clustered Index Scan(OBJECT:([Employees].[PK_Employees] AS [E1]))

|--Compute Scalar(DEFINE:([Expr1004]=CONVERT_IMPLICIT(int,[Expr1007],0)))

|--Stream Aggregate(DEFINE:([Expr1007]=Count(*)))

|--Clustered Index Scan (OBJECT:([Employees].[PK_Employees] AS [E2]),

Where:([E1].[HireDate]<[E2].[HireDate]))

嵌套循环联接举例介绍 

create table Customers (Cust_Id int, Cust_Name varchar(10))

insert Customers values (1, 'Craig')

insert Customers values (2, 'John Doe')

insert Customers values (3, 'Jane Doe')

create table Sales (Cust_Id int, Item varchar(10))

insert Sales values (2, 'Camera')

insert Sales values (3, 'Computer')

insert Sales values (3, 'Monitor')

insert Sales values (4, 'Printer')

打开set statistics profile on开关

执行以下查询:

select *

from Sales S inner join Customers C

on S.Cust_Id = C.Cust_Id

option(loop join)

上面的查询使用了一个loop join hint来强制查询优化器使用嵌套循环联接,执行上述语句得到的查询计划如下: 

说明:

上述计划中外表是Customers,而内表为Sales,这样在开始扫描Customers表时,每次只处理一个Customer,对于每一个Customer,需要扫描Sales表,由于在Customers表有3条记录,因而在Sales表上则需要执行3次扫描,每扫描一次Sales表会返回4行记录.通过将每一个Sale与当前的Customer进行比较来判断这两行是否具有相同的Cust_ID.如果具有相同的Cust_ID,则返回该行.由于Customers表有3条记录,Sales表有4条记录,因而需要执行3*4=12次比较,仅有3条匹配.

下面我们在Sales表上创建一个索引来看看会有何不同?

create clustered index CI on Sales(Cust_Id)

重新执行上述的查询,这次得到如下的查询计划:  

说明:

与前一个结果不同的是,这一次在Sales表使用的是索引查找,并非是表扫描.对每一个Customer,在Sales表上仍需要3次的查找,然而,对于第一次的查找只需返回满足联接谓词与当前Cust_ID相匹配的那些记录,这样,对于表扫描来说(12行),查找仅需3行.

注意:索引查找取决于C.CustID列(是外表还是内表?)执行每次查找时(C.CustID具有不同的值,我们称其为相关参数).如果嵌套循环联接中出现有相关参数,在showplan中以OUTER REFERENCES关键字输出.将此种嵌套循环联接类型(使用相关参数的索引查找)称之为索引联接,这种联接是最常见的.

嵌套循环联接支持哪些联接谓词类型呢?

嵌套循环联接支持所有联接谓词,包括等联接(相等)谓词和不等联接(不相等)谓词.

嵌套循环联接支持以下几种逻辑联接运算符:

Ø Inner Join

Ø Left Outer Join

Ø Cross Join

Ø Cross apply and outer apply

Ø Left semi-join and left anti-semi-join

不支持以下逻辑联接运算符:

Ø Right and full outer join

Ø Right semi-join and right anti-semi-join

显然,我们很容易扩展嵌套循环联接算法以支持left outer和semi-joins.例如:以下是left outer join的伪语言描述:

for each row R1 in the outer table

begin

for each row R2 in the inner table

if R1 joins with R2

return (R1, R2)

if R1 did not join

return (R1, NULL)

end

现在考虑如何支持right outer联接的情况,在这种情况下,我们需要同时返回(R1,R2)和(NULL,R2)的记录行.问题是我们需要扫描内表多次,而外表只需一次.在处理这些多次扫描时,可能会遇到在内表上执行多次的扫描.由此可以推断内表的某行不需联接.另外,如果使用索引联接,则在内表上不会发生此种情况.Right outer联接可以和left outer 联接互换,right semi-join也可以和left semi-join互换.

我们可以针对right outer和semi-join使用嵌套循环联接.例如:Customers left outer 联接Sales,使用上述在Sales表上的聚集索引,也可以在内表Sales上使用索引查找.另一方面,customer right outer联接Sales可以转换为Sales left outer联接Customers,此时则需要在Customer上创建索引.

对于Full outer联接会是怎样的?

嵌套循环联接并不直接支持此种联接,不过,可以通过转换来完成,例如:T1 full outer join T2,可以转换为T1 left outer 联接T2 UNION T2 left anti-semi-join T1.

从以上例子看出,嵌套循环联接适用于内部输入表较少的查询.

来自 “ ITPUB博客 ” ,链接:http://blog.itpub.net/355374/viewspace-495989/,如需转载,请注明出处,否则将追究法律责任。

上一篇: 索引示例介绍
请登录后发表评论 登录
全部评论

注册时间:2008-11-08

  • 博文量
    43
  • 访问量
    84622