SQL优化:索引优化

SQL索引

   SQL索引在数据库优化中占有一个非常大的比例, 一个好的索引的设计,可以让你的效率提高几十甚至几百倍,在这里将带你一步步揭开他的神秘面纱。

  1.1 什么是索引?

  SQL索引有两种,聚集索引和非聚集索引,索引主要目的是提高了SQL Server系统的性能,加快数据的查询速度与减少系统的响应时间 

下面举两个简单的例子:

        图书馆的例子:一个图书馆那么多书,怎么管理呢?建立一个字母开头的目录,例如:a开头的书,在第一排,b开头的在第二排,这样在找什么书就好说了,这个就是一个聚集索引,可是很多人借书找某某作者的,不知道书名怎么办?图书管理员在写一个目录,某某作者的书分别在第几排,第几排,这就是一个非聚集索引

        字典的例子:字典前面的目录,可以按照拼音和部首去查询,我们想查询一个字,只需要根据拼音或者部首去查询,就可以快速的定位到这个汉字了,这个就是索引的好处,拼音查询法就是聚集索引,部首查询就是一个非聚集索引.

    看了上面的例子,下面的一句话大家就很容易理解了:

  1. 聚集索引存储记录是物理上连续存在,而非聚集索引是逻辑上的连续,物理存储并不连续。
  2. 就像字段,聚集索引是连续的,a后面肯定是b,非聚集索引就不连续了,就像图书馆的某个作者的书,有可能在第1个货架上和第10个货架上。
  3. 还有一个小知识点就是:聚集索引一个表只能有一个,而非聚集索引一个表可以存在多个。

 

   1.2 索引的存储机制

    首先,无索引的表,查询时,是按照顺序存续的方法扫描每个记录来查找符合条件的记录,这样效率十分低下,举个例子,如果我们将字典的汉字随即打乱,没有前面的按照拼音或者部首查询,那么我们想找一个字,按照顺序的方式去一页页的找,这样效率有多底,大家可以想象。

       聚集索引和非聚集索引的根本区别表记录的排列顺序和与索引的排列顺序是否一致,其实理解起来非常简单,还是举字典的例子:如果按照拼音查询,那么都是从a-z的,是具有连续性的,a后面就是b,b后面就是c, 聚集索引就是这样的,他是和表的物理排列顺序是一样的,例如有id为聚集索引,那么1后面肯定是2,2后面肯定是3,所以说这样的搜索顺序的就是聚集索引。

        非聚集索引就和按照部首查询是一样是,可能按照偏房查询的时候,根据偏旁‘弓’字旁,索引出两个汉字,张和弘,但是这两个其实一个在100页,一个在1000页,(这里只是举个例子),他们的索引顺序和数据库表的排列顺序是不一样的,这个样的就是非聚集索引

      原理明白了,那他们是怎么存储的呢?在这里简单的说一下,聚集索引就是在数据库被开辟一个物理空间存放他的排列的值,例如1-100,所以当插入数据时,他会重新排列整个整个物理空间,而非聚集索引其实可以看作是一个含有聚集索引的表,他只仅包含原表中非聚集索引的列和指向实际物理表的指针。他只记录一个指针,其实就有点和堆栈差不多的感觉了

 

  1.3 什么情况下设置索引 

动作描述

使用聚集索引 

 使用非聚集索引

 外键列

 应

 应

 主键列

 应

 应

 列经常被分组排序(order by)

 应

 应

 返回某范围内的数据

 应

 不应

 小数目的不同值

 应

 不应

 大数目的不同值

 不应

 应

 频繁更新的列

不应 

 应

 频繁修改索引列

 不应

 应

 一个或极少不同值

 不应

 不应

 

  • 建立索引的原则:

1) 定义主键的数据列一定要建立索引。

2) 定义有外键的数据列一定要建立索引。

3) 对于经常查询的数据列最好建立索引。

4) 对于需要在指定范围内的快速或频繁查询的数据列;

