SQLServer中多表连接时驱动顺序对性能的影响
本⽂出处:
(保留出处并⾮什么原创作品权利,本⼈拙作还远远达不到,仅仅是为了链接到原⽂,因为后续对可能存在的⼀些错误进⾏修正或补充,⽆他)
最近在SQL Server中多次遇到开发⼈员提交过来的有性能问题的SQL,其表⾯的原因是表之间去的驱动顺序造成的性能问题,具体表现在(已排除其他因素影响的情况下),存储过程偶发性的执⾏时间超出预期,甚⾄在调试的时候,直接在存储过程的SQL语句中植⼊某些具体的参数,在性能上仍达不到预期的响应时间。
此类问题在排除了服务器资源因素,索引,锁,parameter sniff等常见问题之后,确认识是表之间的驱动顺序造成的,因为在尝试sql语句的末尾加上option(force order)之后,性能迅速提升。
通常情况下,表之间连接的时候是采⽤“⼩表驱动⼤表”是⼀种相对⽐较⾼效的⽅式,也即在loop join的时候,先循环⼩表,通过循环驱动⼤表,然后产⽣查询结果集。
该性能表⾯上看,是表之间的驱动顺序顺序造成的,在强制⼀个驱动顺序之后,性能有⾮常明显的提升,
但是再进⼀步思考,为什么默认情况下,SQL Server没有选择⼀个合理的驱动顺序?
因此本⽂就简单阐述这两个问题:
1)为什么表之间的驱动顺序会影响性能?
2)为什么SQL Server在某些情况下没有选择出正确的驱动顺序?
为什么表之间的驱动顺序会影响性能?
⾸先演⽰⼀下表在连接的时候,驱动顺序对性能的影响,其中test_smalltable插⼊1W⾏数据,test_bigtable插⼊10W⾏测试数据,依次来代表⼩表与⼤表
create table test_smalltable
(
id int identity(1,1) primary key,
otherColumns char(500)
)
create table test_bigtable
(
id int identity(1,1) primary key,
otherColumns char(500)
)
declare@i int=0
while@i<100000
begin
if@i<10000
begin
insert into test_smalltable values (NEWID())
end
insert into test_bigtable values (NEWID())
set@i=@i+1
end
在测试表写⼊数据完成之后,使⽤⼀下两个SQL,通过强制使⽤loop join的驱动顺序的⽅式来观察其IO情况
select*from test_smalltable a inner loop join test_bigtable b on a.id = b.id option(force order)
GO
select*from test_bigtable a inner loop join test_smalltable b on a.id = b.id option(force order)
GO
如图,是两个SQL执⾏之后产⽣的IO信息,可以发现,因为两个表的驱动顺序不⼀致,导致的逻辑IO⼏乎差了⼀个数量级。
造成此问题的原因,可能有⼀些难以理解,双循环嵌套,谁在外谁在内还有差别,表⾯上看不都是⼀样的?其实不然。
loop join是采⽤的类似如下双循环嵌套的⽅式来执⾏的,直⾄外层的表循环结束,循环(查询)完成
foreache(outer_row in outer_table)
{
  foreache(internal_row in internal_table)
  {
    if (outer_row.key = internal_row.key)
    {
      --输出结果
    }
  }
}
以上述测试为例,做⼀个粗略的对⽐统计
如果外层是⼩表(1W⾏),外层循环1W次,分别对内层的⼤表(10W⾏)查询,然后结束查询,相当于循环1W次,分别⽤Id查询内层表,
可以粗略地认为整体的代价是:1W+1W*10W = 11W,这⾥先忽略具体代价的单位
join on是什么连接如果外层是⼤表(10W⾏),外层循环10W次,分别对内层的⼩表(1W⾏)查询,然后结束查询,相当于循环10W次,分别⽤Id查询内层表,
可以粗略地认为整体的代价是:10W+10W*1W = 20W,同理,这⾥也先忽略代价的单位
现在就很清楚了,前者(⼩表驱动⼤表)的代价是11W,后者(⼤表驱动⼩表)的代价是20W,因此,通常来说,⼩表驱动⼤表是⼀种相对较为⾼效的⽅式。
但是要注意这⾥的⼤表与⼩表,不仅仅是“表”级别的概念,因为实际中SQL并没有这么简单,还可以是根据筛选条件过滤之后的结果的概念,这也是引出第⼆个问题的关键点。
为什么SQL Server在某些情况下没有选择出正确的驱动顺序
  在上述的测试中,如果不加查询提⽰,执⾏计划的⽣成是跟表书写的顺序没有关系的,⼀下截图可以看到,书写顺序不⼀样,执⾏计划仍旧是⼀样的。
  也就是说,在书写SQL语句的时候,⼤表在前或者在后,正常情况下是不影响执⾏计划的⽣成的。
