一. 索引的设计


 


(一)建立“适当”的索引,是快速查询的基础。


 


索引( index )是除表之外另一重要的、用户定义的存储在物理介质上的数据结构。当根据索引码的值搜索数据时,索引提供了对数据的快速访问。事实上,没有索引,数据库也能根据 Select 语句成功地检索到结果,但随着表变得越来越大,使用 “ 适当 ” 的索引的效果就越来越明显。注意,在这句话中,我们用了 “ 适当 ” 这个词,这是因为,如果使用索引时不认真考虑其实现过程,索引既可以提高也会破坏数据库的工作性能。


索引实际上是一种特殊的目录, SQL SERVER 提供了两种索引:
1. 聚集索引( clustered index ,也称聚类索引、簇集索引)


我们把这种正文内容本身就是一种按照一定规则排列的目录称为“聚集索引”。


例如: 汉语字典中按拼音查某一个字,就是使用“聚集索引”,实际上,你根本用不着查目录,直接在字典正文里找,就能很快找到需要的汉字(假设你知道发音)。


2. 非聚集索引( nonclustered index ,也称非聚类索引、非簇集索引)


我们把目录纯粹是目录,正文纯粹是正文的排序方式称为“非聚集索引”。


例如: 汉语字典中按部首查某一个字,部首目录和正文一定要刻意的通过页码才能联系到一起,其顺序不是天然一致的。


 


聚集索引与非聚集索引的 最大区别就在于:聚集索引是对原数据表进行排序,因此只要符合索引条件,就能够直接连续的读取数据记录,几乎可以达到对数据表的零扫描;而非聚集索引仅仅只是另外建了一张索引表,取数据的时候,从索引表取得结果后,还需要到指针所指的数据行读取相应数据,因此,在性能上,聚集索引会大大优于非聚集索引。
  但是在一张表中,聚集索引只允许一个,是比较宝贵的,因此要尽可能的用于那些使用频率最高的索引上。 另外,查询时必需用到索引的起始列,否则索引无效。另外,起始列也必需是使用频繁的列,那样的索引性能才会达到最优化。


 


(二)表:何时应使用聚集索引或非聚集索引

动作描述                         使用聚集索引 使用非聚集索引
列经常被分组排序                         ○                            ○
返回某范围内的数据                      ○                    
一个或极少不同值                                       
小数目的不同值                             ○                    
大数目的不同值                                                            ○
频繁更新的列                                                                ○
外键列                                              ○                            ○
主键列                                             ○                            ○
频繁修改索引列                                                            ○


(三)索引建立的一些注意项
1 、不要把聚集索引浪费在主键上,除非你只按主键查询
虽然 SQL SERVER 默认是在主键上建立聚集索引的,但实际应用中,这样做比较浪费。通常,我们会在每个表中都建立一个 ID 列,以区分每条数据,并且这个 ID 列是自动增大的,步长一般为 1 。此时,如果我们将这个列设为主键, SQL SERVER 会将此列默认为聚集索引。这样做有好处,就是可以让您的数据在数据库中按照 ID 进行物理排序,但这样做实用价值不大。
从我们前面谈到的聚集索引的定义我们可以看出,使用聚集索引的最大好处就是能够根据查询要求,迅速缩小查询范围,避免全表扫描。在实际应用中,因为 ID 号是自动生成的,我们并不知道每条记录的 ID 号,所以我们很难在实践中用 ID 号来进行查询。这就使让 ID 号这个主键作为聚集索引成为一种资源浪费。聚集索引相对与非聚集索引的优势是很明显的,而每个表中只能有一个聚集索引的规则,这使得聚集索引变得更加宝贵,应该用在其他查询频率高的字段上。其次,让每个 ID 号都不同的字段作为聚集索引也不符合 “ 大数目的不同值情况下不应建立聚合索引 ” 规则;当然,这种情况只是针对用户经常修改记录内容,特别是索引项的时候会负作用,但对于查询速度并没有影响。

2 、索引的建立要根据实际应用需求来进行
并非是在任何字段上简单地建立索引就能提高查询速度。聚集索引建立的规则大致是“既不能绝大多数都相同,又不能只有极少数相同”。举个例子,在公文表的收发日期字段上建立聚合索引是比较合适的。在政务系统中,我们每天都会收一些文件,这些文件的发文日期将会相同,在发文日期上建立聚合索引对性能的提升应该是相当大的。在群集索引下,数据物理上按顺序存于数据页上,重复值也排列在一起,因而在范围查找时,可以先找到这个范围的起末点,且只在这个范围内扫描数据页,避免了大范围扫描,提高了查询速度。
另一个相反的例子:比如在雇员表的“性别”列上只有“男”与“女”两个不同值,因此就完全没必要建立索引。

