Loading

查询处理

假设磁盘子系统传输一个块的数据的平均时间是\(t_r\)秒,磁盘块平均访问时间是\(t_s\)秒(磁盘搜索时间加上旋转延迟),一次传输b个块并执行S次磁盘搜索的时间是\(b\times t_r + S \times t_s\)

选择运算时间开销

下面是一些常见的选择运算所需要的时间开销,这里我们假设一个文件中有\(b_r\)个磁盘块,如果选择操作使用了B+树索引,那么\(h_i\)代表B+树的高度。

  • 线性搜索:线性搜索就是搜索整个文件中的所有块,找到所有满足条件的。它效率低但是大部分类型的搜索它都能够工作。
  • 索引搜索:索引定义了一条从根到所搜索记录的一条路径,这条路径通常很短,只需要几次磁盘块访问。并且大部分数据库系统将索引树的所有叶子节点放到内存的缓存中,这把寻找路径工作的磁盘块访问次数降低到了1次。因为相比叶子节点,非叶子节点在所有节点中的占比通常不超过1%。(下一层节点数相比上一层节点数有指数级增加,而B+树的指数很大)
编号 算法 开销 原因
A1 线性搜索 \(t_s+b_r\times t_r\) 一次初始磁盘块搜索和对文件中所有块的扫描
A2 线性搜索,码属性等值比较 \(t_s+\left \lceil b_r/2 \right \rceil \times t_r\)(平均) 因为最多一条记录满足条件,最坏情况下也要\(b_r\)次传输磁盘块
A3 B+树主索引,码属性等值比较 \((h_i+1)\times (t_s+t_r)\) 一共需要树的高度+1次IO操作,多出来的一次是取实际记录
A4 B+树主索引,非码属性等值比较 \(h_i\times (t_r+t_s) + b\times t_r\) 需要\(h_i\)次块搜索和传输来找到包含搜索码的叶子节点,b是所有包含指定搜索码的块数,对这些块进行扫描然后对每条记录进行等值比较
A5 B+树辅助索引,码属性等值比较 同A3
A6 B+树辅助索引,非码属性等值比较 \((h_i+n)\times (t_r+t_s)\) 由于辅助索引是非聚集的,所以n个匹配的项目需要搜索n次磁盘块,如果n过大代价可能很高
A7 B+树主索引,比较 同A4 和A4一致,因为都要沿着B+树找到包含满足搜索码的第一个叶子节点,然后沿着叶子节点顺序查找到第一个不满足的搜索码
A8 B+树辅助索引,比较 同A6

复杂选择的实现

合取和析取

即and和or操作。

  • 利用一个索引的合取选择:因为合取操作的每一个简单条件都要满足才能为真,所以可以任意取一个简单条件中创建过索引的搜索码,然后利用上面的任意一个索引搜索算法读取出所有满足这个简单条件的记录,然后逐个测试是否满足条件。该算法的代价取决于使用了何种索引算法。
  • 利用组合索引的合取选择:如果合取操作的几个简单条件的搜索码构成了组合索引,那么可以直接使用组合索引的顺序进行搜索,这样可以进一步缩小读取的范围,然后对于这些读取出来的记录,逐个同条件中的非索引属性做比较。该算法的代价取决于使用了何种索引算法。
  • 通过标识符的交实现合取选择:该算法要求条件中的部分字段具有能够指向记录指针的索引,该算法对所有条件中的这种字段使用索引进行检索,取出满足的记录,所有满足记录的交集就是结果。如果条件中并非所有字段都有这种索引,还要对结果进行过滤,也就是与剩余的字段比较。
  • 通过标识符的并实现析取选择:如果条件中的所有字段都具有能够指向记录指针的索引,对所有条件中的这种字段使用索引进行检索,取出满足的记录,所有满足记录的并集就是结果。如果条件中有任意多个字段没有这种索引,都需要再对整个关系进行一次线性扫描,找出满足条件的元组。所以如果析取式中有一个属性没有索引,就可以直接线性扫描了。

取反

也就是!=操作。

在后面的练习里。

排序

