本文属于SQL Server T-SQL执行内幕系列
接上文,当解析和编译完成后,请求的生命周期就进入下一步——优化(Optimisation)。在SQL语言中,优化的本质就是找最好的路线。意思是在多种可能的候选数据访问方式中选择最佳一个。比如两表关联的简单查询语句,每个表有1个索引,那么就有4种可能的数据访问方式(AB两表的索引扫描、AB两表的索引查找,A扫描B查找、A查找B扫描)。随着表和索引的数量增长可以预估得到可能的方式呈指数级增长。如果再考虑JOIN算法如(嵌套循环nested loop、哈希联接hash、合并联接merge)等,那复杂度将可能超乎你想象。
SQL Server及主流关系型数据库管理系统都使用基于开销/成本的优化(cost based optimizer,CBO),意味着优化器会考虑尽可能多的候选方案,然后计算出它们的预估开销,最后选择最小开销的那个执行计划作为实际执行之用。根据网上资料显示,N个表关联,理论上最少要分析N!次,最多可达(2N-2)!/(N-1)!次,也就是6表关联可能需要720次分析,10表关联就可以达到3628万次。所以本人认为的优化的核心结论就是——少。其中一个就是关联数量尽可能少。但是如何才算少,没有标准,It depends!
这个开销成了“性能”的根本,SQL Server并没有公布官方计算公式,但是从使用和多年来专家们的测试,开销的计算主要来自于:每个表的大小、列值的分布(这两个方面可以通过统计信息获得)、当前CPU的消耗、执行计划中操作符的所需内存等,这些计算结果最终会算入执行计划每个操作符的具体开销,然后逐级汇总到整个执行计划的总体开销。优化器选择的依据就是这个总体开销。关于统计信息,可以看一下我的另外几篇文章:
1. 什么东西导致了执行计划的严重错误——需要更新统计信息吗?
说明:执行计划由一系列的操作符(如果使用图形化执行计划就是那些图标)组成,每个操作符都完成一定的操作(包含数据访问、数据关联、数据处理等)。
但是候选执行计划的开销的预估非常复杂而且因为候选数量的巨大导致优化器不可能总是预估所有的可能方案,所以很多时候优化器在一个负载很重的服务器中,并不能选择真正的最佳执行计划。另外为了避免编译优化这类高CPU开销操作,SQL Server会尽可能把选择的执行计划存储在内存的一个专门存储中(Plan cache),下次执行时会先检查是否存在对应的计划缓存,没有再进行优化。
关于计划缓存可以查阅官方文档:https://msdn.microsoft.com/zh-cn/library/ms181055.aspx
性能优化的本质简单来说,就是通过在合理的资源使用前提下,尽可能缩短响应时间。注意两个关键词:合理的资源使用及响应时间。很多人以响应时间为唯一的性能指标,但是试想一下(实际也出现过),当某个语句写法A运行时间5秒且执行频率低,但是几乎占据了服务器所有资源导致其他操作无法进行。写法B运行时间10秒,但是占用资源很少,其他操作没有明显受影响。这种情况下,如果要权衡得失,我会选择后者,虽然单个操作慢一点,但是如果能在可接受的范围且并非核心操作,那么要以大局为重。
本人曾经优化过这么一套系统,由于最大并行度没有控制,有些高开销语句一执行就把CPU占满,导致其他操作全部等待。并且非常频繁。整个系统的 响应时间很久。通过简单地配置最大并行度及优化long running queries。系统马上顺畅,当然后续还有很多的优化工作比如索引、写法、设计、服务器配置等。
更多细节在本人的《SQL Server性能优化与管理的艺术》一书第八章。
总结
从本质上来说,解析、编译和优化过程,是把逻辑操作树的操作映射到物理操作中,然后由存储引擎来执行。优 化的产物是一个执行计划,包含了 一系列物理操作符的树结构。从理论上来说,为了找到最佳执行方案,优化器需要产生所有可能的执行计划并正确地评估每个执行计划的开销。但是哪怕简单的语句都可能产生数千甚至百万级的执行计划,为了权衡得失,优化器会作出取舍,比如如果优化器使用1秒来找到一个执行计划只需要1分钟完成执行,那么就不会再去检查是否还有更好的执行计划。
在优化器根据基数预估找到认为最佳的执行计划之后,就会把执行计划进行缓存并传入执行引擎按照这个“路径”去执行并返回结果。同时生成的执行计划会放到内存的缓存中(计划缓存,plan cache),下一次执行时,如果该缓存的执行计划可用,则跳过优化部分直接使用,减少开销,否则就重新按照基数预估进行新的执行计划生成。