【搬家】【数据库】【优化】SQL 优化学习小结——索引和语句优化

时间:2022-04-26 23:25:49
本文最早于 2013年10月17日于本人个人博客(http://mooowooo.tk)发表,现博客搬家至此,转载请注明出处。


SQL 语句的优化对于每个与数据库打交道的程序员来说都是必不可少的一课,这里将我参加公司新人培训所学到的内容总结如下,希望对需要的朋友有所帮助。

本次学习以 Windows SQL Server 2008 SP2 版本为主,除某些细节特性外,也适用于其他的数据库。

首先是关于提高查询(Query)操作效率的问题。在对数据库的操作过程中,查询操作是我们最常进行的动作。我们知道数据存放在数据库中的表里,那么其实查询操作也就是针对数据表里内容的检索。我们需要了解的是,针对数据表的检索,有两种方式,一种是scan,即逐行扫描,另一种是 seek,即二分查找。只要对算法稍有了解的朋友就知道seek 的速度是远远快于 scan 的。但是要注意的是seek 仅仅适用于已经对其建立了索引(index)的数据表有效,并且针对建立索引的数据表,seek 查找也并非100%起效,暂且不表,我们待会儿再说。

那么,怎样对数据表建立索引呢?其实很简单,一般说来,如果一个表有对其设置主键(primary key),那么主键其实就已经默认是该表的索引了(并且一般来说这个索引十分高效)。当然你也可以用其他的列建立索引,具体的语句和操作可以很简单的搜索到,这里就不再赘述。

当然仅仅这样并不够,前面说过,就算建立起索引,也不一定就是使用 seek 查询,是否使用 seek 查询,跟我们所写的语句有密切关系。在这之前,我们还有一点关于索引的内容需要介绍一下。

索引其实分为两种,一种叫做 聚簇索引(如之前提到的主键索引),另一种叫做 非聚簇索引 。那么他们的区别是什么呢?举个例子,就好像一本中文字典,当我们要查字的时候,有两种办法查找,一种方法是根据拼音检索另一种方法是根据偏旁部首检索区别在于,拼音检索列表所对应到的内容页是连续的,而偏旁部首检索列表对应的内容页很大可能是非连续的。同样的,对索引来说,聚簇索引 就好比拼音检索,非聚簇索引 就好比偏旁部首检索。这样解释足够明白了吧。如果有兴趣的朋友可以做个实验,连续向数据表里塞入 100000条数据,然后用聚簇索引筛选出 top 100,看看是不是跟塞入的顺序一模一样。

另外,关于索引其实有几个指导性的意见,即:

1- 栏位越小越好。所谓栏位越小越好,其原因是数据保存数据时是以页(Page)进行划分的,让索引尽量在同一个页(Page)中,可以避免分页的过程增加性能消耗。
2- 尽量选用独立性高重复性低的列作为索引,有一个可以参考的公式:选择性=1-(符合条件的数据条目数/总数据条目数)*100%。这个值越高,说明这一列作为索引的价值越大
3- 索引的建立对检索数据有极大帮助,但是对于增删改操作却有阻碍。其实不难想象,每次增删改时不但要对数据本身进行操作,还要对数据的索引进行操作,自然要花费更大的代价
4- 可以用几个列一起,建立一个 复合索引。但是复合索引并不能取代独立索引,因为复合索引在使用过程中,如果只用其中某一列检索时,如果这列不是复合索引的第一个匹配列,那么复合索引并不起作用

好了,关于索引要了解的内容基本上说完了,那么怎样通过索引来提高查询操作的效率。前面说过,就算建立起了索引,也并非一定能提高效率,提高效率的核心还在于针对语句结构的优化。针对语句结构的优化不仅能提高查询操作的效率,增删改操作也都能得到相应的效率上的改善,Lock 也会减少。下面我们来详细说一说:

首先是在语句中怎样使用到强大的索引功能呢?首先要明确,索引起效的地方主要是在 where 子句。在摆条件的过程中,要尽量符合索引查询的要求,而进一步的要尽量符合索引seek 查询的要求。其核心思想是:不要直接在 where 子句里进行运算操作。执行运算操作将直接导致索引失效,因为运算的过程将会一次扫描数据表的每一条数据,如果数据表很大的话,那将是一个非常缓慢的过程。

那么所谓运算操作,具体有哪些呢?首先,最简单的是数字的运算字符串的连接分割等操作,以及诸如 left()ltrim() 等会对数据本身进行修改的操作都可以看作运算操作。那么如果遇到需要对数据进行处理后再执行查询的情况怎么办呢?我们完全可以使用临时变量来解决这个问题,如下面的SQL语句:

-- Type 1
select * from table_1 where TransDate=dbo.FormatDate(getdate(), ‘yyyymmdd’)
-- Type 2
set @TransDate=dbo.FormatDate(getdate(), ‘yyyymmdd’)
select * from table_1 where TransDate= @TransDate

其中 Type 1 的情况就进行了运算,大大降低了执行的效率,而 Type 2 中,在本地将运算过程执行完成,而仅仅将一个固定的值作为条件进行查询,这样一来,如果针对TransDate 列建立了索引的话,这列索引就能够发挥作用,大大提高了查询效率。

另一个要点是尽量避免在查询语句中使用到 负向查询 的操作,如!< ,!>,!=,<>,or,not,not in,in,not exist,not like,like ‘abc%’ 这类操作。因为这类操作也会将数据表的每一条数据都扫描一次,十分的耗费资源。当然,我们这里所说的都是数据量很大的情况,如果在数据量不大的时候,直接使用这些条件,包括之前说过的条件,也并无太大影响。

另外,在某些情况下,会出现语句无法优化的情况,但一般很少遇到这样的情况,偶尔一次,应该也不会有太大影响。一般我们怎么解决呢?针对 or 这个来说,在where 子句条件里没有全部做好索引的情况下,建议使用 union all 来替代,但不要使用 union,因为 union 在合并重复项的过程中仍旧会扫描全部条目,那么就失去优化的意义了。诸如此类的同等效果替换,大家可以再实践中慢慢领会,我这里也就不一一列举了,只写出几个常见的语句优化手段,来结束这一节的内容。

1- existin 效率高。
2- 针对连续的数据betweenin 效率高。
3- distinctorder by 等操作效率低下,应当尽量避免。
4- join 通常效率高于 subquery 但不绝对,可以用 exec plan 来确认。
5- union allunion 效率高。
6- 数量不大的情况下,用 declare @tablecreate #table 效率高,因为@table 的内容存在内存里,读写速度快于存在 tempdb 里的 #table,但是当数据量大的时候,会耗尽内存而适得其反
7- select into 容易导致 lock 系统表(sysobjectsysindexs等),尽量先delcare @tableinsert into 语句复合使用,如 insert into @table select * from table
8- 如果必须用 in 或者 or,可以用强制索引来改善性能:select * from talbe with(index=name) where sex = ‘man’ or age in (8, 32, 45)