很多时候,返回的结果需要有序,而且将文件按照某个(些)搜索码进行物理排序会提升很多操作的性能。

如果只是在排序码上建立索引(辅助索引)那么仅仅是在逻辑上对搜索码进行排序了,实际上记录可能存在于不同的磁盘块中,所以物理上它们并没有被排序,这时使用order by顺序读取可能导致每一条记录都要访问磁盘。出于这些原因,有时还是要在物理上对记录进行排序。

所有数据在内存中直接进行排序称为内部排序,而数据无法全部放进内存中,需要一些其他办法进行的排序操作称为外部排序。数据库中的排序属于外部排序。

外部排序归并算法

假设内存中有M个块可以供排序使用。

第一步要从关系中读入M个块,对每个块进行单独排序,然后保存到一个文件中,这个文件叫归并段文件,直到关系中没有数据了。这一步过后,所有归并段文件中的记录都是有序的。

第二步就是对所有归并段进行归并,对所有归并段文件分配一个内存块作为缓冲块(假设归并段文件个数小于M,并且剩下一个块存放结果),然后将每一个归并段文件\(R_i\)中的内容填充到它对应的缓冲块中,取出这些缓冲块中最小的那个元组(就是从每个缓冲块中的第一个元组中挑选最小的那个,因为缓冲块内部有序),然后写出这个元组到大文件中,重复这个过程,最后的大文件就是有序的。

如果关系比内存大很多,在第一步会产生远远大于M个归并块,在第二个阶段无法分配足够的缓冲块。此时需要多趟归并。

就是第一趟归并选择前\(M-1\)个段按照上面所述第二步进行归并(因为要留一个块),得到一个有序的大归并段(注意归并段作为一个文件,它没有大小限制,块才有大小限制),然后接下来的\(M-1\)个段进行归并,再得到一个有序的大归并段,直到所有的归并段都处理过并成为大归并段,这时总归并段数缩减为之前的\(\frac{1}{M-1}\)。如果这些新的大归并段个数仍然大于等于M再对这些新的大归并段进行如上操作,直到它们小于M,进行最后一次归并。

下面分析它的效率。

\(b_r\)为关系\(r\)占用块数。第一步需要\(2b_r\)次磁盘块传输,因为存在读取磁盘块和写入归并块两个操作。

初始阶段归并块数为\(\left \lceil b_r/M \right \rceil\),每次归并,归并块数缩减到原来的\(\frac{1}{M-1}\),所以总共所需\(\log_{M-1}(b_r/M)\)趟归并(回想二分搜索效率,每次数据量缩小到原来的\(1/2\),时间复杂度是\(O(\log_2(N))\))。

因为每一趟归并,关系中的每一个数据块各读写一次,所以关系外排序的磁盘块传输总数为

\[b_r(2 \left \lceil \log_{M-1}(b_r/M)\right \rceil + 1) \]

连接运算

我们主要计算一个连接算法的磁盘搜索次数和磁盘读取次数。

磁盘搜索次数就是磁盘的磁头需要移动到正确位置的次数,磁盘读取次数就是磁盘块传输到内存中的次数。

循环嵌套连接

简单粗暴双循环。对于r中的每一个元组和s中的每一个元组进行匹配,如果匹配就添加到结果中。这里假设\(b_r\)为关系\(r\)占用的磁盘块数,\(b_s\)为关系\(s\)占用的磁盘块数,最坏情况下,系统只能缓存一个磁盘块,需要\(b_r+n_r\times b_s\)次磁盘块访问,因为对\(r\)中的每一个元组\(t_r\)都需要读取每一个\(b_s\)

下面是磁盘搜索次数,对于内层的\(s\),因为每次都读取一遍,所以每次循环只需要1次磁盘搜索,一共需要\(n_r\)次,而对于外层,则需要\(b_r\)次磁盘搜索,因为每次磁头都被内层循环移动,所以每个块需要重新进行磁盘搜索。总共\(n_r+b_r\)次磁盘搜索。

如果一个关系足够小到可以被装到内存,那么这个关系最好应用到内层循环中,因为从上面的公式可以看到内层的读写次数应该多于外层,如果这样的话,只需要\(b_r+b_s\)次读取磁盘块和两次磁盘搜索(对于两个关系都能装到内存中的情形也是一样的)

