众所周知,SQL Server是我们常见的关系型数据库之一。简单地写出一个存储过程实现businees功能并不是一件难事,难的是如何写出一个高效的存储过程,快速地得到我们想要的结果。也就是我们通常所说的高性能存储过程,或者说性能优化。在进行具体说明之前我们首先来看一下一个查询的执行流程是什么样的。
查询过程
SQL Server数据库引擎由两个主要部分组成,一个是存储引擎,一个是查询过程。前者主要负责磁盘和内存中进行的数据读取以及对数据一致性的保证。而后者则顾名思义,主要就是接受查询,设计出一个优化的计划,然后执行这个计划,并最终返回结果。
整个查询过程所做的事情可以用下图来表示:
当我们接收到一个查询语句的时候,第一件事就是解析这个查询语句,主要是看看他要查的是哪一个表啊,做得是一个什么join啊之类的。这一步也至少保证了我们的接收的查询是能够被解析的,否则我们也不能知道究竟要做些什么,这里的检查更多的是语法方面的检查。
在解析完成之后,就是进行绑定,这个步骤更多的是一个名字的对应处理,就是把上一步解析出来的内容和我们系统中真实存在一些object进行对应,顺便也检查这些object是否真实存在,从而进一步对查询的有效性进行验证,当然这里的检查就更多地是逻辑方面的检查了。
在绑定之后,我们就需要找到一个最优的执行计划,这里会有列出一些可能的执行计划,然后从中找到最优的执行计划。然而现实并不总是如我们所料,比如说一个查询语句,可能有几万,几十万设置几百万种执行计划,我们不可能遍历检查所有的执行计划,然后找出最优的执行计划。因为这个遍历也是要花时间的,所以这里会有一个trade off。因为我们只能说这里会找到一个局部最优解(想到了AI,哈哈)来设置执行计划。
有了查询优化后的结果,在后面两步中我们就不需要动太多的脑筋,只要执行这个查询,并返回结果就可以了。
了解了这样的整个过程之后,我们知道其实所有的关键就是查询优化这一块,为了让这一步能够选择出最优的执行计划,我们首先要来看一下它究竟会产生哪些执行计划,然后再来分析这些执行计划在我们的场景中是好还是不好,或者在我们的场景中哪些步骤占据了大的时间块,从而才可以给我们优化提供具体的建议。因此本文主要就是介绍SQL Server中的执行计划。
前期准备
本文有很多的测试来说明我们的讲解,所以需要大家准备一些软件和数据
SQL Server,我装的是SQL Server 2017
SQL Server sample data:https://github.com/Microsoft/sql-server-samples/releases,本文使用的是AdventureWorks-oltp-install-script.zip进行测试。
数据访问的操作
首先我们来看一下数据访问的操作,所谓数据访问就是直接访问数据,可以是访问一个表也可以是访问一个索引。通常有两种方法:一种是扫描(scan)一种是查找(seek)。扫描就是读取整个结构,可以访问一个heap或者一个clustered索引或者一个non-clustered索引。而查找则不会读取整个结构,他则是更高效地通过索引访问一行,所以从这个角度来看,查找就只能应用在索引上面了。简单总结如下表所示:
下面我们来看一下几个扫描的例子,在开始具体的例子之前,先说明一下如何得到执行计划,其实得到执行计划有很多种方法,一种最简单的方法就是SSMS中打开‘include actual exectution plan’选项,如下图所示:
全表扫描
这样我们就可以看到下面的执行计划:
我们可以清楚的看到这里就是一个全表的扫描。
Clustered index扫描
这样我们可以看到下面的执行计划:
这里有一点需要强调的是,尽管我们的clustered index在保存的时候是有序的,但并不保证我们通过他进行scan出来的结果也是有序排列的,因此如果你想要按照clustered index进行排序的结果,那么请显式加上order by的语句,如下图所示:
此时把鼠标放到clustered index scan的图标上可以看到如下结果:
这里我们可以看到在弹出的property窗口中的ordered属性被置为了true,这就表明我们的结果是有序的,而你要是返回去看看之前的这个属性,毫无疑问,它是false的。
Non-clustered index扫描
我们可以看到下面的结果
这个例子中,我们查询了addressId, city以及stateProvinceId三列,而我们查询的是IX_AddressLine1_AddressLine2_City_StateProvinceID_PostalCode,这个index是基于AddressLine1,AddressLine2,City,StateProvinceID和PostalCode这几列产生的,很显然city和stateProvinceId是处于这个index中的,但是addressID却不在其中,那我们究竟怎么得到这一列的呢?其实原因很简单,addressId是一个clustered index,所有的non clustered index都默认包含clustered index这一列,所以我们在设置non clustered index的时候没有必要显式包含clustered index的列。
上面所说的三种扫描都会扫描所有的结构,下面我们来看看查询的一些例子:
Clustered index查询
结果如下图所示:
Non-Clustered index查询
结果如下图所示:
对clustered index查询和non-clustered index查询来说,他们两者是差不多的,唯一的差别在于前者前者可以cover所有的column,而后者则只能特定的column,原因也很简单就是clustered index他在逻辑上就是根据clustering的key进行排序的。可惜的是一个表只能有一个clustered index。
书签查询(Bookmark lookup)
我们在上节最后说non-clustered index查询并不能cover所有的column,那如果我们查询的column有些在这个non-clustered index中,有些则不在,那这种时候该怎么办呢?有两种可能的选择,一种是先扫描non-clustered index,然后再去查询表格得到别的column,另外一种就是直接去扫描表格。这两种都是有可能的,具体情况可能需要具体分析。
使用方法一的例子:
结果如下图所示:
这里因为City和ModifiedDate不在non-clustered index中,我们选择的是先用这个index进行seek,然后再key lookup整个表。这里上面的index seek只会执行一遍,然后会执行n遍的下面的key lookup,n就是我们在index seek中找到的行数。把鼠标放到对应的执行计划中,我们可以看到Number of Executions分别是1(non-clustered index seek)和25 (key lookup).
使用方法二的例子:
同样的查询语句,不同的查询值也是可能有不同的执行计划的,我们这里把stateProviceID=1改成stateProviceID=20
执行结果如下图:
我们可以看到这里选择了一个基础表的扫描,而没有选择先去扫描nonclustered index的扫描,其实就这个简单例子来说,他主要是和输出的行数有关系的,我们不难想象,假如输出的函数够多,那我们就不如直接去扫描全表来得快了,大家可以自己去实验看这个输出行数的临界值是多少会影响到最招的执行计划。
上面的例子上,我们有一个clustered的index,有时我们还会进行heap的lookup,看下面这个例子:
结果如下:
至此,我们关于执行计划的第一部分的内容:数据访问操作以及书签查询就介绍完毕了,我们会在后续文章继续介绍别的执行计划的内容,敬请期待。
领取专属 10元无门槛券
私享最新 技术干货