【数据库基础】4. 查询执行


查询处理概述


查询处理的步骤如下:

【数据库基础】4. 查询执行_第1张图片

查询处理可以大概分为三个部分:

  1. 语法分析与翻译
  2. 查询优化
  3. 查询执行

执行代价的度量


设计查询执行方案之前,我们必须能度量一个查询执行方案的代价

在大型数据库系统中,在磁盘上存取数据的代价是查询执行的主要代价,因为比起内存操作,磁盘存取速度很慢

代码在 CPU 中执行也需要时间,但只要 CPU 的运算次数不算太多,CPU 时间相比于 I/O 耗时就可以忽略

在磁盘上存取数据可以分为寻道和数据传输两个步骤,因此我们可以用寻道的次数和传输的块数来度量一个执行方案的代价,我们用 t s t_s ts 表示一次寻道所需的时间, t r t_r tr 表示传输一个块所需的时间, t s t_s ts 的典型值为 4 毫秒, t r t_r tr 的典型值为 0.1 毫秒


选择运算


通用方案

最通用也是最简单的执行选择运算的方案就是:扫描整个关系,对于扫描到的每一个元组,检查它是否满足选择条件

假设一个关系有 B B B 个块,由于整个关系都要扫描,因此磁盘传输的块数为 B B B ,通常情况下,我们可以认为一个关系保存在一个文件中,关系的每个块在物理地址上是大致连续的,因此我们只需要在扫描开始的时候寻道一次

等值条件

当选择条件仅为在某个属性或属性集上取特定的值时,我们可能有比扫描整个关系更快的方案

主索引

若关系在对应属性或属性集上有主索引,那么选择的效率会大大提升,借助主索引,可以定位到所有满足等值条件的元组,并且在这种情况下,这些元组一定存储在文件的连续的块中,假设这些元组存放在文件中连续的 b b b 个块上,检索一次索引需要的块访问次数为 h h h ,则磁盘传输的块数为 b + h b+h b+h ,寻道次数为 h + 1 h+1 h+1 ,通常情况下 h h h 不会很大,相比于扫描整个关系,该方案主要减少了磁盘传输的块数

当然,如果关系在对应属性或属性集上仅组织成顺序文件,而没有创建索引,也可以用二分搜索代替主索引定位满足等值条件的元组

辅助索引

若关系在对应属性或属性集上有辅助索引,则显然要考虑如何利用辅助索引,和主索引不同,利用辅助索引虽然也可以定位到所有满足等值条件的元组,但这些元组可能分布在文件的各个块中,假设共有 n n n 个元组满足选择条件,检索一次索引需要的块访问次数为 h h h ,则在最坏的情况下,这 n n n 个元组所在的块各不相同,需要磁盘传输 n + h n+h n+h 块,每传输一个块,就可能需要一次寻道,最坏情况下需要寻道 n + h n+h n+h

从上面的分析可以看出,如果满足等值条件的元组比较少,那么使用辅助索引就是值得的,否则使用辅助索引甚至还不如扫描整个关系的方案

范围条件

类似于等值条件,当选择条件为在某个属性或属性集上取特定范围内的值时,也可以利用索引提升效率

主索引

如果关系在对应属性或属性集上有主索引,或虽然没有索引,但按顺序组织成了顺序文件,都可以做到和等值条件类似的加速,利用索引或二分搜索直接定位到范围的下届,然后按顺序扫描之后的元组,假设满足范围条件的元组有存放在文件中连续的 b b b 个块上,检索一次索引需要的块访问次数为 h h h ,则磁盘传输的块数为 b + h b+h b+h ,寻道次数为 h + 1 h+1 h+1

辅助索引

如果关系在对应属性或属性集上有辅助索引,可以利用它直接定位所有满足范围条件的元组,设满足条件的元组数为 n n n ,检索一次索引需要的块访问次数为 h h h ,最坏情况下需要磁盘传输 n + h n+h n+h 个块,寻道 n + h n+h n+h

和等值条件一样,只有在满足范围条件的元组较少的情况下,使用辅助索引才能加速选择运算

合取

实际应用中,选择条件不一定只有一个,可能会由若干个条件组成一个复合条件,当一个复合条件是若干个等值条件或范围条件的合取时,可以考虑利用索引进行加速