3 、在聚集索引中加入所有需要提高查询速度的字段,形成复合索引
根据一些实验的结果,我们可以得出一些可供参考的结论:
ü 仅用复合聚集索引的起始列作为查询条件和同时用到复合聚集索引的全部列的查询,速度是几乎一样的,甚至比后者还要快(在查询结果集数目一样的情况下);
ü 仅用复合聚集索引的非起始列作为查询条件的话,
这个索引是不起任何作用的。
ü 复合聚集索引的所有列都用上,而且因为查询条件严格,查询结果少的话,会形成“索引覆盖”,性能可以达到最优。
ü 最重要的一点:无论是否经常使用复合聚合索引的其他列,其起始列一定要是使用最频繁的列。

4. 根据实践得出的一些其他经验,特定情况下有效
ü 用聚合索引比用不是聚合索引的主键速度快;
ü 用聚合索引比用一般的主键作 order by 速度快,特别是在小数据量情况;
ü 使用聚合索引内的时间段,搜索时间会按数据占整个数据表的百分比成比例减少,而无论聚合索引使用了多少个;
ü 日期列不会因为有分秒的输入而减慢查询速度;
ü 由于改变一个表的内容,将会引起索引的变化。频繁的 insert,update,delete 语句将导致系统花费较大的代价进行索引更新,引起整体性能的下降。一般来讲,在对查询性能的要求高于对数据维护性能要求时,应该尽量使用索引,否则,就要慎重考虑一下付出的代价。在某些极端情况下,可先删除索引,再对数据库表更新大量数据,最后再重建索引,新建立的索引总是比较好用。

二、 编写优化的 SQL 语句,充分利用索引
下面就某些 SQL 语句的 where 子句编写中需要注意的问题作详细介绍。在这些 where 子句中,即使某些列存在索引,但是由于编写了劣质的 SQL ,系统在运行该 SQL 语句时也不能使用该索引,而同样使用全表扫描,这就造成了响应速度的极大降低。
SQL 语句在提交给数据库进行操作前,都会经过查询分析阶段, SQLSERVER 内置的查询优化器会分析查询条件的的每个部分,并判断这些条件是否符合扫描参数( SARG )的标准。只有当一个查询条件符合 SARG 的标准,才可以通过预先设置的索引,提升查询性能。
SARG 的定义:用于限制搜索操作的一种规范,通常是指一个特定的匹配,一个确定范围内的匹配或者两个以上条件的 AND 连接。一般形式如下:
列名 操作符 < 常数 或 变量 >

< 常数 或 变量 > 操作符 列名
列名可以出现在操作符的一边,而常数或变量出现在操作符的另一边。如:
Name=’ 张三 ’
价格 >5000
5000< 价格
Name=’ 张三 ’ and 价格 >5000
如果一个表达式不能满足 SARG 的形式,那它就无法限制搜索的范围了,也就是说 SQL SERVER 必须对每一行都判断它是否满足 Where 子句中的所有条件,既进行全表扫描。所以,一个索引对于不满足 SARG 形式的表达式来说是无用的 , 如:当查询条件为“价格 *2 >5000” 时,就无法利用建立在价格字段上的索引 。
SQLSERVER 内置了查询优化器,能将一些条件自动转换为符合 SARG 标准,如:将 “ 价格 *2 >5000” 转换为 “ 价格 >2500/2 ”,以达到可以使用索引的目的,但这种转化不是 100% 可靠的,有时会有语义上的损失,有时转化不了。如果对 “ 查询优化器 ” 的工作原理不是特别了解,写出的 SQL 语句可能不会按照您的本意进行查询。所以不能完全依赖查询优化器的优化,建议大家还是利用自己的优化知识,尽可能显式的书写出符合 SARG 标准的 SQL 语句,自行确定查询条件的构建方式,这样一方面有利于查询分析器分析最佳索引匹配顺序,另一方面也有利于今后重读代码。
介绍完 SARG 后,我们再结合一些实际运用中的例子来做进一步的讲解:
1 、 Like 语句是否属于 SARG 取决于使用 % 通配符的样式
如: name like ‘ 张 %’ ,这就属于 SARG
而: name like ‘% 张 ’ , 就不属于 SARG
通配符 % 在字符串首字符的使用会导致索引无法使用,虽然实际应用中很难避免这样用,但还是应该对这种现象有所了解,至少知道此种用法性能是很低下的。
2 、 “ 非 ” 操作符不满足 SARG 形式,使得索引无法使用
不满足 SARG 形式的语句最典型的情况就是包括非操作符的语句,如: NOT 、 != 、 <> 、 !< 、 !> 、 NOT EXISTS 、 NOT IN 、 NOT LIKE 等。
下面是一个 NOT 子句的例子:
   ... where not (status ='valid')
   not 运算符也隐式的包含在另外一些逻辑运算符中,比如 <> 运算符。见下例:
   ... where status <>'invalid';

  再看下面这个例子:
   select * from employee where salary<>3000;
  对这个查询,可以改写为不使用 not :
   select * from employee where salary<3000 or salary>3000;

  虽然这两种查询的结果一样,但是第二种查询方案会比第一种查询方案更快些。第二种查询允许对 salary 列使用索引,而第一种查询则不能使用索引。