效率估算

计算上面的student和takes示例所需要的磁盘搜索次数和磁盘读取次数。

因为student比takes规模小,所以我们将它放到内层作为关系s,而takes作为关系r。

最坏情况下的磁盘读取次数是:\(400 + 10000\times 100 = 1000400\)次,而磁盘搜索次数是\(10000+400=10400\)次。

最好情况下的磁盘读取次数是:\(400+100=500\)次,磁盘搜索次数为两次。

如果使用takes作为内层关系:最坏情况下磁盘读取次数是\(100+5000\times 400 = 2000100\),磁盘搜索次数是\(5000+100=5100\)

块嵌套循环连接

使用块嵌套循环,每次先分别读取出来自两个关系中的一个块,这样在最坏无缓存的情况下,就不用对于外层关系的每一个元组\(t\),内层都读取一次所有块了,而是对于外层的一个块\(B_r\),内层读取所有块。最坏情况需要\(b_r + b_r\times b_s\)读取磁盘块,需要\(2b_r\)次磁盘搜索,如果内存无法容纳任何一个关系,让规模较小的在外层更好。

最好情况下,内存能容纳内层关系,那么需要\(b_r+b_s\)次磁盘块读取,需要两次磁盘搜索。

效率估算

最坏情况下磁盘读取次数是:\(100+100\times 400=40100\)次,磁盘搜索次数是:\(200\)次。

最好情况和之前一致。

改进

循环嵌套连接和块嵌套循环连接可以继续改进

  1. 如果使用等值比较进行连接,连接属性是内层的码,在内层搜索时搜到第一个即可退出内层循环。
  2. 在块嵌套循环连接算法中,外层可以不以磁盘块作为分块单位,而是以内存中最多能容纳的大小作为单位,减少磁盘读取和搜索次数。(当然要预留足够空间)
  3. 对内层循环做轮流的向前向后的扫描,以保证上次循环时在缓存中的热数据这次仍有部分可用。
  4. 在等值比较连接时,若内层循环连接属性上有索引,可以使用索引查找法。

索引嵌套循环连接

如果在等值比较连接中,内层循环的关系中在连接属性上有索引,那么相当于对于每一个外层的元组,对内层进行一次索引搜索。

这样总共的磁盘访问次数是:\(b_r+n_r\times c_r\)\(c_r\)是对内层进行索引搜索的平均磁盘访问次数,磁盘搜索次数也是\(b_r+n_r\times c_s\)\(c_s\)是对内层进行索引搜索的平均磁盘搜索次数。其实它们是一样的,因为这里的每一次磁盘访问都需要磁盘搜索,所以它们的次数一致。

时间代价是:\(b_r(t_s+t_r)+n_r\times c\)

还是上面student和takes的例子,使用索引嵌套循环链接,磁盘访问次数是:\(100+5000\times 5=25100\),这里的5是假设的,磁盘搜索次数也是\(25100\),块传输次数减少,但磁盘搜索次数变多了许多,磁盘搜索比磁盘读取更加耗时,所以这种直接使用索引的算法大部分情况下还不如之前。

归并排序连接

归并排序连接算法的前提是,两个表各自按照连接属性进行排序。它的思想就是利用两个表针对连接属性的有序性,将两个表中具有相同连接属性的元组找出来,然后只针对这些元组进行拼接。该算法中,使用JoinAttrs代表两个表的连接属性。 这个符号代表将两个连接属性相同的元组进行拼接。

大部分情况下,数据库系统不会为了使用归并排序连接而对两个连接表手动进行排序,担当两个表(或其中一个)已经排好序了,那么有可能使用归并排序连接。

可以使用下面的数据在脑子里走一遍这个过程,JoinAttrs是a1

效率估算时,我们假设两个表已经按照连接属性排序。

磁盘访问次数:\(b_r+b_s\)
磁盘搜索次数:不考虑缓冲也是\(b_r+b_s\),考虑缓冲之后,假设为每个关系分配\(b_b\)个缓冲块,那么磁盘搜索次数就是\(\left \lceil b_r/b_b \right \rceil +\left \lceil b_s/b_b \right \rceil\)