一维索引

一维索引是指以单个属性作为搜索码的索引,若当前的选择条件是若干个等值条件或范围条件的合取,假设其中某一个条件 θ \theta θ 对应的属性为 a a a ,并且关系上有一个属性 a a a 上的一维索引,则我们可以借助该索引筛选出满足条件 θ \theta θ 的全部元组,每筛选出一个满足条件 θ \theta θ 的元组,我们再检测其是否满足其它条件

执行该方案的代价约等于借助索引筛选出满足条件 θ \theta θ 的全部元组的代价,如果有多个一维索引满足条件,则优化器在制定执行计划之前需要先估算使用哪个一维索引筛选元组代价更小

高维索引

虽然我们之前讨论的 B+ 树索引和散列索引都是一维索引,但高维索引也是存在的,设 a 1 , a 2 , ⋯   , a n a_1,a_2, \cdots ,a_n a1,a2,,an 是组成复合条件的若干个等值条件或范围条件所对应的全部属性,若关系上存在一个在属性集 a 1 , a 2 , ⋯   , a n a_1,a_2, \cdots ,a_n a1,a2,,an 上的高维索引,则可以借助该高维索引直接定位满足所有等值条件和范围条件的元组

多个一维索引

如果组成复合条件的等值条件或范围条件中,绝大部分条件对应的属性上有一维索引,则可以分别利用这些索引,找到指向满足对应条件的元组的指针集合,再对所有的指针集合求交

对指针集合求交很简单,只需要对所有指针集合中的所有指针排序,然后只保留出现次数等于指针集合数的那些指针

如果组成复合条件的所有条件对应的属性上都有索引,那么运算至此就执行完毕了,但如果还有少部分条件对应的属性上没有索引,我们还需要访问我们刚刚求交得到的所有指针,检测其指向的元组是否满足剩下的少部分条件,由于在求交的过程中我们就给所有指针排好序了,因此我们访问指针的顺序和它们指向的元组在文件中的顺序一致

析取

当一个复合条件是若干个等值条件或范围条件的析取,也可以考虑用索引进行加速

多个一维索引

只有组成复合条件的所有条件对应的属性上有一维索引,我们才有可能借助索引加速,分别利用这些索引,找到指向满足对应条件的元组的指针集合,再对指针集合求并即可

指针集合求并也很简单,只需要给所有集合中的所有指针排序并去重

θ 1 , θ 2 , ⋯   , θ n \theta_1 , \theta_2, \cdots , \theta_n θ1,θ2,,θn 表示组成复合条件的所有条件, m 1 , m 2 , ⋯   , m n m_1,m_2, \cdots ,m_n m1,m2,,mn 分别表示满足 θ 1 , θ 2 , ⋯   , θ n \theta_1 , \theta_2, \cdots , \theta_n θ1,θ2,,θn 的元组个数,如果 ∑ i = 1 n m i \sum_{i=1}^nm_i i=1nmi 比较大,则该方案可能还不如扫描整个关系的通用方案

在组成复合条件的所有关系中,只要有一个条件对应的属性上没有索引,我们就应该使用扫描整个关系的通用方案,因为为了检测那个对应属性上没有索引的条件,我们必须扫描整个关系


排序


排序按待排序的元组能否全部装入内存中可以分成内部排序和外部排序,数据库系统中大多数排序是外部排序

排序的原因有两个:

  1. SQL 查询可能要求查询结果按某种顺序排序
  2. 事先将关系排序可以加速某些操作

外部归并排序算法

外部归并排序算法是最常见的外部排序算法之一,假设内存给排序分配了 M M M 个块的空间,此外还分配了一个块作为输出的缓冲区

外部归并排序算法分为两步:创建归并段、对归并段进行归并

算法第一步要将关系划分成若干个可以存放在内存中的归并段,并将每个归并段内部排好序,之后将每个排好序的归并段都以文件的形式写回磁盘中,通常情况下归并段越少越好,因此归并段的大小一般是 M M M 个块,刚好能存放在内存中,除非划分到最后一个归并段时,剩余的数据不足 M M M 个块

