目录
介绍两个快速、易用的技术:基本问题法(BQ)和快速上限估算法(QUBE)
发现不合适的索引
基本问题法
对每个SELECT语句,以下问题的答案都必须按下述步骤来考虑
如何确定一个方案能否让SELECT在最差输入的情况下仍然运行的足够快
注意
快速上限估算法
服务时间
排队时间
基本概念:访问
读取一组连续的索引行
读取一组连续的表行
计算访问次数
随机访问
顺序访问
FETCH处理
关于QUBE应在何时使用的讨论
介绍两个快速、易用的技术:基本问题法(BQ)和快速上限估算法(QUBE)
发现不合适的索引
一旦一个应用的明细方案确定下来,我们就应该确认当前的索引对新的应用来说是否合适。这里考虑两种简单、快速并且可行的方法:
(1)基本问题法(BQ)
(2)快速上限估算法(QUBE)
基本问题法
对每个SELECT语句,以下问题的答案都必须按下述步骤来考虑
问:是否有一个已存在的或者计划中的索引包含了WHERE子句所引用的所有列?
答:
(1)如果否,那么我们应当考虑将缺少的谓词列加到现有的索引上去。这将产生一个半宽索引,尽管索引的等值匹配过程并不让人满意(一星),但是索引过滤确保回表访问只发生在所有查询条件都满足的时候;
(2)如果这还达不到足够的性能,那么下一个选择是将所有涉及的列都加到索引上,以使访问路径只需要访问索引。这将产生一个避免所有表访问的宽索引;
(3)如果SELECT仍然很慢,就使用第四章所介绍的两个候选索引算法来设计一个新的索引。
如何确定一个方案能否让SELECT在最差输入的情况下仍然运行的足够快
可以每次创建一个索引,用最差输入来测试响应时间。为了确保测得的响应时间接近于正常生产库上运行的性能表现,还必须把缓冲池考虑进来,并观察每个事务的缓冲池命中率。测试的第一个事务很可能在缓冲池中并没有相应的缓存页,所以最差输入下的磁盘读的指标会比正常环境的要高。此外,第一个访问索引的事务必须等待文件被打开。而另一方面,如果变量的输入值保持不变,那么第二个访问该索引的事务将很可能会获得100%的缓冲命中率。
为了获得具有代表性的响应时间,每个索引方案都应在进行过预热事务之后再开始测试,可以通过传入一些典型的输入值(但不是最差情况的值)来打开文件,并将大部分非叶子索引页加载到数据库的缓冲池中。这样,使用最差输入值的事务就会有一个比较有代表性的响应时间了,至少已经把CPU和磁盘读的服务时间考虑进来了。
如果模拟测试和QUBE方法都没有实施,那么应当使用宽索引的方案,并在应用切换到生产环境后立即启用异常报告来发现那些连宽索引都无法满足性能要求的场景。
注意
BQ的目的只是确保我们至少可以通过索引过滤来最小化对表的访问,除此以外没有其他的作用。
快速上限估算法
在最初的评估阶段(当前索引是否让select足够快?),QUBE比BQ更耗时,但它能揭示所有索引或者表设计相关的性能问题:假设对每个谓词所用的最差过滤因子都非常接近于实际的最差情况过滤因子值。根据定义,QUBE方法是悲观的(上限),它有时会有警告误报,但它不会像Q那样漏掉某些未发现的问题。
QUBE的目的是在一个非常早的阶段将潜在的慢访问路径问题暴露出来。为了能在实际项目中使用,任何语句级别的预测公式都必须足够简单,这样才能将评估过程的额外开销保持在一个可以接受的程度。
这个快速估算法的输出结果是本地相应时间(LRT),即在数据库服务器中的耗时。
服务时间
在简单的场景下(I/O时间与CPU时间不重叠),服务时间等于CPU时间加上了排除磁盘驱动排队的随机读时间。如果没有资源竞争,则本地响应时间等于服务时间。
排队时间
在一个常规的多用户环境中,程序的并发会导致对所需资源的各种竞争,因此这些并发程序不得不排队来获取这些资源。以下资源在LRT的范畴内:
(1)CPU排队时间(所有处理器都忙着处理更高优先级的任务)
(2)磁盘驱动器排队(包含请求页的驱动器处于繁忙状态)
(3)锁等待(请求的表或行被锁定在一个不兼容的级别)
(4)多道编程的级别、线程数或其他方面已经达到了上限(这些都是为了防止资源过载而设置的系统值)
QUBE会忽略除磁盘驱动排队外的其他所有类型排队,以提供一个简单的评估过程,用于评估那些对性能影响特别大的方面。
在排除上诉因素后,我们得到的是一个非常简单的估算过程,仅需两个输入变量,TR和TS,必要时还有第三个输入变量F。这些就可以将SQL的处理及I/O成本考虑进来嘞,并且他们是影响索引设计的主要因素。
基本概念:访问
根据定义,DBMS读取一个索引行或一个表行的成本称为一次访问:索引访问或表访问。如果DBMS扫描索引或表的一个片段(被读取的行在物理上是相邻的),那么第一次的读取即为一次随机访问。对于后续行的读取每一行都是顺序访问。在当前的硬件条件下,顺序访问的成本比随机访问要低得多。一次索引访问的成本与一次表访问的成本基本上是相同的。
读取一组连续的索引行
物理上彼此相邻是指所有行都通过指针链接在一起,链接的先后顺序由索引的键值严格定义。当几个索引行键值相同时,就根据索引行存储的指针值进行链接。
现在,读取一组连续的索引行非常快,一次磁盘旋转会将多个叶子页读取进内存中,而且只有在磁盘指针移到下一个柱面时才需要进行一次短暂的寻址。
不过这个完美的顺序还是会被打破的,至少有以下三个影响因素:
(1)如果一个叶子页没有足够的空间存储新插入的索引行,那么叶子页就必须被分裂。之后链表仍会按照正确的顺序链接索引行,但是这与底层的物理存储顺序就不再一致了,一些“按道理”应该是顺序的访问就变成随机访问了。不过索引的重组可以再次恢复最理想的顺序。
(2)意想不到的数据增长可能会填满原本连续的空间(区或者类似的概念)。操作系统于是会寻找另外一个连续的空间,并将它链接到原来空间的后面。这时候从第一个区到第二个区的访问就是产生一次随机访问。
(3)RAID5条带会将前几个叶子页存储在一个驱动器上,将后面的叶子页存储在另外的驱动器上。这就会产生额外的随机读。但实际上条带的积极作用要大于随机读带来的性能恶化。一个智能的磁盘服务器可以将后续的叶子页并行的从多个驱动器读取至磁盘缓存中,从而大大降低了单个叶子页的I/O时间。此外,在RAID5条带策略下,一个被频繁访问的索引不太可能导致某一个磁盘负载过高,因为I/O请求会被均匀的分不到RAID5阵列内的多个磁盘驱动器上。
忽略上述情况,我们仍然假设,如果两个索引行在链表上彼此相邻(或者在非唯一索引中,相同键值的行指针彼此相邻),那我们就认为这两行在物理上页相邻。这就意味着QUBE认为所有的索引都有最理想的顺序。
读取一组连续的表行
读取一组连续的表行有如下两种情况:
(1)全表扫描:从表页1开始,读取该页上所有记录,然后再访问表页2,以此类推。按照记录在表页中存储的顺序进行读取,没有其他特殊的顺序。
(2)聚簇索引扫描:读取索引片上的第一个索引行,然后获取相应的表行,在访问第二个索引行,以此类推。如果索引行与对应的表行记录顺序完全一致(聚簇率100%),那么除了第一次外的所有表访问都是顺序访问。
存储表的传统方式跟索引一样,也是将所有的表页保留在一个连续的空间内。引起顺序杂乱和碎片化的因素也于索引中的相似,但有两个地方不同:
(1)如果往表中插入的记录在聚簇索引所定义的主页中装不下,则通常不会移动现有的行,而是将新插入的记录存储在离主页尽可能近的表页中。对第二个页的随机I/O会使聚簇索引扫描的更慢,但是如果这条记录离主页很近,这些额外的开销可以被避免,因为顺序预读功能会一次性将多个表页装载到数据库缓存中。
(2)一条记录被更新之后,可能会因为表行过长而导致其无法存储在当前表页中,这时DBMS就必须将该行记录迁移到另一个表页中,同时在原有的表页中存储指向新表页的指针。当该行被访问时,这将引入额外的随机访问。
表可以通过重组来还原行记录的顺序,从而减少不必要的随机访问。QUBE假设所有的行、索引都是以最理想的顺序组织的。
根据QUBE的定义,扫描索引或者表的一个片段只需进行一次随机访问。数据库专家们需要对重组的必要性进行监控,以保证我们所做的乐观假设都是合理的。
计算访问次数
随机访问
首先思考一下磁盘读和随机访问的区别。一次磁盘读所访问的对象是一个页,而一次访问的访问对象则是一行。一次随机磁盘读会将一整页读取至数据库的缓冲池中,但是根据定义,前后两次的随机读不太可能会访问到同一页。因此,QUBE中单次随机访问所消耗的时间与一次磁盘读的平均耗时是一样的,都是10ms。虽然随机读是同步的,但是由于现在的处理器速度非常快,所以在估算单次访问的开销时可以忽略CPU时间,这一CPU时间通常小于0.1ms。
顺序访问
一次顺序访问是指读取物理上连续的下一行,这一行在同一页或下一页中。由于顺序读的CPU时间与I/O时间是重叠的,因此顺序访问的消耗时间就是两者中较大的那个。在QUBE中,一次顺序读所消耗的时间是0.01ms。
当计算访问次数时,为了简单起见,我们会遵循以下规则:
(1)忽略索引的非叶子页。我们假定它们都在数据库的缓冲池中,或者至少在磁盘服务器的读缓存中;
(2)假设DBMS能够直接定位到索引片的第一行(忽略为了定位索引行位置而使用二分查找或者其他技术所消耗的时间);
(3)忽略跳跃使顺序读所节省的任何时间;
(4)假设所有的索引和表都处于理想的组织顺序。
顺序访问的时间(0.01ms)是数据库在判断是否接受一行时所做的必要处理的时间。
FETCH处理
被接受的行的数量可以通过FETCH调用的次数来确定(除非多行FETCH可用),这些行将经历更多的处理程序。
如果QUBE被用来比较可选的访问路径,比如比较全表扫描和使用特定索引,那么F参数是无关紧要的,只需要考虑TR和TS。但如果使用QUBE来确定LRT,F参数就可能会比较重要。
在处理被接受了的行时,可能会涉及排序操作,排序的整体开销通常与FETCH行数成正比。大多数情况下,排序的开销非常小。
关于QUBE应在何时使用的讨论
理想情况下,QUBE应当在新方案的设计过程中使用。QUBE甚至可以在设计程序之前就开始使用,只需要简单的知道数据库一定会处理的最差输入场景即可。事实上,直到估算结果令人满意时再开始设计程序是非常明智的做法。有时,我们还必须设计较为复杂的程序结构以满足性能的要求。