效率估算

还是student和takes的例子,磁盘访问次数\(100+400=500\),最坏条件下\(b_b=1\),磁盘搜索次数\(100+400=500\)

未排序情况

结合上面外部归并排序算法的例子,可以估算出当参与连接的关系未排序时,需要主动排序再进行归并连接时的算法效率。

懒得算了,直接看结论,和之前相比是有很好的改善了。

散列连接

散列连接的基本思想是,如果两个关系进行连接时,有两个元组匹配,那么它们在连接属性上有相同的hash值(因为只有连接属性相同的一对儿元组会被匹配)。比如r与s连接,其中有一对匹配的元组\(t_r,t_s\),如果\(t_r\)在桶\(r_i\)中,\(t_s\)必定在桶\(s_i\)中。

下面是算法

  1. 读取\(s\)中的每个元组,通过hash函数\(h\)计算连接属性的hash值,并将它们分配到\(n_h\)个散列桶\(s_i\)中,这些散列桶存在硬盘上
  2. 读取\(r\)中的每个元组,通过hash函数\(h\)计算连接属性的hash值,并将它们分配到\(n_h\)个散列桶\(r_i\)中,这些散列桶存在硬盘上
  3. \(0\)开始递增\(i\),直到\(i=散列桶个数\)
  4. 读取\(s_i\)到内存中,并在内存中对\(s_i\)构造(build)散列索引,方便之后同\(r_i\)中的数据比较。这个散列索引的散列函数一定和之前的不同,因为该桶中的所有数据应用之前的散列函数时都将得到相同的散列值
  5. 对于\(r_i\)中的每一个元组\(t_r\)探查(probe)\(s_i\)的散列索引中满足连接属性与该元组相等的元组\(t_s\)
  6. \(t_r\)\(t_s\)合并,放到结果中

上面的算法分为划分阶段和连接阶段。需要仔细思考如上步骤中,哪里是在内存中操作,哪里在磁盘中操作。比如对于连接阶段的\(s_i\),我们必须将每一个\(s_i\)的数据全部读到内存中,因为我们要在内存中建立它的散列索引,对于\(r_i\),我们并不用全部读到内存中。

我们针对\(s_i\)构造了散列索引,所以称关系\(s\)构造时输入(build input),我们使用\(r_i\)中的每一个元组对\(s_i\)的散列索引进行探查,所以我们称关系\(r\)探查时输入(probe input)。

因为要将\(s_i\)全部读入内存中并在内存中建立散列索引,所以我们要有一个足够大的桶数量\(n_h\)以保证每一个桶中的数据量足够小,足以让我们读入内存。

假如关系\(s\)\(b_s\)块,内存中有\(M\)个块可供使用,我们要确保\(n_h\)个桶中的每一个桶中的数据量能被这\(M\)块所容纳,所以,\(n_h\)至少是\(\left \lceil b_s/M \right \rceil\)个。

实际上,除了桶中的原始数据外,我们还要留出一部分空间来确保对桶中的数据建立散列索引,也就是说\(n_h\)要比我们刚刚估算的更大一些。方便起见,分析中有时忽略针对\(s_i\)桶建立散列索引所需要的内存空间。

递归划分

在划分阶段,也就是这个部分:

对一个关系进行划分操作,我们要划分出\(n_h\)个桶,为了连接阶段的性能,我们要尽量的让\(n_h\)大一点。但这和划分阶段冲突,划分阶段喜欢\(n_h\)尽量小,小到散列桶的个数小于等于内存块的个数,把每一个散列桶都放到一个内存块上,因为如果\(n_h\)大到内存块无法容纳,那么很多hash操作将元组分配到对应的桶时就需要进行随机的磁盘块访问(直接访问磁盘上的桶),这会徒增磁盘搜索和读写开销(我猜的)。我们不断更新连接算法的目的就是减少磁盘搜索和磁盘读写开销。