算法第二步就是对归并段进行归并,首先将磁盘中的归并段读入内存,不过不是一下子将某个归并段全部读入,而是将多个归并段各读一块进入内存,一般情况下我们一次将 M M M 个归并块各读一块进入内存(除非剩余归并段数量不足),之后开始对读入的这些归并段进行多路归并,当某个归并段被读入内存的所有元组全部被归并完之后,就再从磁盘中将该归并段的下一块读入内存

归并结束后,我们就得到一个新的归并段,我们把新归并段写到磁盘中,然后继续归并旧的归并段,当所有旧归并段被归并完之后,我们重复算法的第二步,只不过被归并的是新归并段,我们一直重复算法第二步,直到最新产生的归并段只有一个为止

外部归并排序的代价

创建归并段的代价很好计算,设关系总共有 B B B 个块,则我们一共会创建 ⌈ B M ⌉ \lceil \frac{B}{M} \rceil MB 个归并段,每个归并段需读写各一次,因此创建归并段时,读写操作会交叉进行,每次读写之前都需要寻道,寻道次数为 2 ⌈ B M ⌉ 2 \lceil \frac{B}{M} \rceil 2MB ,所有归并段中包含关系的所有的块,因此磁盘传输块数为 2 B 2B 2B

对归并段进行归并时,该步骤会重复若干次,由于每重复一次,归并段的数量就会变成原来的 1 M \frac{1}{M} M1 ,因此可以计算出,该步骤重复的次数为 log ⁡ M B M \log_{M}{\frac{B}{M}} logMMB

每执行一次第二步,我们会将关系的所有块都读写一次,并且读操作并不是连续的,中间会夹杂着写操作,最坏情况下,读操作完全不连续,每次读写之前都需要寻道,寻道次数为 2 B 2B 2B ,磁盘传输的块数显然也是 2 B 2B 2B

因此,最坏情况下,外部归并排序的总代价为:磁盘传输 2 B ( 1 + log ⁡ M B M ) 2B(1+ \log_{M}{\frac{B}{M}}) 2B(1+logMMB) 块,寻道 2 ⌈ B M ⌉ + 2 B log ⁡ M B M 2 \lceil \frac{B}{M} \rceil +2B \log_{M}{\frac{B}{M}} 2MB+2BlogMMB


连接运算


通用方案

嵌套循环连接(nested-loop join)

最通用最简单的执行连接运算的方案就是用一个二重循环,我们将两个待连接的关系其中之一指定为外层关系,另一个指定为内层关系,我们在外层循环中遍历外层关系,每遍历到一个外层关系的元组,就在内层循环中遍历一次内层关系,检测内层关系中的每个元组和当前遍历到的外层关系元组是否满足连接条件

假设两个待连接关系都在内存中存放不下,这就导致每次在内层循环中遍历内层关系时,我们都必须重新从磁盘中读取一次内层关系,设外层关系的元组数为 n n n ,块数为 B 1 B_1 B1 ,内层关系的块数为 B 2 B_2 B2 ,则磁盘传输块数为 B 1 + n ⋅ B 2 B_1+n \cdot B_2 B1+nB2 ,至于寻道次数,每次遍历内层关系之前肯定要寻道,而遍历外层关系时,虽然读取的外层关系的块在物理位置上连续,但遍历外层关系的过程中,磁头会因为要遍历内层关系而移走,因此每读取外层关系的一块,就需要寻道一次,总寻道次数为 n + B 1 n+B_1 n+B1

当两个关系中至少有一个关系可以完整地存放在内存中时,效率会大有改观,首先我们指定那个能被完整地存放在内存中的关系为内层关系,并在执行连接之前,将其完整地读入内存中,之后我们的 I/O 操作就只剩下读入外层关系的所有块,这时在遍历外层关系的时候,磁头不会再像之前那样移走,因此磁盘传输块数为 B 1 + B 2 B_1+B_2 B1+B2 ,寻道次数为两次

当两个关系中至少有一个可以完整地存放在内存中时,嵌套循环连接的效率达到连接运算的最佳效率,因为不管使用何种方案处理连接运算,都至少需要将待连接的两个关系各遍历一次,而嵌套循环连接在这种情况下的代价刚好等于将两个关系各遍历一次的代价