5) 经常用在WHERE子句中的数据列。

6) 经常出现在关键字order by、group by、distinct后面的字段,建立索引。如果建立的是复合索引,索引的字段顺序要和这些关键字后面的字段顺序一致,否则索引不会被使用。

7) 对于那些查询中很少涉及的列,重复值比较多的列不要建立索引。

8) 对于定义为textimagebit的数据类型的列不要建立索引。

9) 对于经常存取的列避免建立索引 

9) 限制表上的索引数目。对一个存在大量更新操作的表,所建索引的数目一般不要超过3个,最多不要超过5个索引虽说提高了访问速度,但太多索引会影响数据的更新操作

10) 对复合索引,按照字段在查询条件中出现的频度建立索引。在复合索引中,记录首先按照第一个字段排序。对于在第一个字段上取值相同的记录,系统再按照第二个字段的取值排序,以此类推。因此只有复合索引的第一个字段出现在查询条件中,该索引才可能被使用,因此将应用频度高的字段,放置在复合索引的前面,会使系统最大可能地使用此索引,发挥索引的作用。

 

  • 索引的不足之处

上面都在说使用索引的好处,但过多的使用索引将会造成滥用。因此索引也会有它的缺点:

  1. 虽然索引大大提高了查询速度,同时却会降低更新表的速度,如对表进行INSERT、UPDATE和DELETE。因为更新表时,MySQL不仅要保存数据,还要保存一下索引文件。
  2. 建立索引会占用磁盘空间的索引文件。一般情况这个问题不太严重,但如果你在一个大表上创建了多种组合索引,索引文件的会膨胀很快。索引只是提高效率的一个因素,如果你的MySQL有大数据量的表,就需要花时间研究建立最优秀的索引,或优化查询语句。


使用索引时,有以下一些技巧和注意事项:

  • 索引不会包含有NULL值的列

只要列中包含有NULL值都将不会被包含在索引中,复合索引中只要有一列含有NULL值,那么这一列对于此复合索引就是无效的。所以我们在数据库设计时不要让字段的默认值为NULL。

  • 使用短索引

对串列进行索引,如果可能应该指定一个前缀长度。例如,如果有一个CHAR(255)的列,如果在前10个或20个字符内,多数值是惟一的,那么就不要对整个列进行索引。短索引不仅可以提高查询速度而且可以节省磁盘空间和I/O操作。

  • 索引列排序

MySQL查询只使用一个索引,因此如果where子句中已经使用了索引的话,那么order by中的列是不会使用索引的。因此数据库默认排序可以符合要求的情况下不要使用排序操作尽量不要包含多个列的排序,如果需要最好给这些列创建复合索引

  • like语句操作

一般情况下不鼓励使用like操作,如果非使用不可,如何使用也是一个问题。like “%aaa%” 不会使用索引而like “aaa%”可以使用索引

  • 不要在列上进行运算
 
1
select * from users where YEAR(adddate)<2007; 

将在每个行上进行运算,这将导致索引失效而进行全表扫描,因此我们可以改成

1
select * from users where adddate<‘2007-01-01’;  


  • 不使用NOT IN和<>操作


1.4 如何创建索引

  1.41 创建索引的语法:

 