那么为什么,⼀开始提到的问题,为什么SQL Server在某些情况下没有选择出正确的驱动顺序还会出现?
实际情况中,SQL的写法很少有这么简单的,更多的时候是在表连接之后,有各种各样的where条件。
上⾯说了,⼤表与⼩表的概念,不仅仅是“表”级别的概念,更多的是根据筛选条件过滤之后的结果(⾏数,或者⼤⼩)的概念,⽐如,如下SQL,在where条件上可能加上各种筛选条件,⽐如可能是类似于type类型的,可能是时间范围的,还有可能两个表上都有某些筛选条件。
select * from test_smalltable a
inner join test_bigtable b on a.id = b.id
herColumns = '' herColumns = '' and other filter condition
那么此时,在⾯对复杂的查询的时候,SQL Server如何评估每个表经过各种条件筛选后的结果集的⼤⼩?
当然是依据where 后⾯的筛选条件(或者是on 后⾯的加的筛选条件),问题就来了,where 后⾯或者on后⾯的筛选条件,如何⼜依据什么来提供⼀个⼤概的筛选后的结果集?
没错,⼜是统计信息!
现在问题就清晰了起来,SQL Server依据统计信息,在经过各种(或许是⽐较复杂)的筛选条件过滤之后,得到⼀个“它⾃⼰认为的预估⼤⼩的结果集”,然后依据这个结果集来决定驱动顺序。
SQL Server在“它⾃⼰认为的预估⼤⼩的结果集”的基础上进⾏类似于“⼩表驱动⼤表”的⽅式进⾏运算(当然不仅仅是loop join,这⾥暂不说其他的join⽅式),
⼀旦这个预估的结果集的⼤⼩有较⼤的误差,即便是误差不⼤,但是⾜以改变真正的“⼩表驱动⼤表”的⽅式进⾏运算,第⼆个问题就出现了。
因此,总的来说,错误的驱动顺序,本质上在利⽤统计信息进⾏预估的时候,因为统计信息不⾜够准确或者预估算法⾃⼰的问题。 
参考:
导致SQL Server错误地⽤⼤表驱动的⽅式来执⾏运算,类似问题就出现了。
鉴于该问题的特殊性,很难造case,就不造case演⽰了,截两个实际遇到的对⽐结果。实际情况中,驱动顺序对性能产⽣的影响,可能
是从0.5秒到10秒的差别,也可能是1分钟到10分钟的差别
  当然,加option(force order)的时候,要注意写法本⾝的是不是将⼩表放在了最前⾯,
  在复杂的情况下,虽然是驱动顺序造成的问题,但是加option(force order)并不⼀定好使,因为多表连接的时候,按照书写的⽅式强制驱动,也不⼀定刚好就是⼀个合理的驱动顺序
  甚⾄有更严重的问题出现,参考:,因此不建议乱⽤option(force order)
总结:
  ⾯对较为复杂的查询和筛选条件的时候,尤其是在表中的数据较⼤的情况下,统计信息⽣成的预估,以及预估产⽣的表之间的驱动顺序,会对性能产⽣较⼤的影响。
  ⾯对类似问题,要确实直接原因是什么,根本原因是什么,如何快速确认问题,⼜要如何解决和避免,都是值得思考的,也是做性能优
化的时候要考虑的问题之⼀。