块嵌套循环连接(block nested-loop join)

嵌套循环连接可以进行改进,在嵌套循环连接中,每次遍历内层关系,只检测内层关系的元组和一个外层关系元组是否满足连接条件,如果可以检测每个内层关系元组和很多外层关系元组中的每个是否满足连接条件,就能减少内层关系的遍历次数

假设内存最多给外层关系分配 M M M 个块的空间,那么每次就读入外层关系的 M M M 块,然后遍历内层关系,检测每个内层关系元组和这 M M M 块外层关系中的每个元组是否满足连接条件,这样磁盘传输块数变为 B 1 + ⌈ B 1 M ⌉ ⋅ B 2 B_1+ \lceil \frac{B_1}{M} \rceil \cdot B_2 B1+MB1B2 ,寻道次数变为 2 ⌈ B 1 M ⌉ 2 \lceil \frac{B_1}{M} \rceil 2MB1

从代价分析可以看出,当两个关系都在内存中村放不下时,将块数更少的关系指定为外层关系能使效率更高

等值连接

等值连接(包括自然连接)一般有一些专用的方案

索引嵌套循环连接(indexed nested-loop join)

若待连接的两个关系中有一个关系在等值连接对应的属性上有索引,那么可以指定这个关系为内层关系,这样对于一个外层关系元组,寻找和它符合连接条件的内层关系元组时就不需要再遍历内层关系了,而是可以利用索引直接定位

若满足连接条件的元组对数目不多,索引嵌套循环连接可以大幅提升效率,尤其是在索引是主索引的情况下

不光是等值连接,只要连接条件能够借助内层关系的索引加速,都可以使用索引嵌套循环连接

归并连接(merge join)

如果待连接的两个关系都按等值连接对应的属性排序,就可以通过一次归并来找到所有满足等值条件的元组对,因为归并的过程中,等值连接对应的属性相等的元组总是会以相邻的顺序被扫描到,扫描出对应属性在某个取值上的全部元组之后,就可以开始连接了,当然,对应属性可能在某个取值上有过多的元组,导致内存存放不下,这时候将存不下的元组写到磁盘中,然后使用块嵌套循环连接即可

假设内存给两个关系各分配了 M M M 个块的空间作为输入缓冲区,则磁盘传输块数为 B 1 + B 2 B_1+B_2 B1+B2 ,寻道次数为 ⌈ B 1 M ⌉ + ⌈ B 2 M ⌉ \lceil \frac{B_1}{M} \rceil + \lceil \frac{B_2}{M} \rceil MB1+MB2 ,若对应属性在某些取值上有过多元组,导致内存存放不下,则代价会有轻微的增加

可以看出,在内存空间较为充裕时,归并连接的效率很高

混合归并连接(hybrid merge join)

归并连接要求待连接的关系都按连接对应的属性排序,若关系未按对应属性排序,一般的做法是在连接之前先进行一次排序,但如果未按对应属性排序的关系上有对应属性的 B+ 树辅助索引,我们还有其它的办法

有一棵 B+ 树索引,就意味着可以通过遍历 B+ 树的叶节点,知道所有元组在对应属性上的取值,因此我们仍然可以进行归并的过程,但遍历的不是整个关系,而是 B+ 树的叶节点,并且归并的输出结果也有所不同,利用两个关系进行归并,输出是元组对,而如果利用两棵 B+ 树归并,输出则是指针对,因为 B+ 树叶节点中只存放了指向元组的指针

最后,我们将指针对中的指针按物理地址排序,然后按照顺序依次访问其指向的元组,将指针对恢复成元组对,由于访问按照物理顺序,因此效率会非常高

混合归并连接的好处在于不需要对整个关系进行排序,而只需要对连接结果中的指针进行排序,当满足连接条件的元组对较少时,混合归并连接的效率相比于归并连接大大提升

如果我们有一个关系按照对应属性排序,而另一个没有排序,依然可以使用混合归并连接,只不过归并输出的结果是元组 — 指针对

散列连接(hash join)

