前言html
前面咱们的几篇文章介绍了一系列关于运算符的介绍,以及各个运算符的优化方式和技巧。其中涵盖:查看执行计划的方式、几种数据集经常使用的链接方式、联合运算符方式、并行运算符等一系列的咱们常见的运算符。有兴趣的童鞋能够点击查看。数据库
本篇咱们介绍关于子查询语句的一系列内容,子查询通常是咱们造成复杂查询的一些基础性操做,因此关于子查询的应用方式就很是重要。并发
废话少说,开始本篇的正题。dom
技术准备工具
数据库版本为SQL Server2008R2,利用微软的一个更简洁的案例库(Northwind)进行分析。oop
1、独立的子查询方式post
所谓的独立的子查询方式,就是说子查询和主查询没有相关性,这样带来的好处就是子查询不依赖于外部查询,因此能够独立外部查询而被评估,造成本身的执行计划执行。性能
举个例子学习
SELECT O1.OrderID,O1.Freight FROM Orders O1 WHERE O1.Freight> ( SELECT AVG(O2.Freight) FROM Orders O2 )
这句SQL执行的目标是查询订单中运费大于平均运费数的订单。优化
这里提取平均运费的子句就是一个彻底独立的子查询,彻底不依赖主查询而独立执行。同时这里咱们这里利用利用一个标量计算(AVG),所以正好返回一行。
查看一下该语句的查询计划:
这个查询计划没啥好介绍的,关于子查询的执行计划造成能够参照个人第二篇:SQL Server调优系列基础篇(经常使用运算符总结)
不过这里须要提示一下就是,关于流聚合和计算标量造成的结果值(AVG)只包含一个结果值,因此该语句能正常的执行。
咱们再来看另一种状况
SELECT O.OrderID FROM Orders O WHERE O.CustomerID= ( SELECT C.CustomerID FROM Customers C WHERE C.ContactName=N'Maria Anders' )
该语句的也是获取名字为'Maria Anders'的顾客有多少订单。这句T-SQL语句可否执行的前提是在顾客表里存不存在同名的“'Maria Anders'”顾客,若是存在同名状况,该语句就不能正确执行,而若是恰巧只有一名顾客为'Maria Anders',则能正常执行。
咱们来分析一下对于这种执行的时候才能判断可否正确执行的SQL Server如何判断的
在这里出现了一个新的运算符,名字是:断言。咱们用文本执行计划来查看一下,这个运算符的主要功能是什么
通过上面的分析,咱们已经分析出了上面的“断言”运算符的做用,由于咱们的子查询语句不能保证返回的结果为一行,因此,这里引入了一个断言运算符来作判断。
因此,断言的做用就是根据下文的条件,判断子查询句的查询结果是否知足主语句的查询要求。
若是,断言发现子语句不知足,就会直接报错,好比上面的Expr1005>1
而且,断言运算符还常常用来检测其它条件是否知足,好比:约束条件、参数类型、值长度等。
其实,这里断言要解决的问题就是判断咱们的筛选条件中ContactName中的值是否存在重复值的,对于这种判断相对性能消耗仍是比较小的,有时候对于别的复杂的断言操做须要消耗大量资源,因此咱们就能够根据适当状况状况避免断言操做。
好比,上面的语句咱们能够明确的告诉SQL Server在表Customers中ContactName列就不存在重复值,它就不须要断言了。咱们在上面创建一个:惟1、非汇集索引实现
CREATE UNIQUE INDEX ContactNameIndex ON Customers(ContactName) GO SELECT O.OrderID FROM Orders O WHERE O.CustomerID= ( SELECT C.CustomerID FROM Customers C WHERE C.ContactName=N'Maria Anders' ) drop index Customers.ContactNameIndex GO
通过咱们惟一非汇集索引的提示,SQL Server已经明确的知道咱们的子查询语句不会返回多行的状况,因此就去掉了断言操做。
2、相关的子查询方式
相比上面的独立子查询方式,这里的相关的子查询方式相对复杂点,就是咱们的子查询依赖于主查询的的结果,对于这种子查询就不能单独执行。
咱们来看个这样的子查询例子
SELECT O1.OrderID FROM Orders O1 WHERE O1.Freight> ( SELECT AVG(O2.Freight) FROM Orders O2 WHERE O2.OrderDate<O1.OrderDate )
这个语句就是返回以前订单中运费量大于平均值的顶点编号。
语句很简单的逻辑,可是这里面的子查询就依赖于主查询的结果项,筛选条件中 WHERE O2.OrderDate<O1.OrderDate,因此这个子查询就不能独立运行。
咱们来看一下这个语句的执行计划
这里的查询计划有出现了一个新的运算符:索引假脱机。
其实,关于索引假脱机的做用主要是用于子查询的独立运行,由于咱们知道这里的子查询的查询条件是依赖于主查询的,因此,这里想运行的话就的先提早获取出主查询的结果项,而这里获取的主查询的结果项须要一个中间表来暂存,这里暂存的工具就是:(索引池)Index Spool,而对这个索引池的操做,好比:新建、增长等操做就是上面咱们所标示的“索引假脱机”了。
索引假脱机分为两种:Eager Spool和Lazy Spool,其实简单点讲就是需不须要马上将结果存入Index Spool里面,仍是经过延迟操做。
而这里造成的索引池(Index Spool)是存放于系统的临时库Tempdb中。
咱们经过文本查询计划,来分析下两个索引假脱机里面的值是什么
通过上面的分析,咱们已经看到了,里面的Eager Spool是和主查询比较造成的结果值,由于这个必需要及时的造成,以便于子查询的进行,因此它的类型为Eager Spool,
而子查询外面的那个Index Spool为Lazy Spool,这个结果项的保存不须要那么及时了,这个保存的就是子查询的造成的结果项了,就是相对每一个订单运费的平均值。
我上面的分析,但愿各位看官能看懂了。
其实,关于这个Index Spool的设计的目的,彻底为了就是提高性能,由于咱们知道上面的查询语句每一个子查询的进行,都必须回调主查询的结果,因此为了不每次都回调,就采用了Index Spool进行暂存,而这个Index Spool存储的位置就是Tempdb,因此Tempdb运行的快慢直接关乎这种查询语句的性能。
这也是咱们为何强调大并发的数据库搭建,建议将Tempdb库单独存放于高性能的硬件环境中。
晒晒联机丛书中关于假脱机数据运算符官方介绍:
Index Spool 物理运算符在 Argument 列中包含 SEEK:() 谓词。Index Spool 运算符扫描其输入行,将每行的副本放置在隐藏的假脱机文件(存储在 tempdb 数据库中且只在查询的生存期内存在)中,并为这些行建立非汇集索引。这样可使用索引的查找功能来仅输出那些知足 SEEK:() 谓词的行。
若是重绕该运算符(例如经过 Nested Loops 运算符重绕),但不须要任何从新绑定,则将使用假脱机数据,而不用从新扫描输入。
跟索引脱机相似的还有一个类似的运算符:表脱机,其功能相似,表脱机存储的应该是键值列,而表脱机则是存储的是多列数据了。
来看例子
SELECT O1.OrderID,O1.Freight FROM Orders O1 WHERE O1.Freight> ( SELECT AVG(O2.Freight) FROM Orders O2 WHERE O2.CustomerID=O1.CustomerID )
这个查询和上面的相似,只不过是查询的同一个客户加入的超过全部订单运费平均值的订单。
此语句一样不是独立的子查询语句,每一个子查询的结果的造成都须要依赖主查询的结果项,为了加快速度,提高性能,SQL Server会将主表查询的的结果项暂存到一张临时表中,这个表就被称为表脱机
咱们来看这句话的执行计划:
这里就用到了一个表脱机的运算符,这个运算符的做用就是用来暂存后面扫描获取的结果集合,用于下面的子查询的应用
这个表脱机造成的结果项也是存储到临时库Tempdb中,因此它的应用和前面提到的索引脱机相似。
上面的执行计划中,还提到了一个新的运算符:段(Segment)
这个运算符的解释是:
Segment 既是一个物理运算符,也是一个逻辑运算符。它基于一个或多个列的值将输入集划分红多个段。这些列显示为 Segment 运算符中的参数。而后运算符每次输出一个段。
其实做用就是将结果进行汇总整理,将相同值汇聚到一块儿,跟排序同样,只不过这里能够对多列值进行汇聚。
咱们再来看一个例子,加深 一下关于段运算的做用
SELECT CustomerID,O1.OrderID,O1.Freight FROM Orders O1 WHERE O1.Freight= ( SELECT MAX(O2.Freight) FROM Orders O2 WHERE O2.CustomerID=O1.CustomerID )
这个语句查询的是:每一个顾客所产生的最大运费的订单数据。
以上语句,若是理解起来有难度,咱们能够变通如下的相同逻辑的T-SQL语句,相同的逻辑
SELECT O1.CustomerID,O1.OrderID,O1.Freight FROM Orders O1 INNER JOIN ( SELECT CustomerID,max(Freight) Freight FROM Orders GROUP BY CustomerID ) AS O2 ON O1.CustomerID=O2.CustomerID AND O1.Freight=O2.Freight
先根据客户编号分组,而后获取出最大的运费项,再关联主表获取订单信息。
以上两种语句生成的相同的查询计划:
这里咱们来解释一下,SQL Server的强大之处,也是段运算符使用的最佳方式。
原本这句话要实现,按照逻辑须要有一个嵌套循环链接,参照上面的方式,使用表脱机的方式进行数据的获取。
可是,咱们这句话获取的结果项是每一个顾客的最大运费的订单明细项,并且CustomerID列做为输出项,因此这里采用了,先按照运费列(Freight)排序,
而后采用段运算符进行将每一个顾客相同的数据汇聚到一块儿,而后再输出每一个顾客的前一列(TOP 1)获取的就是最每一个顾客的运费最大的订单项。
省去了任何的表假脱机、索引假脱机、关联链接等一系列复杂的操做。
SQL Server看来这种智能化的操做仍是挺强的。
咱们再来分析SQL Server关于子查询这块的智能特性,由于通过上面的分析经过对比,相关的子查询语句在运行时须要更多的消耗:
一、有时候须要经过索引假脱机(Index Spool)、表脱机(Table Spool)进行中间结果项的暂存,而这一过程的中间项须要建立、增长、删除、销毁等操做都须要消耗大量的内存和CPU
二、关于相关子查询中以上提到的中间项的造成都是位于Tempdb临时库中,有时候会增大Tempdb的空间,增长Tempdb库的消耗、页争用等问题。
因此,要避免上面的问题,最好的方式是避免使用相关子查询,尽可能使用独立子查询进行操做。
固然,SQL Server一样提供了自动转换的功能,智能的去分析语句,避免相关的子查询操做进行:
来看一个稍差的写法:
SELECT o.OrderID FROM Orders O WHERE EXISTS ( SELECT c.CustomerID FROM Customers C WHERE C.City=N'Londom' AND C.CustomerID=O.CustomerID )
上面的语句,咱们写的是相关的子查询操做,可是在执行计划中造成的确实独立的子查询,这样从而避免相关的子查询所带来的性能消耗。
其实上面语句,相对好的写法是以下
SELECT o.OrderID FROM Orders O WHERE O.CustomerID IN ( SELECT c.CustomerID FROM Customers C WHERE C.City=N'Londom' )
这样所造成的就是彻底独立的子查询,这也是SQL Server要执行的意图。因此这个语句造成的查询计划是和上面的查询计划同样。
这里的优化所有得益于SQL Server的智能化。
可是咱们在写语句的时候,须要本身了解,掌握好,这样才能写出高效的T-SQL语句。
参考文献
结语
本篇篇幅有点长,可是介绍的子查询内容也还不是很全,后续慢慢的补充上,咱们写的SQL语句中不少都涉及到子查询,因此这块应用仍是挺广泛的。到本篇文章关于平常调优的T-SQL中的查询语句常常用到的一些运算符基本介绍全了,固然,还有一些别的增删改一系列的运算符,这些平常生活中咱们通常不采用查询计划调优,后续咱们的文章会将这些运算符也添加上,以供参考之用。
在完成本系列关于查询计划相关的调优以后,我打算将数据库有关统计信息这块也作一个详细的分析介绍。由于统计信息是支撑SQL Server评估最优执行计划的最重要的决策点,
因此统计信息的重要性不言而喻。有兴趣的童鞋能够提早关注。
关于SQL Server性能调优的内容涉及面很广,后续文章中依次展开分析。
有问题能够留言或者私信,随时恭候有兴趣的童鞋加入SQL SERVER的深刻研究。共同窗习,一块儿进步。
文章最后给出上几篇的链接,看来有必要整理一篇目录了.....
若是您看了本篇博客,以为对您有所收获,请不要吝啬您的“推荐”。