3 、 函数运算不满足 SARG 形式,使得索引无法使用
例:下列 SQL 条件语句中的列都建有恰当的索引,但执行速度却非常慢:
select * from record where substring(card_no,1,4)=′5378′(13 秒 )
select * from record where amount/30< 1000 ( 11 秒)
select * from record where convert(char(10),date,112)=′19991201′ ( 10 秒)
分析:
where 子句中对列的任何操作结果都是在 SQL 运行时逐列计算得到的,因此它不得不进行全表扫描,而没有使用该列上面的索引;如果这些结果在查询编译时就能得到,那么就可以被 SQL 优化器优化,使用索引,避免表搜索,因此将 SQL 重写成下面这样:
select * from record where card_no like ′5378%′ ( < 1 秒)
select * from record where amount < 1000*30 ( < 1 秒)
select * from record where date= ′ 1999/12/01′ ( < 1 秒)

你会发现 SQL 明显快很多

4 、 尽量不要对建立了索引的字段,作任何的直接处理
select * from employs where first_name + last_name ='beill cliton';
无法使用索引

改为:
select * from employee where
first_name = substr('beill cliton',1,instr('beill cliton',' ')-1)
and
last_name = substr('beill cliton',instr('beill cliton',' ')+1)
则可以使用索引
5 、 不同类型的索引效能是不一样的,应尽可能先使用效能高的
比如:数字类型的索引查找效率高于字符串类型,定长字符串 char , nchar 的索引效率高于变长字符串 varchar,nvarchar 的索引。
应该将
where username=' 张三 ' and age>20
改进为
where age>20 and username=' 张三 '
注意:

此处, SQL 的查询分析优化功能可以做到自动重排条件顺序,但还是建议预先手工排列好。

6 、 尽量不要使用 is null 与 is not null 作为查询条件
  任何包含 null 值的列都将不会被包含在索引中,如果某列数据中存在空值,那么对该列建立索引的性能提升是值得怀疑的,尤其是将 null 作为查询条件的一部分时。建议一方面避免使用 is null 和 is not null, 另一方面不要让数据库字段中存在 null, 即使没有内容,也应利用缺省值,或者手动的填入一个值,如 :’’ 空字符串。

7 、 某些情况下 IN 的作用与 OR 相当 ,且都不能充分利用索引
例:表 stuff 有 200000 行, id_no 上有非群集索引,请看下面这个 SQL :
select count(*) from stuff where id_no in(′0′,′1′) ( 23 秒)

where 条件中的 ′in′ 在逻辑上相当于 ′or′ ,所以语法分析器会将 in (′0′,′1′) 转化为 id_no =′0′ or id_no=′1′ 来执行。我们期望它会根据每个 or 子句分别查找,再将结果相加,这样可以利用 id_no 上的索引;但实际上 , 它却采用了 "OR 策略 " ,即先取出满足每个 or 子句的行,存入临时数据库的工作表中,再建立唯一索引以去掉重复行,最后从这个临时表中计算结果。因此,实际过程没有利用 id_no 上索引,并且完成时间还要受 tempdb 数据库性能的影响。
实践证明,表的行数越多,工作表的性能就越差,当 stuff 有 620000 行时,执行时间会非常长!如果确定不同的条件不会产生大量重复值,还不如将 or 子句分开:
select count(*) from stuff where id_no=′0′
select count(*) from stuff where id_no=′1′
得到两个结果,再用 union 作一次加法合算。因为每句都使用了索引,执行时间会比较短,
select count(*) from stuff where id_no=′0′
union
select count(*) from stuff where id_no=′1′
从实践效果来看,使用 union 在通常情况下比用 or 的效率要高的多,而 exist 关键字和 in 关键字在用法上类似,性能上也类似,都会产生全表扫描,效率比较低下,根据未经验证的说法, exist 可能比 in 要快些。