利用散列也可以执行等值连接,首先找一个散列函数,可以把连接对应的属性映射到 [ 1 , n ] [1,n] [1,n] 区间内的整数,设待连接的两个关系为 r r r s s s ,那么就可以利用这个散列函数,将关系 r r r 中的元组映射到 r 1 , r 2 , ⋯   , r n r_1,r_2, \cdots ,r_n r1,r2,,rn n n n 个桶中,同时将关系 s s s 中的元组映射到 s 1 , s 2 , ⋯   , s n s_1,s_2, \cdots ,s_n s1,s2,,sn n n n 个桶中

映射完成之后,对于 ∀ i ∈ [ 1 , n ] \forall i \in [1,n] i[1,n] r i r_i ri 中的元组只可能与 s i s_i si 中的元组连接,我们只需要依次将 r 1 r_1 r1 s 1 s_1 s1 r 2 r_2 r2 s 2 s_2 s2 ⋯ \cdots r n r_n rn s n s_n sn 读入内存,然后在内存中进行连接即可

对内存中的元组进行等值连接时,我们可以借鉴散列连接的思路,例如我们将 r i r_i ri s i s_i si 中全部元组已经读入内存了,我们可以在 r i r_i ri 上创建一个存放在内存中的散列索引,然后对于 s i s_i si 中每一个元组,可以直接在散列索引中检索所有对应属性上和其相等的 r i r_i ri 中的元组,当然,我们给 r i r_i ri 创建的散列索引所用的散列函数一定要和之前用来划分关系 r r r s s s 的散列函数不同

目前还存在着一个问题,对于 ∀ i ∈ [ 1 , n ] \forall i \in [1,n] i[1,n] ,我们必须要能将 r i r_i ri s i s_i si 完整地存放在内存中,同时还需要一些内存空间来存放散列索引,所以内存空间有可能不足,这时候我们有两个解决办法

第一个解决办法就是,如果 ∃ i ∈ [ 1 , n ] \exist i \in [1,n] i[1,n] ,使得 r i r_i ri s i s_i si 无法完整地存放在内存中,我们就递归地用散列函数将 r i r_i ri s i s_i si 中的元组映射到更多的桶中,当然,这个散列函数必须和之前的散列函数不同

第一个解决方法并不万能,因为可能会存在某种取值,取这种取值的元组特别多,内存存放不下,这时无论怎样递归地将元组映射到更多的桶中,总会存在一对内存存放不下的 r i r_i ri s i s_i si ,遇到这种情况时,我们干脆就保留这对内存存放不下的 r i r_i ri s i s_i si ,将它们写入磁盘中,然后使用块嵌套循环连接来处理它们

假设所有的 r i r_i ri s i s_i si 都能完整地存放在内存中,我们来分析散列连接的代价,设内存可以给关系 r r r s s s 各分配 M M M 个块的空间作为输入缓冲区,同时给每个桶分配 M M M 个块的空间作为输出缓冲区

一开始用散列函数划分关系 r r r s s s 时,先得把两个关系的所有块各读入内存一次,然后我们需要把所有桶的所有块写入磁盘,桶中存放的是两个关系中的全部元组,但所有桶的块之和要略大于 B 1 + B 2 B_1+B_2 B1+B2 ,因为每个桶可能会有一个不满的块,简单起见,我们忽略这个误差,最后我们还要将所有桶各读入内存一遍,因此磁盘传输的块数约为 3 ( B 1 + B 2 ) 3(B_1+B_2) 3(B1+B2)

由于输入缓冲区和输出缓冲区的大小都是 M M M 个块,因此我们在用散列函数划分关系 r r r s s s 时,需要寻道 2 ( ⌈ B 1 M ⌉ + ⌈ B 2 M ⌉ ) 2( \lceil \frac{B_1}{M} \rceil + \lceil \frac{B_2}{M} \rceil ) 2(MB1+MB2) 次,最后我们还要将所有桶各读入内存一次,因为一个桶的所有块的地址是连续的,因此一个桶只需要寻道一次,总寻道次数为 2 ( ⌈ B 1 M ⌉ + ⌈ B 2 M ⌉ + n ) 2( \lceil \frac{B_1}{M} \rceil + \lceil \frac{B_2}{M} \rceil +n) 2(MB1+MB2+n)

可以看出,类似于归并连接,散列连接也拥有非常高的效率,并且不需要待连接的关系按对应属性排序,因此如果待连接的关系是无序的,通常采用散列连接

