当程序中全部的SQL都是用到了一个或者多个索引,许多DBA就会对此感到满意,认为一切都看起来正常。可是,使用一个不合适的索引有可能会致使比全表扫面更差的性能。本随笔将详细地考虑这些极其重要的问题。首先给出咱们讨论问题所需的前提假设。程序员
磁盘随机读取一次4k或者8k大小的页,须要10ms,顺序读取速度40MB/s;CPU扫描一行记录须要5us,从缓存中获取一次数据须要100us;算法
对于下面的简单SQL查询,仅有的两个合理的访问路径:数据库
一、索引扫描缓存
二、全表扫描性能
即便对于最广泛的姓氏(过滤因子1%),这两种选择是否可以提供可接受的响应时间呢?优化
对于第一种选择,数据库管理系统会根据where条件LNAME=:LNAME扫描索引片。对于索引片中的每个索引行,数据库管理系统都必须回到表里检查CITY的值。因为表中的行是根据CNO而不是LNAME聚簇的,因此这个检查操做须要一次磁盘的随机读取。对于最广泛的姓氏,在不考虑CITY的过滤因子的条件下,获取完整的结果集意味着,需比对10000个索引行和10000个表行。那么,这个过程会耗时多少?设计
假设索引(LNAME,FNAME)的大小是10000*100byte=100MB,包括数据和离散的空闲空间,另外再假设顺序读的速度是40MB/s。读取一个宽度为1%的索引片,即1MB,需花费10ms+1/40s=35ms,这显然没有问题,可是10000次随机表读取需花费10000*10ms=100s,这使得这种方式太慢了。3d
对于第二种选择,只有第一个页须要随机读。若是表的大小为1000000*600byte=600MB,包括数据即分散的空闲空间,那么花费的I/O时间为10ms+600/40s=15s,仍旧很慢。blog
第二种选择的CPU时间将会比第一种选择的时间长得多,由于数据库系统须要比对1000000行而不是20000行,并且还要对这些行进行排序。从另外一个方面看,因为是顺序读取,cpu时间跟I/O时间交叠。在这个场景下,全表扫描要比在不合适的索引上扫描快,但这还不够快,须要有一个更好的索引。排序
前一小节咱们讨论了CURSOR41的不合适的索引,这一小结咱们讨论另外一个极端,三星索引,即对于一个查询语句可能的最理想索引。相似图4.2中的查询语句,若是使用了三星索引,只需一次随机读取和一次窄索引片的扫描。所以,其响应时间会比使用普通索引的响应时间少几个数量级。
即便返回的结果集有1000行,CURSOR41(见SQL4.2)的响应时间也不足1s,这是怎么作到的呢?图4.2展现了索引最低一层叶子页的状况。
若是结果集只有1000行的话,那么组合where条件LNAME =: LNAME AND CITY =:CITY的过滤因子就是0.1%。被扫描的索引片就只有1000行,由于索引片的宽度彻底由LNAME和CITY两个条件所决定。这种状况下,查询将花费1*1ms + 1000*0.1ms=0.1s。在这个过程当中,表根本就没有被访问过,由于所需的列值都被复制到了索引中了。
若是与一个查询相关的索引行是相邻的,或者至少相距足够靠近的话,那么这个索引就能够被标上第一颗星。这最小化了必须扫描的索引片的宽度。
若是索引行的顺序跟查询语句的需求一致,则索引能够被标记上第二颗星。这排除了排序操做。
若是索引行包含查询语句中的全部列,那么索引能够标记上第三颗星。这能够避免对表的操做:由于直接访问索引就能够了。
对于这三颗星,第三颗星一般是最重要的,将一个列排除在索引以外可能会致使许多速度较慢的磁盘随机读。咱们把至少包含第三颗星的索引称做对应查询语句的宽索引。
取出全部等值条件(等值,并非范围)的列。把这些列做为索引最开头的列,以任意顺序均可以。对于CURSOR4.1来讲,三星索引能够以LNAME,CITY或CITY,LNAME开头。在这两种状况下,必须扫描的索引片宽度将索至最小。
将order by列加入到索引中。不要改变这些列的顺序,可是忽略那些在第一步中已经加入索引的列。例如,若是在CURSOR4.1 order by中有重复的列,好比 order by FNAME,LNAME或者order by CITY,FNAME,只有FNAME列须要被加到索引中。当FNAME是索引的第三列时,结果集中的记录无需排序就已是以正确的顺序排序的了。第一次读取操做将返回FNAME值最小的那一行。
将查询语句中剩余的列加到索引的尾部,列在索引中添加的顺序对查询语句的性能没有影响,可是将易变的列放在最后能下降更新的成本。如今,索引已包含了知足无须回表的访问路径所须要的所有列。
最终三星索引将会是:
(LNAME,CITY,FNAME,CNO)或(CITY,FNAME,CNO,LNAME)
CURSOR4.1在如下三个方面是最为挑剔的:
下面的SQL4.3须要的信息跟以前相同,只是如今LNAME是在一个范围内。
让咱们为这个CURSOR设计一个三星索引。大部分的推论跟CURSOR4.1相同,可是“between条件”将“=条件”替换后将会有很大的影响。咱们将以相反的顺序考虑三颗星。
首先是第三颗星,按照先前所述,确保查询语句中的全部列都在索引中就能知足第三颗星。这样不须要访问表,那么同步读也就不会形成问题。
添加order by列能使索引知足第二颗星,可是这个仅在将其放在between范围条件列LNAME以前的状况下才成立,如索引(CITY,FNAME,LNAME),因为CITY的值只有一个,因此使用这个索引可使结果集以FNAME的顺序排列,而不须要额外的排序。可是若是order by字段加在between范围条件列LNAME后面,如索引(CITY,LNAME,FNAME),那么索引行不是按照FNAME排序的,须要对结果集进行额外的排序。所以为了知足第二颗星,FNAME必须放在范围条件列LNAME以前,如索引(FNAME,...)或者(CITY,FNAME,...)。
再考虑第一颗星,若是CITY放在索引的第一列,那咱们将会有一个相对较窄的索引片须要扫描,这取决于CITY的过滤因子。可是若是使用(CITY,LNAME)的话,索引片将会更窄,这样在有两个列的状况下咱们只须要访问真正须要的索引列。可是,为了作到这样,并从一个很窄的索引片中获益,其余列(如FNAME)就不能放在这两列之间。
因此,咱们的理想索引会有几颗星呢?首先,确定能有第三颗星,可是,正如咱们刚才所说,咱们只能有第一颗星或者第二颗星,而不能同时拥有二者!换句话说,咱们只能二选一:
在这个例子中,因为between或者其余任何访问条件的出现,意味着咱们不能同时拥有第一和第二颗星。也就是说咱们不能拥有一个三星索引。
根据以上的讨论,理想的索引是一个三星索引。然而,正如咱们所见,当存在访问条件时,这是不可能实现的。咱们(也许)不得不牺牲第二颗星来知足一个更窄的索引片,这样最佳索引就只拥有两颗星。这也就是为何咱们要仔细区分理想和最佳。在这个例子中,理想索引是不可能实现的。将这层因素考虑在内,咱们能够对全部状况下建立最佳索引的过程公式化。建立出的索引将拥有三颗星或者两颗星。
首先设计一个索引片尽量窄(第一颗星)的宽索引(第三颗星)。若是查询使用时不须要排序,那这个索引就是三星索引。不然,这个索引就只能是二星索引,牺牲第二颗星。或者采起另外一种选择,避免排序,牺牲第一颗星保留第二颗星。
下面咱们阐述为查询语句建立最佳索引的算法。
若是候选A引发了所给查询语句的一次排序操做,那么还能够设计候选B。根据定义,对于候选B来讲,第二颗星比第一颗星更重要。
近几年来,排序速度已经提高了不少。如今大多数的排序过程都在内存中进行,用当下最快的处理器排序一行花费的CPU时间大约在5us左右。所以,排序50000行的数据所耗费的时间只有0.5s。这对于一次事务操做来讲是能够接受的,但对于CPU时间来讲是一个比较大的开销。
因为在如今的硬件条件下排序速度很快,因此若是一个程序取出结果集的全部行,那么候选A可能和候选B同样快,甚至比候选B更快。对于程序员来讲,这是最方便的解决方案。许多环境都提供了灵活的命令来浏览结果集。
然而,若是一个程序只需获取可以填充满一个屏幕的数据量,那么候选B可能会比候选A快得多。若是结果集很大的话,为了产生第一屏的数据,二星索引候选A(须要排序)可能会花费很是长的时间。咱们须要时刻记着,客户端的一次错误输入可能会使得结果集变得很是大。
若是访问路径中没有排序的话,使用CURSOR44程序将会很是快(假设LNAME和CITY是索引的前两列,无论顺序如何),即便结果集包含数以百万级的数据行。每一个事务永远都不会使数据库管理系统物化大于20行的数据。