这时可以在划分阶段使用递归划分,它的思想就是先使用一个小的内存足以容纳的\(n_h\),对数据进行第一趟划分。然后对于之前的\(n_h\)个桶中的每个桶中的所有数据,再使用另一个散列函数进行第二趟划分,得到一个更细致的桶,直到每一个小桶都能被内存容纳。

大概就是这样

这样一趟,桶的个数就指数级增长,几趟就划分出超级多的桶。

溢出处理

\(s_i\)的散列索引大于主存时,构造用输入关系\(s\)的划分\(i\)发生散列表溢出。

原因是关系中连接属性相同的可能太多了,或者散列函数选的不好,让数据在散列桶中的分布不均匀。当某些散列桶中的数据明显多于平均数,某些散列桶中的数据明显少于平均数时,称这个桶是偏斜的(skewed)。

少量偏斜可以通过增加划分数目来解决,增加的划分桶称为避让因子(fudge factor)。

严重的偏斜可以使用溢出分解法和溢出避免法。溢出分解就是让溢出的桶使用一个新的散列函数,将这个桶划分成一些更小的桶。溢出避免法则是通过划分期谨慎的划分确保溢出永远不会发生。

代价分析

分析代价时我们不考虑溢出,首先考虑没有递归划分发生

不考虑递归划分

划分阶段需要对两个关系进行读入和写回,是\(2(b_r+b_s)\)次块传输,在连接阶段进行构造与探查,需要将所有数据读入一次,又是\(b_r+b_s\),所以一共是\(3(b_r+b_s)\)。再看划分阶段,如果关系中数据量大,\(n_h\)个划分中的每个划分都可能有一个部分满的块,对这个块需要写回操作。对于每个参与连接的关系,最坏情况下增加\(2\times n_h\)次开销。所以散列连接的代价大概是

\[3(b_r+b_s)+4n_h \]

次块传输

划分阶段假设有\(b_b\)个块作为划分桶的缓冲,那么划分总共需要\(2(\left\lceil b_r/b_b\right \rceil+\left \lceil b_s/b_b\right \rceil)\)次磁盘搜索,构造探查阶段\(n_h\)个划分中每个划分只需要一次磁盘搜索,所以总共是

\[2(\left\lceil b_r/b_b\right \rceil+\left \lceil b_s/b_b\right \rceil) + 2n_h \]

次磁盘搜索

考虑递归划分

递归划分和归并算法差不多,每一趟划分的大小减小到之前的\(\frac{1}{M-1}\),我们需要划分到每一个桶中的数据足以被内存容纳,所以一共需要大概\(\left \lceil \log_{M-1}(b_s) - 1 \right \rceil\)趟递归。

每一趟递归都要读取两个关系中的所有块,所以划分阶段有\(2(b_r+b_s)\left \lceil \log_{M-1}(b_s) - 1\right \rceil\)次磁盘传输。

再加上连接阶段的\(b_r+b_s\),最后,划分阶段有:

\[2(b_r+b_s)\left \lceil \log_{M-1}(b_s) - 1\right \rceil + b_r + b_s \]

次磁盘传输

磁盘搜索次数需要在原来的基础上乘以划分阶段的趟数

\[2(\left\lceil b_r/b_b\right \rceil+\left \lceil b_s/b_b\right \rceil) \times \left \lceil \log_{M-1}(b_s) - 1 \right \rceil + 2n_h \]

估算

其他运算

去除重复

可以使用排序实现,排序时如果发现相邻元组相等直接删除,只保留一个副本。如果使用外部归并排序,可以在创建归并段文件时,将数据写回磁盘之前发现重复数据并剔除,节省不必要的磁盘读写开支。

也可以用散列桶实现。

集合运算

交并差,对两个文件进行排序,然后扫描两个文件,发现相同的只保留其中一个,这是并集,只保留同时在两个文件中出现的元组这是交集,只保留一个中不属于另一个的元组这是差集。只要我们对文件进行排序后,就可以按照类似的方法去做。

也可以用散列实现

题目

累了,先略。。。

posted @ 2021-11-05 12:44  yudoge  阅读(489)  评论(2编辑  收藏  举报