复杂连接

连接的条件并不一定是一个,有时会是多个条件的合取或析取

合取

如果连接的条件是多个条件的合取,可以先只考虑某一个条件,按前面的方案找出所有满足这个条件的元组对,最后再检查这些元组对是否满足其它条件

先考虑哪个条件是有讲究的,要使满足该条件的元组对尽可能少,同时,最好有一个效率较高的方案能够找出所有满足这个条件的元组对,比如归并连接或散列连接

析取

如果连接的条件是多个条件的析取,可以依次考虑每个条件,分别找出所有满足当前条件的元组对,最后对找到的所有元组对集合求并集,就能得到结果,之后会讲如何求集合的并,不过在很多情况下,这种方案的效率还不如通用的块嵌套循环连接


其它运算


去重

对于一个已经排好序的关系去重十分简单,只需要按顺序扫描整个关系,然后比较每个元组和前一个元组是否在所有属性上取值都相等,如果都相等,则删除该元组,否则保留该元组

如果一个关系并没有排序,可以考虑先对其进行排序,然后再去重,但这样可能会比较慢,更有效率的做法是,首先使用一个散列函数将关系中的元组映射到不同的桶中,然后依次将每个桶读入内存,在内存中使用另一个散列函数为该桶创建散列索引,在创建散列索引时,只插入未在索引中出现过的元组,如果索引中已经存在和待插入元组相同的元组,则待插入的元组被抛弃,最后,将散列索引中的元组写到结果中

集合运算

一个关系可以视为元组的集合,因此可以对关系进行集合运算,但前提是进行集合运算的关系的模式必须相同

如果两个关系都按同一属性或属性集排序,可以稍微修改一下归并的过程,从而实现集合运算,如果我们要求集合并,在归并中扫描到相同的元组时,只保留一个,如果求集合交,只保留两个关系中同时出现的元组,如果求集合差,只保留一个关系中出现,而另一个关系中不出现的元组

当然,要求两个关系按同一属性或属性集排序有些过于苛刻,当这个条件不满足时,我们可以选择利用散列,假设将要对关系 r r r s s s 进行集合运算, r 1 , r 2 , ⋯   , r n r_1,r_2, \cdots ,r_n r1,r2,,rn s 1 , s 2 , ⋯   , s n s_1,s_2, \cdots ,s_n s1,s2,,sn 都是桶,首先用同一个散列函数,将关系 r r r 中的元组映射到 r 1 , r 2 , ⋯   , r n r_1,r_2, \cdots ,r_n r1,r2,,rn 中,将关系 s s s 中的元组映射到 s 1 , s 2 , ⋯   , s n s_1,s_2, \cdots ,s_n s1,s2,,sn

之后依次将 r 1 r_1 r1 s 1 s_1 s1 r 2 r_2 r2 s 2 s_2 s2 r n r_n rn s n s_n sn 读入内存中,假设当前已经将 r i r_i ri s i s_i si 读入内存,直接在内存中为 r i r_i ri 建立散列索引,当然散列函数和之前用于划分关系 r r r s s s 的散列函数不同,然后我们根据集合运算的不同执行不同的操作

若我们要求集合并,则对于 s i s_i si 中的元组,如果其不在散列索引中,将其加入散列索引,最后将散列索引中的元组写入结果中

若我们要求集合交,则对于 s i s_i si 中的元组,如果其在散列索引中,将其写入结果中

若我们要求集合差 r − s r-s rs ,则对于 s i s_i si 中的元组,如果其在散列索引中,则从散列索引中将其删去,最后将散列索引中的元组写入结果中

外连接

最通用的处理外连接的方法是,先计算对应的内连接,将连接结果保存起来,然后通过集合差运算求出外连接相比于内连接的额外元组,将额外的元组写入结果中

当外连接的连接条件是等值条件时,可以略微修改归并连接或散列连接的方案,来执行外连接

聚集

聚集的执行方案可以通过略微修改去重的方案得来,在对聚集属性去重的过程中,我们可以把在聚集属性上取值相同的元组放在一起处理,从而统计出聚集函数的值

你可能感兴趣的:(数据库)