|
“ 有了索引,检索就会变快了。 ”
这种想法当然不算错,因为索引本来就是为了让数据查询加快的最常用的方法。可是,各位读者可曾思考过:索引在任何情况下都起作用吗?这次我们要让索引构造恶化,从各种角度分析这个问题。
【实验的目的与环境】
目的:确认执行什么样的处理会导致索引变慢,并以实验检验变慢的原因。
假设:业务操作以 Insert
为主,而且业务中会对大量旧数据执行删除操作
实验的前提:
◎ 在采样各个测试结果之前,重新启动
Oracle ,清空数据库 Cache
◎ 包含索引在内的所有数据都放在一个
extent(initial extent) ,放在同一个 scheme 下
◎ 表数据和索引分别放在各自的表空间
◎
以独特方法让测量时间只包含物理读取时间,而不包含将结果输出的时间(比如输出到控制台的
I/O 时间)
◎ 初始设定参数 db_file_multiblock_read_count
设为预设值 8
◎ 实验结果中的 PHYSICAL READ ( DATA
)表示每次访问 db_file_multiblock_read_count
(该值当前被设定为 8 )个 block 时的 I/O
次数,把这个 I/O 次数乘以 8
就是实际上访问的数据块的数目 (
但是索引检索的时候是以 1block
为单位,这时候的 PH YSICAL READ ( DATA
)值和实际上访问的 Block 数目是相同的 )
【数据的数量】
1. 1 万条记录
2. 100 万条记录(从第 1
万条记录开始反覆执行插入操作,直到达到
100 万条记录)
3. 1 万条(从 100 万条记录中删掉 99
万条记录,剩余 1 万条记录)
在以上 3
种情形下,我们进行各种检验。实验目的不是为了比较各种性能,而是为了找出索引构造恶化的原因。为了尽量让大家能亲自体验
“ 索引检索突然变慢了 ”
的奇怪现象,我们选择使用 NT 环境。
【索引不会理会已被删除的数据】
进行这个实验的原因是因为一次 TREEDUMP
的经历(以后会详细说明)。明明已经删除表中的数据了,但索引却一直维持
0 。 “ 这个 0 是不是代表存在完全没有
ROWID
(后面将解释)的数据块(空数据块)呢?
” 为了解答这个问题,我们建立了一个有
100 万笔数据的表,接着将其中 99
万笔删除,然后用条件表达式执行使用了索引的
SELECT 语句。
【刚开始的预想是正确的】
和我们实验之前想的结果一样,即使删除了的表中的数据,数据块已经是空的了,
Oracle
仍然无法通过索引判断那个数据块是不是空的,导致连没有意义的数据块都被访问到了。以具体的数值表示就是,没有索引的检索比有索引的检索慢了
3
倍以上。对于都使用索引的检索,相较于没有执行删除,且原本就是
1 万笔数据的表(情形 1 ),删除后才变成
1 万笔数据的表(情形 2 )的 I/O 次数多了
300 倍。下面就是检验结论:
1. 删除后已经空掉的数据块, Oracle
还是会继续保存。
2.
因为索引缺乏判断数据块里有没有数据的要素,所以进行如下(<=)的范围检索时,
Oracle
还是不知道那些是空的数据块,而是持续往下读取,结果如下面的图片:

这是索引构造的问题:即使表中数据已经被刪除,也不代表索引中对应的数据块也被刪掉,
Oracle
会将已经为空的数据块预备给将来数据的插入,即作为
Place Holder
。实验结果证明,把删掉的数值重新输入一次,只要索引构造没有太大的改变,各条记录就一定会对号入座。
由此可知,索引对单一检索有用,但是对范围检索未必有用,至少对于经常进行大量删除的表进行大范围检索的时候,索引几乎完全无用。不过如果定期进行数据库维护(执行
REBUILD
等等),情况就未必如此。只是在什么时候、哪个数据块的数据密度变低、从哪里到哪里变成空的,这些几乎不可能预测。由于这个原因频繁执行维护是没有太大实际意义的。所以
DBA
除了掌握各个表的特性(比如某个表经常有大量删除,而不是只有由小到大的插入)之外,还得从数据库维护和
SQL
结构(例如根据情况使用索引)两方面思考如何处理问题。
有了这些基础知识,下次我们开始介绍实际的检验结果,希望能帮助大家判断何时进行维护。
【索引的构造】
解释实验结果之前,先简单说明 B-Tree
的构造。(即使很多读者已经都知道了)

索引就像 上图
所示的结构一样,这种构造称为平衡树
(Balance Tree) 。根节点 (Root node) 和枝节点
(Branch node)
里有包含叶子区索引键集合中最前面的那个索引键和叶子区的数据块地址(
Data Block Address , DBA ) ,
而叶子区包含了实际的索引键。换句话说,叶子区
(leaf block) 包含索引键和真实数据的 ROWID ,
ROWID 代表数据在数据库中偏移。
在本例的图示中,枝的首部没有键,只记录数据块地址。
Oracle
不保留枝下面的最前面的(即最靠左的)叶子节点最前面的叶键(
leaf block key )。这意味着,比枝节点的第 2
个 DBA 的键更小的值,会放在最前 ( 左 )
面的 DBA 里面。图示中可以看到,最左边的
DBA 中,所记录的叶键( 10 ~ 50
)都是小于其所在枝节点记录的最小的键值(
60 )。
【附注】 ROWID = 真实数据存放的物理位置
【发生叶分割的时间点】

从上图可以了解到,叶子块( Leaf block
)中的键 (Leaf key)
会从小到大排列。现在假想每个叶子都已经满了,
“ 新增插入记录 35 会放到哪里? ”
想把新记录 35 插入第 1 个叶数据块的 30
下面,第一个数据块里没有空隙,这时候就会发生叶分割。如果不考虑性能,简单的把从第一个叶数据
(leaf data)
开始到最后一个叶数据往后挪动,把第一个叶数据的空间空出来,然后把数据
35
插入第一个叶数据块。这样做的操作代价非常大,
Oracle
当然不会这么做。图示说明了需要进行叶分割,记录
35 会与紧接其后的 40
等若干叶数据结合为一个新的叶数据块。这样一来,新的叶和被分割的叶可能会出现许多空闲空间。至于空出来多少,受插入数据的顺序影响。
如果数据是由小到大顺序插入,那么就不会出现空闲空间,但是如果插入的时候由大到小或者毫无次序的插入,叶分割就会频繁发生,并产生大量空闲区。实际的系统并不会考虑插入数据的顺序,因此无法预测叶分割的情况。
【索引不停止?】
索引会经过以上所说的三个阶段(根、枝、叶),然后抵达目标记录,但只有在执行唯一检索
(unique search)
的时候才会如此。如果是范围检索,抵达记录的过程是相同的,但是,如果检索中利用了索引由小到大存放的特性寻址符合检索条件的目标记录,那就会在读取到第一个符合条件判断的那个数据块后,持续向前(或者向后)读取,直到检索出所有目标数据块。
这次的实验重点在于了解索引构造的特性,以范围检索为主要示例,实际就是让叶数据块产生偏差
(Skew) ,以验证数据库会 “
数据已被删除,但其索引键仍然被读取 ”
的奇妙现象。
|