两种特殊情况
与计划最优性相关的重新编译在下列两种特殊情况中的处理方式有所不同。
特殊情况 1:在空表或索引视图上创建的统计sql server 2005 处理下列情况的方式不同于 sql server 2000。用户创建了一个空表 t。然后又在 t 一个或多个列上创建了一个统计 s。由于 t 为空,因此统计二进制大对象(直方图)为 null,但已经在 t 上创建了统计。假设在查询编译期间已发现 s 是“令人关注的”。根据重新编译阈值的“500 行”规则,只有至少包含 500 行,t 才会在 sql server 2000 上导致重新编译。所以,如果 t 包含的行不足 500,用户可能使用欠优化的计划。
sql server 2005 可检测到这种特殊情况,并以不同的方式进行处理。在 sql server 2005 中,这种表或索引视图的重新编译阈值为 1。换句话说,即使仅在 t 中插入一行,也可能导致重新编译。发生这种重新编译时,s 将被更新,同时 s 的直方图不再为 null。然而,这一重新编译附带了重新编译阈值 (500 + 0.20 * n)的一般规则。
在 sql server 2005 中,即使发生下列情况,重新编译阈值始终为 1:(1) t 没有统计;或者 (2) t 没有在查询编译期间被认作是“令人关注的”统计。
特殊情况 2:触发器重新编译导致重新编译的与计划最优性相关的所有原因都适用于触发器。另外,由于已插入或已删除的表中的行数在不同的触发器执行间发生巨大变化,也会对触发器产生与计划最优性相关的重新编译。
回想一下,影响一行或多行的触发器会被单独缓存。已插入和已删除的表中的行数通过触发器的查询计划进行保存。这些数字反映了导致计划缓存的触发器执行的行数。如果后续的触发器执行产生了拥有“截然不同的”行数的已插入或已删除的表,那么将对该触发器进行重新编译(并缓存带有新行数的全新的查询计划)。
在 sql server 2005 中,“截然不同”的定义如下:
| log10(n) – log10(m) | > 1 if m > n
| log10(n) – log10(m) | > 2.1 otherwise
其中 n 是已缓存查询计划中的已插入或已删除表的行数,而 m 是当前的触发器执行的对应表的行数。如果同时存在“已插入”和“已删除”的表,将对两者分别执行上面提到的测试。
举一个计算示例,从 10 到 100 的行数更改不会导致重新编译,而从 10 到 101 的更改则完全相反。
在 sql server 2000 中,“截然不同”的定义如下:
| log10(n+5) – log10(m+5) | >= 1
其中 n 和 m 的定义同上。请注意,根据这个公式,在 sql server 2000 中将已插入或已删除的表的基数从 5 改为 95,将导致重新编译,而从 5 到 94 的更改则不然。
识别与统计相关的重新编译可通过包含字符串“statistics changed”的事件探查器跟踪(将在本文后面介绍)的“eventsubclass”列来识别与统计相关的重新编译。
结束语与本文档的主题没有直接相关的一个问题是:给定的多个统计以相同的顺序存在于一组相同的列中,那么在查询优化期间,查询优化器如何决定所要载入的统计呢?答案并不那么简单,但查询优化器采用如下原则:为最近的统计提供比较旧的统计更高的优先权;为使用 fullscan 选项计算得出的统计提供比用样例计算得出的统计更高的优先权;等等。
与计划最优性相关的编译、重新编译和统计创建/更新间的“因果”关系可能会造成混淆。回想一下,统计可通过手动或自动方式创建或更新。只有编译和重新编译才会导致统计的自动创建或更新。另一方面,当(手动或自动)创建或更新一个统计时,重新编译查询计划(可能会发现该统计“令人关注”)的概率将增大。
最佳实务
下面给出了四个用于减少与计划最优性相关的批处理重新编译的最佳实务:
最佳实务:因为表变量的基数发生变化不会导致重新编译,所以可考虑使用表变量来替代临时表。然而,由于查询优化器不跟踪表变量的基数,同时不在表变量上创建或维护统计,因此不可能产生最佳的查询计划。用户必须确认情况是否如此,并适当地加以权衡。
最佳实务:keep plan 查询提示可改变临时表的重新编译阈值,使之与永久表的重新编译阈值相同。所以,如果对临时表的更改会导致大量的重新编译,就可使用此查询提示。可使用下列语法指定该提示:
select b.col4, sum(a.col1)
from dbo.permtable a inner join #temptable b on a.col1 = b.col2
where b.col3 < 100
group by b.col4
option (keep plan)
最佳实务:为了完全避免因与计划最优性相关的(与统计更新相关的)原因而导致的重新编译,可使用下列语法指定 keepfixed plan 查询提示:
select c.territoryid, count(*) as number, c.salespersonid
from sales.store s inner join sales.customer c
on s.customerid = c.customerid
where s.name like '%bike%' and c.salespersonid > 285
group by c.territoryid, c.salespersonid
order by number desc
option (keepfixed plan)
运用这一选项,只有与正确性相关的原因(例如,语句更改所引用的表的架构,或用 sp_recompile 过程标记的表)才会导致重新编译。
在 sql server 2005 中,下方所述的行为方式略有不同。假设带有 option(keepfixed plan) 提示的查询首次被编译,而这一编译会导致统计的自动创建。如果 sql server 2005 可获得一个特殊的“统计锁”,那么就会发生重新编译并自动创建统计。如果无法获得“统计锁”,就会不产生重新编译,并在没有该统计的情况下编译查询。在 sql server 2000 中,出于与统计相关的原因,带有 option(keepfixed plan) 的查询从不会被重新编译,所以在这种情况下,不会尝试获取“统计锁”或自动创建统计。
最佳实务:对表或索引视图上定义的索引和统计关闭统计自动更新,将确保因这些对象所导致的与计划最优性相关的重新编译将停止。但是请注意,用这种方法关闭“自动统计”功能通常并不是一个好办法,因为查询优化器不再响应这些对象中的数据变更,并可能导致次最佳查询计划。不到万不得已不要采用这种方法。
八、编译、重新编译和并发在 sql server 2000 中,存储过程、触发器和动态 sql 的编译和重新编译均被串行化。例如,假定使用“exec dbo.sp1”提交了一个存储过程用以执行。并假设当 sql server 编译 sp1 时,收到了另一个引用相关存储过程的请求“exec dbo.sp1”。第二个请求将等到第一个请求完成 sp1 的编译,然后尝试重用结果查询计划。在 sql server 2005 中,编译被串行化,而重新编译则不会。换句话说,相同存储过程的两个并发重新编译可能会继续。最后结束的重新编译请求将替代由另一个请求生成的查询计划。
九、编译、重新编译和参数嗅探“参数嗅探”是一个过程,通过这一过程,sql server 的执行环境可在编译或重新编译时“嗅探”当前参数值,并将之传递给查询优化器,以用于生成更快的查询执行计划。“当前”一词指导致编译或重新编译的语句调用中所存在的参数值。在 sql server 2000 和 sql server 2005 中,将在编译或重新编译下列批处理类型时嗅探参数值:
• | 存储过程 |
• | 通过 sp_executesql 提交的查询 |
• | 预备的查询 |
在 sql server 2005 中,这一操作被扩展到使用 option(recompile) 查询提示提交的查询上。对于这种查询(可以是 select、insert、update 或 delete),将同时嗅探本地变量的参数值和当前值。在批处理中,所嗅探到的(参数和本地变量的)值后面紧跟着带有 option(recompile) 提示的语句。尤其对于参数来说,不会嗅探批处理调用所附带的值。
十、识别重新编译
sql server 的事件探查器使得识别导致重新编译的批处理变得很简单。启动一个新的事件探查器跟踪,并在存储过程事件类别下,选择下列事件。(为了减少所生成的数据量,建议用户取消选定其他事件。)
• | sp:starting |
• | sp:stmtstarting |
• | sp:recompile |
• | sp:completed |
此外,为了检测与统计更新相关的重新编译,可选择“对象”类别下的“自动统计”事件。
现在,启动 sql server 2005 management studio,并执行下列 t-sql 代码:
use adventureworks -- on sql server 2000, say "use pubs"
go
drop procedure demoproc1
go
create procedure demoproc1 as
create table #t1 (a int, b int)
select * from #t1
go
exec demoproc1
go
exec demoproc1
go
暂停事件探查器跟踪,并将看到下列事件。
eventclass | textdata | eventsubclass |
sp:starting | exec demoproc1 | |
sp:stmtstarting | -- demoproc1 create table #t1 (a int, b int) | |
sp:stmtstarting | -- demoproc1 select * from #t1 | |
sp:recompile | deferred compile | |
sp:stmtstarting | -- demoproc1 select * from #t1 | |
sp:completed | exec demoproc1 | |
sp:starting | exec demoproc1 | |
sp:stmtstarting | -- demoproc1 create table #t1 (a int, b int) | |
sp:stmtstarting | -- demoproc1 select * from #t1 | |
sp:completed | exec demoproc1 |
该事件序列指示“select * from #t1”为导致重新编译的语句。eventsubclass 列指出了进行重新编译的原因。在这种情况下,当 demoproc1 在开始执行之前被编译,就可对“create table”语句进行变异。后续的“select”语句可能不会被编译,因为其引用了一个在初始编译时不存在的临时表 #t1。因此,demoproc1 的已编译计划是不完整的。当 demoproc1 开始执行时,随即创建了 #t1,然后就可以对“select”语句进行编译。由于 demoproc1 已经执行,因此根据我们对重新编译的定义,这一编译可视为重新编译。此重新编译的真正原因是“延迟编译”。
请注意,有趣的一点是:当再次执行 demoproc1 时,查询计划将不再是不完整的。重新编译已经将 demoproc1 的一个完整的查询计划插入计划缓存中。所以,第二次执行过程中未发生任何重新编译。
sql server 2000 在这方面的情况也相同。
通过选择下列这组跟踪事件,也可以识别导致重新编译的批处理。
• | sp:starting |
• | sp:stmtcompleted |
• | sp:recompile |
• | sp:completed |
如果在选择了这组新的跟踪事件后执行了刚才所述的例子,那么跟踪输出将如下所示。
eventclass | textdata | eventsubclass |
sp:starting | exec demoproc1 | |
sp:stmtcompleted | -- demoproc1 create table #t1 (a int, b int) | |
sp:recompile | deferred compile | |
sp:stmtcompleted | -- demoproc1 select * from #t1 | |
sp:completed | exec demoproc1 | |
sp:starting | exec demoproc1 | |
sp:stmtcompleted | -- demoproc1 create table #t1 (a int, b int) | |
sp:stmtcompleted | -- demoproc1 select * from #t1 | |
sp:completed | exec demoproc1 |
在此请注意,导致重新编译的语句将在 sp:recompile 事件后被输出。这种方法不如第一种方法直接了当。因此,之后应跟踪第一组事件探查器跟踪事件。
为了看到所有针对 sp:recompile 事件报告的可能导致重新编译的原因,请在 sql server 2005 上发出下列查询:
select v.subclass_name, v.subclass_value
from sys.trace_events e inner join sys.trace_subclass_values v
on e.trace_event_id = v.trace_event_id
where e.name = 'sp:recompile'
上述查询的输出如下。(仅输出不带有阴影的列;带有阴影的列用于提供其他详细信息。)
subclassname | subclassvalue | 重新编译的详细原因 |
schema changed | 1 | 架构、绑定或权限在编译和执行间被更改。 |
statistics changed | 2 | 统计被更改。 |
deferred compile | 3 | 因 dnr(延迟名称解析)导致重新编译。在编译时未找到对象,对运行时延迟检测。 |
set option change | 4 | 批处理中的 set 选项被更改。 |
temp table changed | 5 | 临时表架构、绑定或权限被更改。 |
remote rowset changed | 6 | 远程行集架构、绑定或权限被更改。 |
query notification environment changed | 8 | (sql server 2005 新增!) |
partition view changed | 9 | sql server 2005 有时将独立于数据的隐含谓词添加到一些索引视图中的查询的 where 子句。如果基础数据发生变化,那么这些隐含谓词将无效,而相关联的缓存查询计划需要重新编译。 (sql server 2005 新增!) |
在 sql server 2000 中,eventsubclass 列包含从 1 到 6 的整数值,意义与上表所列的内容相同。sql server 2005 新增了最后两个类别。
对本节所述的两个例子,sql server 2000 上的跟踪输出与 sql server 2005 相同,除了在 sql server 2000 上,eventsubclass 列包含“3”而非字符串“deferred compile”。从内部来说,语句级重新编译发生在 sql server 2005 上,因此,仅有“select * from #t1”在 sql server 2005 上进行重新编译,而在 sql server 2000 上,整个 demoproc1 都将被重新编译。
因混用 ddl 和 dml 而导致重新编译
在批处理或存储过程中混用数据定义语言 (ddl) 和数据操作语言 (dml) 语句并不是一个好办法,因为这会引起不必要的重新编译。下面这个例子运用存储过程阐述了这一点。(批处理也会发生同样的情况。但是,由于 sql server 2005 事件探查器没有提供必要的跟踪事件,因此无法对其进行实时观测。)创建下列存储过程。
drop procedure mixddldml
go
create procedure mixddldml as
create table tab1 (a int) -- ddl
select * from tab1 -- dml
create index nc_tab1idx1 on tab1(a) -- ddl
select * from tab1 -- dml
create table tab2 (a int) -- ddl
select * from tab2 -- dml
go
exec mixddldml
go
在事件探查器跟踪输出中,可观测到下列事件。
eventclass | textdata | eventsubclass |
sp:starting | exec mixddldml | |
sp:stmtstarting | -- mixddldml create table tab1 (a int)???????--ddl | |
sp:stmtstarting | -- mixddldml select * from tab1???-- dml | |
sp:recompile | deferred compile | |
sp:stmtstarting | -- mixddldml select * from tab1???-- dml | |
sp:stmtstarting | -- mixddldml create index nc_tab1idx1 on tab1(a)????-- ddl | |
sp:stmtstarting | -- mixddldml select * from tab1???-- dml | |
sp:recompile | deferred compile | |
sp:stmtstarting | -- mixddldml select * from tab1???-- dml | |
sp:stmtstarting | -- mixddldml create table tab2 (a int)???????--ddl | |
sp:stmtstarting | -- mixddldml select * from tab2???-- dml | |
sp:recompile | deferred compile | |
sp:stmtstarting | -- mixddldml select * from tab2???-- dml | |
sp:completed | exec mixddldml |
这里说明了 mixddldml 是如何编译的。
1. | 在首次编译(不是重新编译)mixddldml 时,将为其生成一个主干计划。因为不存在表 tab1 和 tab2,所以无法生成三个“select”语句的计划。主干计划包含两个“create table”语句和一个“create index”语句的计划。 |
2. | 当开始执行过程时,将创建表 tab1。由于不存在针对第一个“select * from tab1”的计划,因而将发生语句级重新编译。(在 sql server 2000 中,也将通过这一重新编译为第二个“select * from tabl”生成一个计划。) |
3. | 第二个“select * from tab1”将导致重新编译,因为还不存在相应查询的计划。在 sql server 2000 中,也会发生这类重新编译,但具体原因有所不同:由于在“tab1”上创建了非聚集索引,“tab1”的架构发生了变化。 |
4. | 接着,创建了“tab2”。“select * from tab2”引发了重新编译,因为不存在相应查询的计划。 |
总之,在这个例子中,sql server 2000 和 sql server 2005 中都发生了三次重新编译。但是,sql server 2005 的重新编译成本要低于 sql server 2000,因为前者属于语句级而非存储过程级重新编译。
如果根据下方所示来编写存储过程,那么将观察到有趣的现象。
create procedure ddlbeforedml as
create table tab1 (a int) -- ddl
create index nc_tab1idx1 on tab1(a) -- ddl
create table tab2 (a int) -- ddl
select * from tab1 -- dml
select * from tab1 -- dml
select * from tab2 -- dml
go
exec ddlbeforedml
go
在事件探查器跟踪输出中,可观察到下列事件。
eventclass | textdata | eventsubclass |
sp:starting | exec ddlbeforedml | |
sp:stmtstarting | -- ddlbeforedml create table tab1 (a int)???????-- ddl | |
sp:stmtstarting | -- ddlbeforedml create index nc_tab1idx1 on tab1(a)????-- ddl | |
sp:stmtstarting | -- ddlbeforedml create table tab2 (a int)???????-- ddl | |
sp:stmtstarting | -- ddlbeforedml select * from tab1???--dml | |
sp:recompile | deferred compile | |
sp:stmtstarting | -- ddlbeforedml select * from tab1???--dml | |
sp:stmtstarting | -- ddlbeforedml select * from tab1???--dml | |
sp:recompile | deferred compile | |
sp:stmtstarting | -- ddlbeforedml select * from tab1???--dml | |
sp:stmtstarting | -- ddlbeforedml select * from tab2???????????-- dml | |
sp:recompile | deferred compile | |
sp:stmtstarting | -- ddlbeforedml select * from tab2???????????-- dml | |
sp:completed | exec ddlbeforedml |
在 sql server 2005 中,出于语句级重新编译,仍会发生这三次重新编译。与 mixddldml 存储过程相比,重新编译的次数并没有减少。如果在 sql server 2000 上尝试相同的例子,重新编译的次数将从 3 次减少到 1 次。在 sql server 2000 中,重新编译在存储过程级上进行,因而可以一次性编译三个“select”语句。总之,与 sql server 2000 相比,sql server 2005 的重新编译工作量没有增加,但是重新编译的次数却增多了。
新闻热点
疑难解答