8 、 使用变通的方法提高查询效率
   like 关键字支持通配符匹配,但这种匹配特别耗时。例如: select * from customer where zipcode like “21_ _ _” ,即使在 zipcode 字段上已建立了索引,在这种情况下也可能还是采用全表扫描方式。如果把语句改为: select * from customer where zipcode > “ 21000” ,在执行查询时就会利用索引,大大提高速度。但这种变通是有限制的,不应引起业务意义上的损失,对于邮政编码而言, zipcode like “21_ _ _” 和 zipcode > “ 21000” 意义是完全一致的。
9 、 组合索引的高效使用
假设已在 date , place , amount 三个字段上建立了组合索引
select count(*) from record
where date > ′ 19991201′ and date < ′ 19991214′ and amount > 2000
(< 1 秒 )

select date,sum(amount) from record group by date
( 11 秒)

select count(*) from record
where date > ′ 19990901′ and place in (′BJ′,′SH′)
( < 1 秒)
这是一个设置较合理的组合索引。它将 date 作为前导列,使每个 SQL 都可以利用索引,并且在第一和第三个 SQL 中形成了索引覆盖,因而性能达到了最优。如果索引不便于更改,修正 SQL 中的条件顺序以配合索引顺序也是可行的。
10 、 order by 按聚集索引列排序效率最高
排序是较耗时的操作,应尽量简化或避免对大型表进行排序,如缩小排序的列的范围,只在有索引的列上排序等等。
我们来看:( gid 是主键, fariqi 是聚合索引列)
select top 10000 gid,fariqi,reader,title from tgongwen
用时: 196 毫秒。 扫描计数 1 ,逻辑读 289 次,物理读 1 次,预读 1527 次。
select top 10000 gid,fariqi,reader,title from tgongwen order by gid asc
用时: 4720 毫秒。 扫描计数 1 ,逻辑读 41956 次,物理读 0 次,预读 1287 次。
select top 10000 gid,fariqi,reader,title from tgongwen order by gid desc
用时: 4736 毫秒。 扫描计数 1 ,逻辑读 55350 次,物理读 10 次,预读 775 次。
select top 10000 gid,fariqi,reader,title from tgongwen order by fariqi asc
用时: 173 毫秒。 扫描计数 1 ,逻辑读 290 次,物理读 0 次,预读 0 次。
select top 10000 gid,fariqi,reader,title from tgongwen order by fariqi desc
用时: 156 毫秒。 扫描计数 1 ,逻辑读 289 次,物理读 0 次,预读 0 次。
从以上我们可以看出,不排序的速度以及逻辑读次数都是和“ order by 聚集索引列 ” 的速度是相当的,但这些都比“ order by 非聚集索引列 ” 的查询速度是快得多的。
同时,按照某个字段进行排序的时候,无论是正序还是倒序,速度是基本相当的。


三、 关于节省数据查询系统开销方面的措施
1 、 使用 TOP 尽量减少取出的数据量
TOP 是 SQL SERVER 中用来提取前几条或前某个百分比数据的关键词。
select top 20 gid,fariqi,reader,title from tgongwen order by gid desc
select top 60 percent gid,fariqi,reader,title from tgongwen order by gid desc
在实际的应用中,应该经常利用 top 剔除掉不必要的数据,只保留必须的数据集合。这样不仅可以减少数据库逻辑读的次数,还能避免不必要的内存浪费,对系统性能的提升都是有好处的。