1
CREATE [UNIQUE][CLUSTERED | NONCLUSTERED]  INDEX  index_name  
2
3
ON {table_name | view_name} [WITH [index_property [,....n]]
4
5
说明:
6
7
UNIQUE: 建立唯一索引。
8
9
CLUSTERED: 建立聚集索引。
10
11
NONCLUSTERED: 建立非聚集索引。
12
13
Index_property: 索引属性。
14
15
 UNIQUE索引既可以采用聚集索引结构,也可以采用非聚集索引的结构,如果不指明采用的索引结构,则SQL Server系统默认为采用非聚集索引结构。

1.42 删除索引语法:

 
1
DROP INDEX table_name.index_name[,table_name.index_name]
2
3
说明:table_name: 索引所在的表名称。
4
5
index_name : 要删除的索引名称。

1.43 显示索引信息:

使用系统存储过程:sp_helpindex 查看指定表的索引信息。

执行代码如下:

 
1
Exec sp_helpindex book1;

 1.44查询索引(均可) 

 
1
show index from table_name;
2
show keys from table_name;
3
desc table_Name;

  1.44组合索引

很多时候,我们在mysql中创建了索引,但是某些查询还是很慢,根本就没有使用到索引!一般来说,可能是某些字段没有创建索引,或者是组合索引中字段的顺序与查询语句中字段的顺序不符。

看下面的例子:
假设有一张订单表(orders),包含order_id和product_id二个字段。
一共有31条数据。符合下面语句的数据有5条。执行下面的sql语句:

 
1
select product_id
2
from orders
3
where order_id in (123, 312, 223, 132, 224);

这条语句要mysql去根据order_id进行搜索,然后返回匹配记录中的product_id。所以组合索引应该按照以下的顺序创建:

 
1
create index orderid_productid on orders(order_id, product_id)
2
mysql> explain select product_id from orders where order_id in (123, 312, 223, 132, 224) \G
3
*************************** 1. row ***************************
4
           id: 1
5
  select_type: SIMPLE
6
        table: orders
7
         type: range
8
possible_keys: orderid_productid
9
          key: orderid_productid
10
      key_len: 5
11
          ref: NULL
12
         rows: 5
13
        Extra: Using where; Using index
14
1 row in set (0.00 sec)
可以看到,这个组合索引被用到了,扫描的范围也很小,只有5行。如果把组合索引的顺序换成product_id, order_id的话,mysql就会去索引中搜索 *123 *312 *223 *132 *224,必然会有些慢了。
 
1
mysql> create index orderid_productid on orders(product_id, order_id);                                                      
2
Query OK, 31 rows affected (0.01 sec)
3
Records: 31  Duplicates: 0  Warnings: 0
4
 
5
mysql> explain select product_id from orders where order_id in (123, 312, 223, 132, 224) \G
6
 
7
*************************** 1. row ***************************
8
 
9
           id: 1
10
  select_type: SIMPLE
11
        table: orders
12
         type: index
13
possible_keys: NULL
14
          key: orderid_productid
15
      key_len: 10
16
          ref: NULL
17
         rows: 31
18
        Extra: Using where; Using index
19
1 row in set (0.00 sec)
这次索引搜索的性能显然不能和上次相比了。rows:31,我的表中一共就31条数据。索引被使用部分的长度:key_len:10,比上一次的key_len:5多了一倍。不知道是这样在索引里面查找速度快,还是直接去全表扫描更快呢?
 
1
mysql> alter table orders add modify_a char(255) default 'aaa';
2
Query OK, 31 rows affected (0.01 sec)
3
Records: 31  Duplicates: 0  Warnings: 0
4
 
5
mysql>
6
mysql>
7
mysql> explain select modify_a from orders where order_id in (123, 312, 223, 132, 224) \G         
8
*************************** 1. row ***************************
9
           id: 1
10
  select_type: SIMPLE
11
        table: orders
12
         type: ALL
13
possible_keys: NULL
14
          key: NULL
15
      key_len: NULL
16
          ref: NULL
17
         rows: 31
18
        Extra: Using where
19
1 row in set (0.00 sec)
这样就不会用到索引了。 刚才是因为select的product_id与where中的order_id都在索引里面的。


为什么要创建组合索引呢?这么简单的情况直接创建一个order_id的索引不就行了吗?果只有一个order_id索引,没什么问题,会用到这个索引,然后mysql要去磁盘上的表里面取到product_id。如果有组合索引的话,mysql可以完全从索引中取到product_id,速度自然会快。再多说几句组合索引的最左优先原则:
组合索引的第一个字段必须出现在查询组句中,这个索引才会被用到。果有一个组合索引(col_a,col_b,col_c),下面的情况都会用到这个索引:

 
1
col_a = "some value";
2
col_a = "some value" and col_b = "some value";
3
col_a = "some value" and col_b = "some value" and col_c = "some value";
4
col_b = "some value" and col_a = "some value" and col_c = "some value";
对于最后一条语句,mysql会自动优化成第三条的样子~~。下面的情况就不会用到索引:
 
1
col_b = "aaaaaa";
2
col_b = "aaaa" and col_c = "cccccc";

通过实例理解单列索引、多列索引以及最左前缀原则。实例:现在我们想查出满足以下条件的用户id:

 
1
mysql>SELECT `uid` FROM people WHERE lname`='Liu'  AND `fname`='Zhiqun' AND `age`=26


因为我们不想扫描整表,故考虑用索引。


1.单列索引:

 
1
ALTER TABLE people ADD INDEX lname (lname);


将lname列建索引,这样就把范围限制在lname='Liu'的结果集1上,之后扫描结果集1,产生满足fname='Zhiqun'的结果集2,再扫描结果集2,找到 age=26的结果集3,即最终结果。

由 于建立了lname列的索引,与执行表的完全扫描相比,效率提高了很多,但我们要求扫描的记录数量仍旧远远超过了实际所需 要的。虽然我们可以删除lname列上的索引,再创建fname或者age 列的索引,但是,不论在哪个列上创建索引搜索效率仍旧相似。

2.多列索引:

 
1
ALTER TABLE people ADD INDEX lname_fname_age (lame,fname,age);


为了提高搜索效率,我们需要考虑运用多列索引,由于索引文件以B-Tree格式保存,所以我们不用扫描任何记录,即可得到最终结果。

注:在mysql中执行查询时,只能使用一个索引,如果我们在lname,fname,age上分别建索引,执行查询时,只能使用一个索引,mysql会选择一个最严格(获得结果集记录数最少)的索引

3.最左前缀:顾名思义,就是最左优先,上例中我们创建了lname_fname_age多列索引,相当于创建了(lname)单列索引,(lname,fname)组合索引以及(lname,fname,age)组合索引。

注:在创建多列索引时,要根据业务需求,where子句中使用最频繁的一列放在最左边

建立索引的时机

到这里我们已经学会了建立索引,那么我们需要在什么情况下建立索引呢?一般来说,在WHERE和JOIN中出现的列需要建立索引,但也不完全如此,因为MySQL只对<,<=,=,>,>=,BETWEEN,IN,以及某些时候的LIKE才会使用索引。例如:

 
1
SELECT t.Name FROM mytable t LEFT JOIN mytable m ON t.Name=m.username WHERE m.age=20 AND m.city='郑州'

此时就需要对city和age建立索引,由于mytable表的userame也出现在了JOIN子句中,也有对它建立索引的必要。

刚才提到只有某些时候的LIKE才需建立索引。因为在以通配符%和_开头作查询时,MySQL不会使用索引。例如下句会使用索引:

 
1
SELECT * FROM mytable WHERE username like'admin%'

下句就不会使用:

 
1
SELECT * FROM mytable WHEREt Name like'%admin'

因此,在使用LIKE时应注意以上的区别。



1.5 索引实战(摘抄)

人们在使用SQL时往往会陷入一个误区,即太关注于所得的结果是否正确,而忽略了不同的实现方法之间可能存在的性能差异,

这种性能差异在大型的或是复杂的数据库环境中(如联机事务处理OLTP或决策支持系统DSS)中表现得尤为明显。

笔者在工作实践中发现,不良的SQL往往来自于不恰当的索引设计不充份的连接条件不可优化的where子句

在对它们进行适当的优化后,其运行速度有了明显地提高!

下面我将从这三个方面分别进行总结:

为了更直观地说明问题,所有实例中的SQL运行时间均经过测试,不超过1秒的均表示为(< 1秒)。

 

一、不合理的索引设计----

例:表record620000行,试看在不同的索引下,下面几个 SQL的运行情况:

---- 1.date上建有一非个群集索引

 
1
select count(*) from record where date >'19991201' and date < '19991214'and amount >2000 (25秒)
2
3
select date ,sum(amount) from record group by date(55秒)
4
5
select count(*) from record where date >'19990901' and place in ('BJ','SH') (27秒)
6
7
---- 分析:----
8
9
date上有大量的重复值,在非群集索引下,数据在物理上随机存放在数据页上,在范围查找时,必须执行一次表扫描才能找到这一范围内的全部行。

---- 2.date上的一个群集索引

 
1
select count(*) from record where date >'19991201' and date < '19991214' and amount >2000 (14秒)
2
3
select date,sum(amount) from record group by date(28秒)
4
5
select count(*) from record where date >'19990901' and place in ('BJ','SH')(14秒)
6
7
---- 分析:---- 在群集索引下,数据在物理上按顺序在数据页上,重复值也排列在一起,因而在范围查找时,可以先找到这个范围的起末点,且只在这个范围内扫描数据页,避免了大范围扫描,提高了查询速度。

---- 3.placedateamount上的组合索引

 
1
select count(*) from record where date >'19991201' and date < '19991214' and amount >2000 (26秒)
2
3
select date,sum(amount) from record group by date(27秒)
4
5
select count(*) from record where date >'19990901' and place in ('BJ, 'SH')(< 1秒)
6
7
---- 分析:---- 这是一个不很合理的组合索引,因为它的前导列是place,第一和第二条SQL没有引用place,因此也没有利用上索引;第三个SQL使用了place,且引用的所有列都包含在组合索引中,形成了索引覆盖,所以它的速度是非常快的。

---- 4.dateplaceamount上的组合索引

 
1
select count(*) from record where date >'19991201' and date < '19991214' and amount >2000(< 1秒)
2
3
select date,sum(amount) from record group by date(11秒)
4
5
select count(*) from record where date >'19990901' and place in ('BJ','SH')(< 1秒)
6
7
---- 分析:---- 这是一个合理的组合索引。它将date作为前导列,使每个SQL都可以利用索引,并且在第一和第三个SQL中形成了索引覆盖,因而性能达到了最优。

---- 5.总结:----

缺省情况下建立的索引是非群集索引,但有时它并不是最佳的;合理的索引设计要建立在对各种查询的分析和预测上。

一般来说:

.有大量重复值、且经常有范围查询(between, >,< >=,< =)和order bygroup by发生的列,可考虑建立群集索引;

.经常同时存取多列,且每列都含有重复值可考虑建立组合索引;

.组合索引要尽量使关键查询形成索引覆盖,其前导列一定是使用最频繁的列。

 

二、不充份的连接条件:

例:表card7896行,在card_no上有一个非聚集索引,表account191122行,在account_no上有一个非聚集索引,试看在不同的表连接条件下,两个SQL的执行情况:

 
1
select sum(a.amount) from account a,card b where a.card_no = b.card_no(20秒)
2
3
select sum(a.amount) from account a,card b where a.card_no = b.card_no and a.account_no=b.account_no(< 1秒)

---- 分析:---- 

  • 在第一个连接条件下,最佳查询方案是将account作外层表,card作内层表,利用card上的索引,其I/O次数可由以下公式估算为:外层表account上的22541+(外层表account191122*内层表card上对应外层表第一行所要查找的3页)=595907I/O
  • 在第二个连接条件下,最佳查询方案是将card作外层表,account作内层表,利用account上的索引,其I/O次数可由以下公式估算为:外层表card上的1944+(外层表card7896*内层表account上对应外层表每一行所要查找的4页)= 33528I/O

可见,只有充份的连接条件,真正的最佳方案才会被执行。

总结:

1.多表操作在被实际执行前,查询优化器会根据连接条件,列出几组可能的连接方案并从中找出系统开销最小的最佳方案。连接条件要充份考虑带有索引的表、行数多的表;内外表的选择可由公式:外层表中的匹配行数*内层表中每一次查找的次数确定,乘积最小为最佳方案。

2.查看执行方案的方法-- set showplanon,打开showplan选项,就可以看到连接顺序、使用何种索引的信息;想看更详细的信息,需用sa角色执行dbcc(3604,310,302)

 

三、不可优化的where子句

1.例:下列SQL条件语句中的列都建有恰当的索引,但执行速度却非常慢:

 
1
select * from record where substring(card_no,1,4)='5378'(13秒)
2
3
select * from record where amount/30< 1000(11秒)
4
5
select * from record where convert(char(10),date,112)='19991201'(10秒)
6
7
分析:
8
9
where子句中对列的任何操作结果都是在SQL运行时逐列计算得到的,因此它不得不进行表搜索,而没有使用该列上面的索引;
10
11
如果这些结果在查询编译时就能得到,那么就可以被SQL优化器优化,使用索引,避免表搜索,因此将SQL重写成下面这样:
12
13
select * from record where card_no like'5378%'(< 1秒)
14
15
select * from record where amount< 1000*30(< 1秒)
16
17
select * from record where date= '1999/12/01'(< 1秒)

你会发现SQL明显快起来!

2.例:表stuff200000行,id_no上有非群集索引,请看下面这个SQL

 
1
select count(*) from stuff where id_no in('0','1')(23秒)
2
3
分析:----
4
5
where条件中的'in'在逻辑上相当于'or',所以语法分析器会将in ('0','1')转化为id_no ='0' or id_no='1'来执行。
6
7
我们期望它会根据每个or子句分别查找,再将结果相加,这样可以利用id_no上的索引;
8
9
但实际上(根据showplan),它却采用了"OR策略",即先取出满足每个or子句的行,存入临时数据库的工作表中,再建立唯一索引以去掉重复行,最后从这个临时表中计算结果。因此,实际过程没有利用id_no上索引,并且完成时间还要受tempdb数据库性能的影响。
10
11
实践证明,表的行数越多,工作表的性能就越差,当stuff有620000行时,执行时间竟达到220秒!还不如将or子句分开:
12
13
select count(*) from stuff where id_no='0' select count(*) from stuff where id_no='1'
14
15
得到两个结果,再作一次加法合算。因为每句都使用了索引,执行时间只有3秒,在620000行下,时间也只有4秒。
16
17
或者,用更好的方法,写一个简单的存储过程:
18
19
create proc count_stuff asdeclare @a intdeclare @b intdeclare @c intdeclare @d char(10)beginselect @a=count(*) from stuff where id_no='0'select @b=count(*) from stuff where id_no='1'endselect @c=@a+@bselect @d=convert(char(10),@c)print @d
20
21
直接算出结果,执行时间同上面一样快!

 

---- 总结:---- 

可见,所谓优化即where子句利用了索引,不可优化即发生了表扫描或额外开销。

1.任何对列的操作都将导致表扫描,它包括数据库函数、计算表达式等等,查询时要尽可能将操作移至等号右边

2.inor子句常会使用工作表,使索引失效;如果不产生大量重复值,可以考虑把子句拆开;拆开的子句中应该包含索引。

3.善于使用存储过程,它使SQL变得更加灵活和高效

从以上这些例子可以看出,SQL优化的实质就是在结果正确的前提下,用优化器可以识别的语句,充份利用索引,减少表扫描的I/O次数,尽量避免表搜索的发生。其实SQL的性能优化是一个复杂的过程,上述这些只是在应用层次的一种体现,深入研究还会涉及数据库层的资源配置、网络层的流量控制以及操作系统层的总体设计。


部分引用地址:http://blog.csdn.net/gprime/article/details/1687930


你可能感兴趣的:(SQL优化:索引优化)