4.1 物理查询计划操作符介绍
物理查询计划由操作符构成,每一个操作符实现计划中的一步。物理操作符通常是一个关系代数操作符的特定实现。
4.1.1 扫描表
读关系R的整个内容。
- 表扫描:顺序读块,一个接一个的读取。
- 索引扫描:读索引,根据索引找到块,然后读取。
4.1.2 扫描表时的排序
为啥要排序?其一,sql有order by,要求对关系排序;其二,很多关系代数操作算法要求数据是有序的。
如何实现排序呢?如按属性a对关系R进行排序。
- 如关系本身是按属性a建立的索引,如B+树索引或者其它按a排序的索引,那么直接扫描即可
- 如排序的关系R很小,则可以先进行表扫描读取到内存中,然后内存排序
- 如排序的关系R很大,无法一次全部load到内存中,则可以进行多路归并排序
4.1.3 物理操作符计算模型
如何估计每个操作符的代价呢?由于从磁盘中得到数据的时间比从内存中大的多,因此,使用磁盘IO的数目作为衡量每个操作的代价标准。
另外,有一个假设。假设,任何操作符的操作对象都位于磁盘上,但操作符的结果放在内存中。
4.1.4 衡量代价的参数
有以下三个参数
- 关系数据库数据B。我们关心包含R的所有元组所需的块的数目,这个块的数目表示成
B(R)
,如果我们知道指的是关系R,则可以表示成B。通常,假设R是聚集的,因此R存储在B个块中。 - 元组条数T。关系R的所有元组条数为T,那么一个块中的元组个数为
T/B
。关系中数据的多少。 - 字段不同值的个数V。关系中数据的分布。
我们需要用一个参数来表达操作符使用的内存大小,假设内存被分成缓冲区,缓冲区的大小与磁盘块的大小相同。那么M表示一个特定的操作符执行时可以获得的内存缓冲区的数目。
4.1.5 扫描操作符的IO代价
如果关系R是聚集的,那么表扫描操作符的磁盘IO数目近似为B。
如果关系R不是聚集的,那么表扫描操作符的磁盘IO数目可能退化为T。
4.1.6 实现物理操作符的迭代器
许多物理操作符可以实现为迭代器。迭代器是三个方法的集合,这三个方法允许物理操作符结果的使用者一次一个元组地得到这个结果。
- Open()
- GetNext()
- Close()
为什么要使用迭代器?
迭代器与物化策略相反,物化策略产生每个操作符的整个结果,或者将它存放在磁盘上,或者允许它在内存中占据空间。在使用迭代器时,同一时刻活跃的操作有很多。元组按需要在操作符之间传递,这样就减少了存储要求。但是,并非所有物理操作符对迭代方法或流水线的支持都是有意义的。在某些情况下,几乎所有的工作都需要Open方法来完成,这就等效于物化方法了。
4.2 一趟算法
怎么执行逻辑查询计划中的每个单独步骤?每一个操作符的算法是将逻辑查询计划转变成物理查询计划过程中一个必不可少的部分。
关于各种操作符已提出很多算法,大体上分为三类:
- 基于排序的方法
- 基于散列的方法
- 基于索引的方法
按照代价,可以分为三种:
- 一趟算法。仅从磁盘读取一次数据,要求操作的至少一个对象能完成装入内存
- 两趟算法。数据量大,以至于不能装入内存。特点是,先从磁盘读一遍数据,用某种方式处理后,将全部或绝大部分写回磁盘,然后在第二趟中为了进一步处理,再读取一遍数据。
- 多趟算法。
操作符类型,可以分为三种:
- 一次单个元组,一元操作。如选择,投影,一次读取一个元组,就可以进行处理
- 整个关系,一元操作。如分组操作和去重操作
- 整个关系,二元操作。如并,交,差,连接和积的集合形式以及包形式。
4.2.1 一次的单个元组操作的一趟算法
如选择和投影操作。
一次读取R的一块到输入缓冲区,对每一个元组进行操作,并将选出的元组或投影得到的元组移至输出缓冲区。无论关系R有多少块B,只要内存中有一个输入缓冲区就可以,即M≥1。
4.2.2 整个关系的一元操作的一趟算法
如去重和分组聚合操作。
去重
一次一个地读取R的每一块,对每一个元组:
- 第一次看到这个元组,这时将它复制到输出;
- 之前见到过这个元组,就不用复制到输出了。
关键是如何判断元组是否见到过,可以采用具有大量桶的散列表,或者某种形式的平衡二叉查找树。
分组
分组操作,有零个或者多个分组属性,一个或多个聚集属性。如下sql,user_id就是分组属性,money就是聚集属性。在内存中为每一个组(分组属性的每一个值,如本例中的张三,李四)创建一个项,我们可以一次一块地扫描R的元组。每个组的项包括分组属性的值和每个聚集的一个或多个累计值。如
- MIN和MAX操作,比大小,每次处理一个元组时,如果合适,就更新这个值
- COUNT操作,每次处理一个元组时,就累加1
- SUM操作,累加属性值
- AVG操作,保持两个累加值,一个是个数,一个是属性累加值,待关系读取完后,计算均值
select user_id, sum(money) from salary group by user_id
4.2.3 二元操作的一趟算法
如并,交,差,积和连接。
假设R和S两个关系,R是数据较多的关系,S是数据较少的关系。将较少数据的S读取到内存中,并建立合适的数据结构,该数据结构(散列or平衡树)要求元组可以快速插入,还可以快速检索。
集合并
将S读取到内存中并建立查找结构。所有这些元组都复制到输出。然后一次读取R的一个块,对于R的每一个元组,在内存中判断是否在S中,如果不在,则复制到输出,否则,跳过。
集合交
与集合并唯一的不同时,不将S的元素都复制到输出。
集合差
将S读取到内存中并建立查找结构。需考虑两种情况:
- R - S情况。一次读取R的一个块,检查每个元组,如果元组不在S中,则复制到输出,否则,不处理。
- S - R情况。一个读取R的一个块,检查每个元组,如果元组在S中,则从S的内存副本中删除元组,否则,不处理。最后留在S中的元组,全部复制到输出。
积
笛卡尔积操作。将S读取到内存,不需要建立特殊的数据结构,因为不需要进行查找操作。
然后读取R的每一个块,并对块中的每一个元组,与内存中的S的每一个元组连接。输出每一个连接而成元组。
4.3 嵌套循环连接
一趟半算法,两个操作对象中有有一个元组仅读取一次,另一个操作对象将重复读取。嵌套循环连接可以用于任何大小的关系,不要求有一个关系必须能装入内存中。
4.3.1 基于元组的嵌套循环连接
嵌套循环连接中最简单的形式。计算R(X,Y)与S(Y,Z)的连接,逻辑如下
for(元组a in S){
for(元组b in R){
if(元组 a 和元组 b能形成元组t){
则输出t
}
}
}
4.3.2 基于元组的嵌套循环连接的迭代器
适合用于迭代器结构。
4.3.3 基于块的循环嵌套连接算法
假设S和R,每个关系都不能一次性全部加载到内存,S是块数较小的关系。则先将S的若干块加载到内存中,并建立查找结构。然后将R的块依次加载到内存,并判断R中的元组能否与S中的元组建立连接,能则输出,不能则跳过。外层循环是块数少的S,内层要循环多次,即R要处理多次。
4.4 基于排序的两趟算法
两趟算法,来自操作对象关系中的数据被读到内存,以某种方式处理,再写会磁盘,然后重新读取磁盘以完成操作。为什么要这样做?因为数据量的关系,太大了以至于无法全部一次性load进入内存中。
例如,关系R可以被分成M个子表,每个字表可以再被分为M个缓冲区,内存有M个缓存区,因此第一趟,先对单独的子表进行排序,第二趟,对所有的子表,先取子表的第一块到内存中,归并排序。这样就实现了,无法一次load这么多数据,但是依然可以对整体进行排序。
4.4.1 两阶段多路归并排序
两阶段多路归并排序,算法如下
- 找到所有子表的第一个元素的最小值,可以使用优先队列(堆)结构实现
- 将最小元素移到输出块的第一个可用位置
- 如果输出块满了,则溢出到磁盘存储,重新创建一个输出块
- 如果输入缓冲区的块的数据被移除完了,则用该子表的下一个数据块(有序)来补充,如果子表读取完了,则该输入缓冲区为空
复杂度,第一遍读取一次磁盘块B(R),写一次磁盘块B(R),第二遍,读取一次磁盘块B(R),总计3B(R)次磁盘IO,而且要求B(R)<=M*M。
4.4.2 利用排序去重
与上述算法类似,第一遍先排序,形成多个有序子表;区别在于第二遍,在可用内存中为每个有序子表分配一个缓冲块,并保存一个输出缓冲块。遍历每一块的第一个元组,然后判断该元组是否在输出缓冲块中,如果在了,则忽略掉该元组,并删除掉子表中所有属性值与该元组相同的元组。当内存输入缓冲块为空后,将该缓冲块所属子表的后续数据库,加载到内存,进行处理;如果子表的数据块都处理完成,则该内存空间为空。当输出缓冲块写满后,就溢出到磁盘。
复杂度,第一遍读取一次磁盘块B(R),写一次磁盘块B(R),第二遍,读取一次磁盘块B(R),总计3B(R)次磁盘IO,而且要求B(R)<=M*M。
4.4.3 利用排序进行分组和聚集
逻辑如下
- 第一遍,读取R,可以Load进入M个缓冲块到内存。然后根据分组字段对这些缓冲块单独排序,将每一个排好序的子表写到磁盘。
- 第二遍,重新load排好序的子表,M个缓存块对应M个子表
- 在缓存块中可以获得的每一个元组中反复查找排序关键字的最小值。这个最小值v成为下一分组。
a) 准备在这个分组的列表L上计算所有的聚集,如计数和求和来代替平均。
b) 检查每个排序关键字为v的元组,并累计所需的聚集。
c) 如果一个缓冲区空了,则用同一个子表的下一个块替代它。
当不在有排序关键字v的元组时,输出一个有L的分组属性和对应的聚集值构成的元组。
复杂度,第一遍读取一次磁盘块B(R),写一次磁盘块B(R),第二遍,读取一次磁盘块B(R),总计3B(R)次磁盘IO,而且要求B(R)<=M*M。
4.4.4 基于排序的并
- 第一趟,创建R和S的排序子表
- 为R和S的每个子表使用一个内存缓冲区,用对应子表的每一块初始化各缓冲区
- 重复地在所有缓冲区中查找剩余块的第一个元组。将元组复制到输出,并从缓冲区中删除元组所有的副本。当输入缓冲区变空或者输出缓冲变满时,采用与2PMMS相同的方式进行处理。
需要读取3(B(R)+B(S))次磁盘IO
4.4.5 基于排序的交和差算法
与4.4.4基本相同,区别的一点在于如何处理排序子表前部的元组t的副本。
- 交,如果元组在R和S中都出现,就输出
- 差,R-S,当且仅当,t出现在R,但t没出现在S中,才会输出
需要读取3(B(R)+B(S))次磁盘IO