(乍一看,这可能看起来像是直接执行语句和从存储过程执行语句时的不同执行计划或为什么 SqlServer 优化器与参数如此混淆?,但我的实际问题有点不同)
好吧,这个让我难倒了几个小时。我这里的例子抽象得离谱,所以我怀疑是否可以在本地重新创建,但它为我的问题提供了上下文(另外,我正在运行 SQL Server 2005)。
我有一个存储过程,基本上有两个步骤,构建一个临时表,用很少的行填充它,然后查询一个非常大的表来连接该临时表。它有多个参数,但最相关的是一个datetime
“ @MinDate
.”。本质上:
create table #smallTable (ID int)
insert into #smallTable
select (a very small number of rows from some other table)
select * from aGiantTable
inner join #smallTable on #smallTable.ID = aGiantTable.ID
inner join anotherTable on anotherTable.GiantID = aGiantTable.ID
where aGiantTable.SomeDateField > @MinDate
如果我只是将其作为普通查询执行,通过声明@MinDate
为局部变量并运行它,它会生成一个执行速度非常快的最佳执行计划(首先加入#smallTable,然后在执行时只考虑来自 aGiantTable 的一小部分行其他操作)。似乎意识到#smallTable 很小,所以从它开始会很有效。这很好。
但是,如果我将其作为参数作为存储过程@MinDate
,则会产生完全低效的执行计划。(我每次都在重新编译它,所以这不是一个糟糕的缓存计划……至少,我当然希望不是)
但这就是奇怪的地方。如果我将 proc 更改为以下内容:
declare @LocalMinDate datetime
set @LocalMinDate = @MinDate --where @MinDate is still a parameter
create table #smallTable (ID int)
insert into #smallTable
select (a very small number of rows from some other table)
select * from aGiantTable
inner join #smallTable on #smallTable.ID = aGiantTable.ID
inner join anotherTable on anotherTable.GiantID = aGiantTable.ID
where aGiantTable.SomeDateField > @LocalMinDate
然后它给了我有效的计划!
所以我的理论是这样的:当作为普通查询(而不是存储过程)执行时,它会等待为昂贵的查询构造执行计划,直到最后一分钟,所以查询优化器知道#smallTable 很小并使用该信息给出有效的计划。
但是当作为存储过程执行时,它会一次创建整个执行计划,因此无法使用这些信息来优化计划。
但是为什么使用本地声明的变量会改变这一点?为什么会延迟执行计划的创建?真的是这样吗?如果是这样,即使不以这种方式使用局部变量,是否有办法强制延迟编译(如果这确实是这里发生的事情)?
更一般地说,是否有人知道何时为存储过程的每个步骤创建执行计划?谷歌搜索没有提供任何有用的信息,但我不认为我在寻找正确的东西。还是我的理论完全没有根据?
编辑:自从发布以来,我已经了解了参数嗅探,我认为这是导致执行计划过早编译的原因(除非存储过程确实一次编译),所以我的问题仍然存在——你能强制延迟吗?或者完全禁用嗅探?
select * from aGiantTable
这个问题是学术性的,因为我可以通过替换来强制执行更有效的计划
select * from (select * from aGiantTable where ID in (select ID from #smallTable)) as aGiantTable
或者只是吸收它并掩盖参数,但这种不一致仍然让我很好奇。
tl;dnr
这是一个非常长的问题,所以简而言之:
完整的执行计划是在第一次调用存储过程时创建的,还是在执行时创建的?也就是说,如果一个存储过程由多个步骤组成,每个步骤的执行计划是在第一次调用该过程时创建的,还是仅在过去的步骤完成执行后创建(再次,第一次调用它)?