2 、 字段提取要按照“需多少、提多少”的原则,避免“ select *”
这个举个例子:
select top 10000 gid,fariqi,reader,title from tgongwen order by gid desc
用时: 4673 毫秒
select top 10000 gid,fariqi,title from tgongwen order by gid desc
用时: 1376 毫秒
select top 10000 gid,fariqi from tgongwen order by gid desc
用时: 80 毫秒
由此看来,字段大小越大,数目越多, select 所耗费的资源就越多,比如取 int 类型的字段就会比取 char 的快很多。我们每少提取一个字段,数据的提取速度就会有相应的提升。提升的幅度根据舍弃的字段的大小来判断。
3 、 count(*) 与 count( 字段 ) 方法比较
我们来看一些实验例子( gid 为 Tgongwen 的主键):
select count(*) from Tgongwen
用时: 1500 毫秒
select count(gid) from Tgongwen
用时: 1483 毫秒
select count(fariqi) from Tgongwen
用时: 3140 毫秒
select count(title) from Tgongwen
用时: 52050 毫秒
从以上可以看出,用 count(*) 和用 count( 主键 ) 的速度是相当的,而 count(*) 却比其他任何除主键以外的字段汇总速度要快,而且字段越长,汇总速度就越慢。如果用 count(*) , SQL SERVER 会自动查找最小字段来汇总。当然,如果您直接写 count( 主键 ) 将会来的更直接些。
4 、 有嵌套查询时,尽可能在内层过滤掉数据
如果一个列同时在主查询和 where 子句中出现,很可能当主查询中的列值改变之后,子查询必须重新查询一次。而且查询嵌套层次越多,效率越低,因此应当尽量避免子查询。如果子查询不可避免,那么要在子查询中过滤掉尽可能多的行。
5 、 多表关联查询时,需注意表顺序,并尽可能早的过滤掉数据
在使用 Join 进行多表关联查询时候,应该使用系统开销最小的方案。连接条件要充份考虑带有索引的表、行数多的表,并注意优化表顺序;说的简单一点,就是尽可能早的将之后要做关联的数据量降下来。
一般情况下, sqlserver 会对表的连接作出自动优化。例如:
select name,no from A
join B on A. id=B.id
join C on C.id=A.id
where name='wang'
尽管 A 表在 From 中先列出,然后才是 B, 最后才是 C 。但 sql server 可能会首先使用 c 表。它的选择原则是相对于该查询限制为单行或少数几行,就可以减少在其他表中查找的总数据量。绝大多数情况下, sql server 会作出最优的选择,但如果你发觉某个复杂的联结查询速度比预计的要慢,就可以使用 SET FORCEPLAN 语句强制 sql server 按照表出现顺序使用表。如上例加上: SET FORCEPLAN ON…….SET FORCEPLAN OFF 表的执行顺序将会按照你所写的顺序执行。在查询分析器中查看 2 种执行效率,从而选择表的连接顺序。 SET FORCEPLAN 的缺点是只能在存储过程中使用。

小结:
Ø 聚集索引比较宝贵,应该用在查询频率最高的地方;
Ø 在数据为 “ 既不是绝大多数相同,也不是极少数相同 ” 状态时,
最能发挥聚集索引的潜力;
Ø 复合索引的设置和使用要注意保持顺序一致;
Ø 条件子句的表达式最好符合 SARG 规范,是可利用索引的;
Ø 任何对列的操作都导致全表扫描,如数据库函数、计算表达式等,
查询时应尽可能将操作移至等号的某一边;
Ø 要注意含有 null 值时,是不能充分利用索引的;
Ø exist, in 、 or 等子句常会使索引失效;
如果不产生大量重复值,可以考虑把子句拆开,再用 union 拼合;
Ø 排序时应充分利用带索引的字段;
Ø 尽可能早,快的过滤掉无用的数据,只将必须的数据带到后续的操作中去
从前面讲叙的内容可以看出, SQL 语句优化的实质就是在结果正确的前提下,用分析优化器可以识别的 SARG 规范语句,充份利用索引,减少数据的 I/O 次数,尽量避免全表扫描的发生。
以上内容有些是指导性的理论原则,有些是实际摸索的经验,大家在使用时应灵活处理,根据实际情况,选择合适的方法。本文中列举的实验数据仅作比对用,不具备普遍意义。大家在实际项目中,应充分利用性能监测和分析工具(如 SQLSERVER 带的相关工具)来检验自己的优化效果。
此外,还有很重要的一点要提醒大家,同样复杂的数据操作,在 SQLSERVER 数据库级别完成的代价要远远小于在应用端用程序代码完成的代价,所以建议大家全面,深入的学习 SQL 语法中重要关键字的应用,如: Group By , Having 等,尽量把数据操作任务放在数据库系统中完成 。数据库应用系统的性能优化是一个复杂的过程,上述这些只是在 SQL 语句层次的一种体现,深入研究还会涉及数据库层的资源配置、网络层的流量控制以及操作系统层的总体设计等等,这些将在以后的文章中详细论述