SQL Server的索引对用户和T-SQL开发者来说几乎是透明的。除非用户使用表提示(table hints)来强制优化器使用某个具体的索引,否则查询中的索引不能被具体指定。
通常情况下,基于索引键直方图的值,SQL Server的基于代价的优化器从I/O角度会选择代价最小的索引。
Chapter35会详细讲述优化器如何评估I/O,并决定采用最有效的查询计划。本节主要讲述一些创建有用索引的主要的指导原则,使得优化器能够有效地使用这些索引。
设计索引的一些通用的指南包括以下几点:
- 对于组合索引,尽量保持索引的越靠左边的列越具有高的选择性。索引中的第一列应该最具有唯一性(如果可能),并且索引列的顺序总体上应该具有从高到低的唯一性。然而,记住如果索引的第一列没有在SARG或join子句中出现,那么选择性将不会有任何帮助。为了确保索引能够被大多数查询使用,第一列应该是查询中最常用的列。
-
- 确保join中使用的列具有索引。如果join中的列上没有索引,Join的处理将是低效的。记住:一个PRIMARY KEY约束将自动会在一个列上创建索引,但是FOREIGN KEY约束不会的。如果你的查询通常在表的primary Key 和foreign key之间进行join操作,那么你将会在foreign key上创建索引。
-
- 为你的最关键查询和事务来调整索引。你不能为每个可能的查询都创建索引。然而,如果你能识别你的关键和最常用的查询,并为它们创建索引,那么你的应用将运行地更好。SQL Profile是一个识别你的最常用查询的有用工具。SQL Profie也能识别那些运行慢的查询。
-
- 避免列上的索引具有低选择性。优化器将不可能使用这些索引,它们只是占有些空间并且在插入、删除、修改时增加了以一些不必的负担而已。当索引能覆盖一个查询则是一个例外。索引覆盖将在后面详细介绍。
-
- 小心选择你的聚集和非聚集索引。接下来的两个小节将讨论给出一些选择聚集和非聚集索引的技巧和指南。这些都基于对列上包含的数据和该列上所执行的查询类型来给出的。
-
聚集索引指示(Cluster Index Indications)
借助聚集索引来搜索行总比借助非聚集索引来搜索行快主要有两个原因。原因一是聚集索引只包含了一个指向页的指针而不是指向单个数据行的指针;所以,一个聚集索引比非聚集索引更紧凑。因为聚集索引更小并且不需要额外的书签查找来发现匹配的行,而且比相似定义的非聚集索引可通过更少的页的读操作来发现行。第二个原因是聚集索引的表中的数据物理上就是按照聚集键来存放,
搜索重复值或者聚集键的一个范围值更快;行之间相互邻接并且SQL Server能简单定位第一个满足条件的行然后顺序搜索直到发现最后一个满足条件的行。然而,每个表上只能创建一个聚集索引,你必须明智地选择在哪个列或哪些列上来定义聚集索引。
如果你要求在一个表上只创建一个单独的索引,那创建聚集索引有很大优势;则结果就是在修改、插入和删除时的负担将比创建非聚集索引的负担要小的多。
默认情况下,表中的主键将被定义为聚集的唯一索引。在大多数应用中,表上的主键列总是以单行查找的方式来检索。
对于单行查找,一个非聚集索引通常比一个相似的聚集索引花费更少的I/O代价。你或者你的用户真正注意过读三页去检索单个数据行和四到六页去检索单个数据行之间的区别吗?不一定。然而,如果你执行一个范围检索,比如查找last name,你将会注意到扫描表的10%和使用全表扫描来发现行之间的区别吗?一定会的。
根据这种思想,你可能想为你的主键创建一个唯一的非聚集索引,并选择其他候选列做为你的聚集索引。下面就是一些指南,可以帮助你来选择聚集索引的潜在的候选者:
- 一些频繁搜索的具有许多重复值的列,比如, where last_name = 'Smith'
因为数据物理上是有序的,所有的重复值将聚集在一起。任何一个对该键值的查询将会用最小的I/O来发现所有的值。SQL Server 定位第一个满足SARG的行,然后按顺序扫描数据直到找到最后一个满足SARG的行。
-
- 经常被ORDER BY子句指定的列。
因为数据已经是有序的,如果ORDER BY 是关于聚集索引的,那SQL Server将避免重新排序。记住:即使对一个表扫描,数据也将会按照聚集键值的顺序检索,因为数据表上的数据是按照聚集键值排序。
-
- 经常按照一个范围值进行查询的列,例如,Where price between ¥10 and ¥20
使用聚集索引首先定位第一个满足范围条件的行。因为表中的行按顺序排列,SQL Server能简单按顺序扫描数据页直到最后个满足范围的条件的行。当满足条件的结果集非常大,从执行的逻辑I/O来讲,聚集索引扫描将比借助非聚集索引重复进行书签查找更有效。
-
- 除了主键外,频繁使用在join子句中的列。
聚集索引趋向于比非聚集索引更小;每个查找需要页的I/O一般来讲比非聚集索引更少。当join许多记录时这种区别将是巨大的。一两个额外的读页操作好像对一个单行检索来说不多,但是把这些额外的对100,000join迭代的读页操作相加,你会看到总共100,000到200,00读页操作。
-
选择聚集索引键时应满足四个特点:
-
- Narrow(窄,即长度短)
- Unique(唯一性)
- Unchanging(不变化)
- Ever increasing(不断增长)
-
当你考虑聚集索引列时,你可能想尝试在相对静态的列上创建聚集索引,来最小化由于索引列的修改而引起的数据行重新排序。任何时间当聚集索引的键值改变了,所有把聚集索引作为书签的非聚集索引都需要被修改。
尽量避免在以单调形式插入的顺序的键字段上创建聚集索引,比如一个标识列(identity column)。这会在表的末尾创建一个"热点"(hot spot),结果会在表和索引的的末尾导致锁竞争和死锁。另外,聚集索引也不会重用以前数据页中的空间,因为所有新的行都排在数据表的末尾。这种情况造成了空间的浪费和你的表的增长会比预期的要大。一般的建议是,尽量在一个有某种随机分布的数据值上建立索引。尽量选择一个使得插入和修改活动散布在整个表的聚集键。一些能够使得数据随机化的候选聚集索引包括下列:
- 出生日期
- Last name first name
- 邮编
一个随机hash key(通常只当没有其他实际列可以作为好的候选的聚集索引时才使用)
在整个表上散布你的数据有助于最小化页竞争,同时也提供了更有效的空间利用。如果序列键是你的主键,你仍能用一个唯一、非聚集索引来提供一个访问路径并维护主键的唯一性。
因为你只能以一种方式对表上的数据进行物理排序,你只能有一个聚集索引。你想索引的其它列只能被定义为非聚集索引。
非聚集索引(Noclustered Index Indications)
SQL Server 2000允许你在一个表上最多创建249个非聚集索引。直到表变得非常巨大,一个非聚集索引实际所占用的空间与日益增长的访问性能相比是微不足道的。然而,时刻牢记:随着你在系统添加更多索引,数据修改语句由于索引性能的负担会变得更慢。
当定义非聚集索引时,你也想在选择性高的列上定义索引(也就是,具有低密度值的列)这样它们能被优化器来使用。
一个非聚集索引中的大量重复值经常使得使用非聚集索引比表扫描代价更高(按照I/O)。让我们一起来看一个假设的例子:
Select title from titles
Where price between $5 and $10
假设你在范围内有1,000,000行;这些1,000,000行随机分散在整个表中。尽管索引叶级拥有全部的有序索引行,但在最坏情况下,一次读一个数据行也将要求一个书签查找。
这样,在最坏情况下使用非聚集索引来进行范围检索的I/O估计如下:
引用
非聚集索引的层数
+用于发现所有匹配行的扫描的索引页数
+ 匹配的行数 × 每个书签查找的页数
假如你的表上没有聚集索引,那书签仅仅是一个包括页和行的指针,当发现匹配的数据行时需要读取一个数据页。假如范围内有1,000,000行,当该表没有聚集索引时,借助非聚集索引的最坏情况的估计是:
引用
查找所有书签需要读取的索引页数
+1,000,000匹配行 × 1数据页的读取
= 1,000,000 +I/Os
如果表中有聚集索引,书签就是一个代表数据行的聚集索引键,用书签来查找匹配的行要求搜索聚集索引树来定位数据行。假设聚集索引有两级非叶子节点,它将需要读取三页来在数据页上查找每个满足条件的行。如果范围内有1,000,000行,那么借助聚集索引的非聚集索引来查找数据,在最坏情况下它的代价估计如下:
引用
查找所有书签所读取的索引页的个数
+1,000,000匹配的行 * 每个书签查找需求的3页
=3,000,00+I/Os
把每种情况与表扫描相对比。如果整个表占用了50,000页,那么一个全表扫描将只花费50,000 I/O。所以,在这个例子中,一个表扫描实际将比用非聚集索引更有效。
下面的指南帮助你识别非聚集索引的潜在的候选者。
- SARG或join子句中引用的相对来说具有较高的选择性(密度值低)的列。
-
- Where子句和order by子句都引用的列。
- 当使用非聚集索引来检索数据行时,它们按照非聚集索引键的顺序被检索出来。如果结果集也需要按照非聚集索引进行排序,SQL Server能避免对结果集重新排序,这样可实现一个更有效的查询。下面就是这样一个例子:
Select * from authors
Where state like "c%"
Order by state
一般情况下,非聚集索引对单行查找(single-row lookup),连接(join),有高选择性的列的查询,小范围检索的查询有用。当你考虑非聚集索引的设计时也不要忽略了覆盖索引的优点,下节将会讲到。
索引覆盖(Index Covering)
索引覆盖是这样一种情况,查询中的select 和where子句中所需要的信息都能在非聚集索引中找到。因为非聚集索引包含了一个对应于表中每个数据行的一个叶子行,SQL Server能从非聚集索引的叶子行来满足查询。这导致了数据检索的更快,因为所有的信息能从索引页中直接获得,并且避免了SQL Server查找数据页。
因为非聚集索引的叶子页都连接在一起,索引的叶级可以像表中的数据页一样进行扫描,因为页级行都典型比数据行要小,一个覆盖了查询的非聚集索引将比同样列的聚集索引更快,因为需要读取的页数要更少。
在下面的例子中,quthors表中的关于au_lname 和au_fname的非聚集索引将覆盖查询,因为结果中的列和SARG都能从索引中提取出来:
Select au_lname, au_fname
From authors
Where au_lname like "M%"
GO
其他使用聚合函数(MIN AVG SUM COUNT)的查询或者仅仅检查是否存在的查询也能从索引覆盖中获益。下面是一些能够利用索引覆盖优点的查询:
Select count (au_lname) from authors where au_lname like 'm%'
Select count (*) from authors where au_lname like 'm%'
Select count (*) from authors
你可能会奇怪最后一个查询,它甚至没有一个具体的SARG,怎么还能使用索引。SQL Server知道非聚集索引的特性,一个非聚集索引为表中的每行数据都包含了一行;它能够简单的计算任何一个非聚集索引的行数,而不需要扫描整个表。对最后一个查询,SQL Server选择最小的非聚集索引——也就是,具有最少的叶子页的索引。
向非聚集索引添加列使得发生索引覆盖是一种提高查询响应时间的常见方法。考虑下面的查询:
Select royalty from titles
Where price between $10 and $ 20
如果你仅在price列上创建索引,SQL Server能发现满足price在该范围的索引中的行,但是它还需要访问数据行来检索royalty。范围中有100行,最坏情况下检索数据所花费的IO代价计算如下:
引用
索引的级数
+查找匹配行的索引页的数
+100 * 每个书签查找页数
如果royalty列添加到了price列索引中了,索引能被扫描来检索结果,而不是进行书签查找,这样具有更快的查询响应。使用索引覆盖的IO代价将只是:
引用
索引级数
+查找匹配行的索引页的数
引用
注意:
当考虑添加索引来利用索引覆盖时,小心使得索引变得太宽。当索引行的宽度接近与数据行宽度时,覆盖的优点将失去,因为增加了叶级页的数目。当索引的叶级页的数目接近了表中页的数目,索引级数也增加了,那么索引扫描的时间就开始接近于表扫描时间了。
另外,如果你添加对到索引中的列频繁修改,数据行中列的任何修改也会波及到索引中。这增加了维护的负担,也会影响修改的性能。
正如第33章讨论的那样,当在一个表上创建了 一个聚集索引,聚集键会被所有的非聚集索引引用,作为书签来定位实际的数据行。聚集键实际就是一些列,它们构成了聚集索引和它们的数据值。这种特性有时也能导致索引覆盖。
例如,假设suthors表在au_lname au_fname列上建立聚集索引,并有一个定义在au_id的非聚集索引。非聚集索引的每行都包含了与数据行对应的au_lname au_fname聚集键值。因为这个原因,下面查询将被非聚集索引覆盖:
select au_lname, au_fname
from authors
where au_id like '123%'