SQLServer调优系列基础篇(索引运算总结)
前⾔
上⼏篇⽂章我们介绍了如何查看查询计划、常⽤运算符的介绍、并⾏运算的⽅式,有兴趣的可以点击查看。
本篇将分析在SQL Server中,如何利⽤先有索引项进⾏查询性能优化,通过了解这些索引项的应⽤⽅式可以指导我们如何建⽴索引、调整我们的查询语句,达到性能优化的⽬
的。
闲⾔少叙,进⼊本篇的正题。
技术准备
基于SQL Server2008R2版本,利⽤微软的⼀个更简洁的案例库(Northwind)进⾏解析。
简介
所谓的索引应⽤就是在我们⽇常写的T-SQL语句中,如何利⽤现有的索引项,再分析的话就是我们所写
的查询条件,其实⼤部分情况也⽆⾮以下⼏种:
1、等于谓词:select ...lumn=@parameter
2、⽐较谓词:select ...lumn> or < or  <> or <= or >= @parameter
3、范围谓词:select ...lumn in or not in  or between and @parameter
4、逻辑谓词:select ...⼀个谓词 or、and 其它谓词 or、and 更多谓词....
我们就依次分析上⾯⼏种情况下,如何利⽤索引进⾏查询优化的
⼀、动态索引查
所谓的动态索引查就是SQL Server在执⾏语句的时候,才格式化查询条件,然后根据查询条件的不同⾃动的去匹配索引项,达到性能提升的⽬的。
来举个例⼦
SET SHOWPLAN_TEXT ON
GO
SELECT OrderID
FROM Orders
WHERE ShipPostalCode IN (N'05022',N'99362')
因为我们在表Orders的列ShipPostalCode列中建⽴了⾮聚集索引列,所以这⾥查询的计划利⽤了索引查的⽅式。这也是需要建⽴索引的地⽅。
我们来利⽤⽂本的⽅式来查看该语句的详细的执⾏计划脚本,语句⽐较长,我⽤记事本换⾏,格式化查看
我们知道这张表的该列⾥存在⼀个⾮聚集索引,所以在查询的时候要尽量使⽤,如果通过索引扫描的⽅式消耗就⽐较⼤了,所以SQL Server尽量想采取索引查的⽅式,其实IN
关键字和OR关键字逻辑是⼀样的。
于是上⾯的查询条件就转换成了:
[Northwind].[dbo].[Orders].[ShipPostalCode]=N'05022'
OR
[Northwind].[dbo].[Orders].[ShipPostalCode]=N'99362'
这样就可以采⽤索引查了,先查第⼀个结果,然后再查第⼆个,⽽这个过程在SQL Server中就被称为:动态索引查。
是不是有点智能的感觉了....
所以有时候我们写语句的时候,尽量要使⽤SQL Server的这点智能了,让其能⾃动的查到索引,提升性能。
有时候偏偏我们写的语句让SQL Server的智能消失,举个例⼦:
--参数化查询条件
DECLARE @Parameter1 NVARCHAR(20),@Parameter2 NVARCHAR(20)
SELECT @Parameter1=N'05022',@Parameter2=N'99362'
SELECT OrderID
FROM Orders
WHERE ShipPostalCode IN (@Parameter1,@Parameter2)
我们将这两个静态的筛序值改成参数,有时候我们写的存储过程灰常喜欢这么做!我们来看这种⽅式的⽣成的查询计划
本来很简单的⼀个⾮聚集索引查搞定的执⾏计划,我们只是将这两个数值没有直接写⼊IN关键字中,⽽是利⽤了两个变量来代替。
看看上⾯SQL Server⽣成的查询计划!...这都是些啥???还⽤起来嵌套循环,我就查询了⼀个Orders表...你嵌套循环个啥....上⾯动态索引查的能⼒去哪了???
好吧,我们⽤⽂本查询计划来查看下,这个简单的语句到底在⼲些啥...
|--Nested Loops(Inner Join, OUTER REFERENCES:([Expr1009], [Expr1010], [Expr1011]))
|--Merge Interval
|    |--Sort(TOP 2, ORDER BY:([Expr1012] DESC, [Expr1013] ASC, [Expr1009] ASC, [Expr1014] DESC))
|        |--Compute Scalar(DEFINE:([Expr1012]=((4)&[Expr1011]) = (4) AND NULL = [Expr1009], [Expr1013]=(4)&[Expr1011], [Expr1014]=(16)&[Expr1011]))
|              |--Concatenation
|                  |--Compute Scalar(DEFINE:([Expr1004]=[@Parameter2], [Expr1005]=[@Parameter2], [Expr1003]=(62)))
|                  |    |--Constant Scan
|                  |--Compute Scalar(DEFINE:([Expr1007]=[@Parameter1], [Expr1008]=[@Parameter1], [Expr1006]=(62)))
|                        |--Constant Scan
|--Index Seek(OBJECT:([Northwind].[dbo].[Orders].[ShipPostalCode]), SEEK:([Northwind].[dbo].[Orders].[ShipPostalCode] > [Expr1009] AND [Northwind].[dbo].[Orders].[ShipPostalCode] < [Expr1010]) ORDERED FORWARD)挺复杂的是吧,其实我分析了⼀下脚本,关于为什么会⽣成这个计划脚本的原因,是为了解决如下⼏个问题:
1、前⾯我们写的脚本在IN⾥⾯写的是两个常量值,并且是不同的值,所以形成了两个索引值的查通过OR关键字组合,
这种⽅式貌似没问题,但是我们将这两个数值变成了参数,这就引来了新的问题,假如这两个参数我们输⼊的是相等的,那么利⽤前⾯的执⾏计划就会⽣成如下
[Northwind].[dbo].[Orders].[ShipPostalCode]=N'05022'
OR
[Northwind].[dbo].[Orders].[ShipPostalCode]=N'05022'
这样执⾏产⽣的输出结果就是2条⼀样的输出值!...但是表⾥⾯确实只有1条数据...所以这样输出结果不正确!
所以变成参数后⾸先解决的问题就是去重问题,2个⼀样的变成1个。
2、上⾯变成参数,还引⼊了另外⼀个问题,加⼊我们两个值有⼀个传⼊的为Null值,或者两个都为Null值,同样输出结果⾯临着这样的问题。所以这⾥还要解决的去Null值的问题。
为了解决上⾯的问题,我们来粗略的分析⼀下执⾏计划,看SQL Server如何解决这个问题的
简单点将就是通过扫描变量中的值,然后将内容进⾏汇总值,然后在进⾏排序,再将参数中的重复值去掉,这样获取的值就是⼀个正确的值,最后拿这些去重后的参数值参与到嵌套循环中,和表Orders进⾏索引查。
但是分析的过程中,有⼀个问题我也没看明⽩,就是最好的经过去重之后的常量汇总值,⽤来嵌套循环连接的时候,在下⾯的索引查的时候的过滤条件变成了 and  查
我将上⾯的最后的索引查条件,整理如下:
|--Index Seek(OBJECT:([Northwind].[dbo].[Orders].[ShipPostalCode]), SEEK:
(
[Northwind].[dbo].[Orders].[ShipPostalCode] > [Expr1009]
AND
[Northwind].[dbo].[Orders].[ShipPostalCode] < [Expr1010]
) ORDERED FORWARD)
这个地⽅怎么搞的?我也没弄清楚,还望有看明⽩童鞋的稍加指导下....
好了,我们继续
上⾯的执⾏计划中,提到了⼀个新的运算符:合并间隔(merge interval operator)
我们来分析下这个运算符的作⽤,其实在上⾯我们已经在执⾏计划的图中标⽰出该运算符的作⽤了,去掉重复值。
其实关于去重的操作有很多的,⽐如前⾯⽂章中我们提到的各种去重操作。
这⾥怎么⼜冒出个合并间隔去重?其实原因很简单,因为我们在使⽤这个运算符之前已经对结果进⾏了排序操作,排序后的结果项重复值是紧紧靠在⼀起的,所以就引⼊了合并间隔的⽅式去处理,这样性能是最好的。
更重要的是合并间隔这种运算符应⽤场景不仅仅局限于重复值的去除,更重要的是还应⽤于重复区间的去除。
来看下⾯的例⼦
--参数化查询条件
DECLARE @Parameter1 DATETIME,@Parameter2 DATETIME
SELECT @Parameter1='1998-01-01',@Parameter2='1998-01-04'
SELECT OrderID
FROM ORDERS
WHERE OrderDate BETWEEN @Parameter1 AND DATEADD(DAY,6,@Parameter1)
OR OrderDate BETWEEN @Parameter2 AND DATEADD(DAY,6,@Parameter2)
我们看看这个⽣成的查询计划项
可以看到,SQL Server为我们⽣成的查询计划,和前⾯我们写的语句是⼀模⼀样的,当然我们的语句也没做多少改动,改动的地⽅就是查询条件上。
我们来分析下这个查询条件:
WHERE OrderDate BETWEEN @Parameter1 AND DATEADD(DAY,6,@Parameter1)
OR OrderDate BETWEEN @Parameter2 AND DATEADD(DAY,6,@Parameter2)
很简单的筛选条件,要获取订单⽇期在1998-01-01开始到1998-01-07内的值或者1998-01-04开始到1998-01-10内的值(不包含开始⽇期)
这⾥⽤的逻辑谓词为:OR...其实也就等同于我们前⾯写的IN
但是我们这⾥再分析⼀下,你会发现这两个时间段是重叠的
这个重复的区间值,如果⽤到前⾯的直接索引查,在这段区间之内的搜索出来的范围值就是重复的,所以为了避免这种问题,SQL Server⼜引⼊了“合并间隔”这个运算符。
其实,经过上⾯的分析,我们已经分析出这种动态索引查的优缺点了,有时候我们为了避免这种复杂的执⾏计划⽣成,使⽤最简单的⽅式就是直接传值进⼊语句中(当然这⾥需要重编译),当然⼤部分的情况我们写的程序都是只定义的参数,然后进⾏的运算。可能带来的⿇烦就是上⾯的问题,当然有时候参数多了,为了合并间隔所应⽤的排序就消耗的内存就会增长。怎么使⽤,根据场景⾃⼰酌情分析。
⼆、索引联合
所谓的索引联合,就是根据就是根据筛选条件的不同,拆分成不同的条件,去匹配不同的索引项。
举个例⼦
SELECT OrderID
FROM ORDERS
WHERE OrderDate BETWEEN '1998-01-01' AND '1998-01-07'
OR ShippedDate BETWEEN '1998-01-01' AND '1998-01-07'
这段代码是查询出订单中的订单⽇期在1998年1⽉1⽇到1998年1⽉7⽇的或者发货⽇期同样在1998年1⽉1⽇到1998年1⽉7⽇的。
逻辑很简单,我们知道在这种表⾥⾯这两个字段都有索引项。所以这个查询在SQL Server中就有了两个选择:
1、⼀次性的来个索引扫描根据匹配结果项输出,这样简单有效,但是如果订单表数据量⽐较⼤的话,
性能就会很差,因为⼤部分数据就根本不是我们想要的,还要浪费时间去扫描。
2、就是通过两列的索引字段直接查获取这部分数据,这样可以直接减少数据表的扫描量,但是带来的问题就是,如果分开扫描,有⼀部分数据就是重复的:那些同时在1998年1⽉1⽇到1998年1⽉7⽇的订单,发货⽇期也在这段时间内,因为两个扫描项都包含,所以再输出的时候需要将这部分重复数据去掉。
我们来看SQL Server如何选择
看来SQL Server经过评估选择了第2中⽅法。但是上⾯的⽅法也不尽完美,采⽤去重操作耗费了64%的资源。
其实,上⾯的⽅法,我们根据⽣成的查询计划可以变通的使⽤以下逻辑,其效果和上⾯的语句是⼀样的,并且⽣成的查询计划也⼀样
SELECT OrderID
FROM ORDERS
WHERE OrderDate BETWEEN '1998-01-01' AND '1998-01-07'
UNION
SELECT OrderID
FROM ORDERS
WHERE  ShippedDate BETWEEN '1998-01-01' AND '1998-01-07'
我们再来看⼀个索引联合的例⼦
SELECT OrderID
FROM ORDERS
WHERE OrderDate = '1998-01-01'
OR ShippedDate = '1998-01-01'
我们将上⾯的Between and不等式筛选条件改成等式筛选条件,我们来看⼀下这样形成的执⾏计划
基本相同的语句,只是我们改变了不同的查询条件,但是⽣成的查询计划还是变化蛮⼤的,有⼏点不同之处:
1、前⾯的⽤and  的筛选条件,通过索引查返回的值进⾏组合是⽤的串联的⽅式,所谓的串联就是两个数据集拼凑在⼀起就⾏,⽆所谓顺序连接什么的。
2、前⾯的⽤and  的筛选条件,通过串联拼凑的结果集去重的⽅式,是排序去重(Sort Distinct)...并且耗费了⼤量的资源。这⾥采⽤了流聚合来⼲这个事,基本不消耗我们来分析以下产⽣着两点不同的原因有哪些:
⾸先、这⾥改变了筛选条件为等式连接,所通过索引查所产⽣的结果项是排序的,并且按照我们所要查询的OrderID列排序,因此在两个数据集进⾏汇总的时候,正适合合并连接的条件!需要提前排序。所以这⾥最优的⽅式就是采⽤合并连接!
那么前⾯我们⽤and  的筛选条件通过索引查获取的结果项也是排序的,但是这⾥它没有按照OrderID排序,它是按照OrderDate或者ShippedDate列排序的,⽽我们的结果是要OrderID列,所以这⾥的排序是没⽤的......所以SQL Server只能选择⼀个串联操作,将结果汇聚到⼀起,然后在排序了......我希望这⾥我已经讲明⽩了...
其次、关于去重操作,毫⽆疑问采⽤流聚合(Aggregate)这种⽅式最好,消耗内存少,速度⼜快...但是前提是要提前排序...前⾯选⽤的排序去重(Sort Distinct)纯属⽆奈之举...
总结下:我们在写语句的时候能确定为等式连接,最好采⽤等式连接。还有就是如果能确定输出条件的最好能写⼊,避免多余的书签查,还有万恶的SELEECT *....
如果写了万恶的SELECT *...那么你所写的语句基本上就可以和⾮聚集索引查告别了....顶多就是聚集索引扫描或者RID查...
瞅瞅以下语句
SELECT *
FROM ORDERS
WHERE OrderDate = '1998-01-01'
OR ShippedDate = '1998-01-01'
最后,奉上⼀个AND的⼀个连接谓词的操作⽅式,这个⽅式被称为:索引交叉,意思就是说如果两个或多个筛选条件如果采⽤的索引是交叉进⾏的,那么使⽤⼀个就可以进⾏查询。
来看个语句就明⽩了
sql优化的几种方式
SELECT OrderID
FROM ORDERS
WHERE OrderDate = '1998-01-01'
AND ShippedDate = '1998-03-05'
这⾥我们采⽤了的谓词连接⽅式为AND,所以在实际执⾏的时候,虽然两列都存在⾮聚集索引,理论都可以使⽤,但是我们只要选⼀个最优的索引进⾏查,另外⼀个直接使⽤书签查出来就可以。省去了前⾯介绍的各种神马排序去重....流聚合去重....等等不⼈性的操作。
看来AND连接符是⼀个很帅的运算符...所以很多时候我们在尝试写OR的情况下,不如换个思路改⽤AND更⾼效。
参考⽂献
微软联机丛书
参照书籍《SQL.Server.2005.技术内幕》系列
结语
此篇⽂章主要介绍了索引运算的⼀些⽅式,主要是描述了我们平常在写语句的时候所应⽤的⽅式,并且举了⼏个例⼦,算作抛砖引⽟吧,其实我们平常所写的语句中⽆⾮也就本篇⽂章中介绍的各种⽅式的更改,拼凑。⽽且根据此,我们该怎样建⽴索引也作为⼀个指导项。
下⼀篇我们介绍⼦查询⼀系列的内容,有兴趣可提前关注,关于SQL Server性能调优的内容涉及⾯很⼴,后续⽂章中依次展开分析。
有问题可以留⾔或者私信,随时恭候有兴趣的童鞋加⼊SQL SERVER的深⼊研究。共同学习,⼀起进步。
⽂章最后给出上⼏篇的连接,看来有必要整理⼀篇⽬录了.....
如果您看了本篇博客,觉得对您有所收获,请不要吝啬您的“推荐”。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。