数据库的三大类操作
- 一次单一元组的一元操作–select, projection
- 整个关系的一元操作–distinct, group by, sort
- 整个关系的二元操作:并,交,差,笛卡尔积,join
对于一次单一元组的一元操作,有迭代器算法;对于整个关系的一元操作,有一趟扫描算法,二趟扫描算法,多趟扫描算法。扫描多次的主要原因是内存容量不够,这些扫描算法的实现都有基于排序、散列和索引的
1. 一次单一元组的一元操作
查询实现有两种策略,一种是物化计算策略,一种是流水线计算策略。其中物化策略的每一个操作都要扫描一遍数据库,且存储中间结果;流水线策略一组关系操作才扫描一遍数据库,不存储中间结果。流水线策略可用迭代器算法实现
迭代器的构造
表空间扫描法读取关系
假设R是一个类,以下是伪代码
Open(){
b:=R的第一块
t:=b的第一个元组
}
GetNext(){
if(t已超过块b的最后一个元组){
将b前进到下一块
if(没有下一块)
return notFound
else/*b是一个新块*/
t:=b的第一个元组;
}
oldt:=t;
将t前进到b的下一个元组;
return oldt;
}
Close(){
}
实现并操作
假设Union(R,S)是一个类
Open(){
R.Open();
CurRel:=R;//当前关系为R
}
GetNext(){
if(CurRel==R){
t:=R.GetNext();
if(t<>notfound)
return t;//未处理完
else{//已处理完R
S.Open();
CurRel:=S;
}
}
return S.GetNext();
}
Close(){
R.close();
S.close();
}
实现selection操作
Selection(R)
Open(){
R.Open();
}
GetNext(){
cont://一个条件循环
t:=R.GetNext();
if(t<>notfound)
if(F(t)==True)//条件判断
return t;
else goto cont;
else return notfound;
}
Close(){
R.Close();
}
在selection操作的基础上做projection操作
Projection(Selection(R))
Open(){
Selection.Open();
}
GetNext(){
t:=Selection.GetNext();
if(t<>notfound){
p:=Projection(t,alpha)//alpha为投影内容
return p
}
else return notfound;
}
Close(){
Selection.Close();
}
这样就可以自动地从前一个操作的迭代器构造后一个操作的迭代器
迭代器实现Join
Join(R,S)
Open(){
R.Open();
S.Open();
r:=R.GetNext();
}
GetNext(){
repeat{
s:=s.GetNext();
if(s==notfound){
S.Close();
r:=R.GetNext();
if(r==notfound)
return notfound;
else{
S.Open();
s:=S.GetNext();
}
}
}
until(r与s能够连接)
return r和s的连接
}
Close(){
R.Close();
S.Close();
}
2. 整个关系的一元操作
对于关系R,假设B(R)是R的存储块数目;T(R)是R的元组数目
一趟扫描:
- 对于聚簇关系——关系的元组集中存放(一个块中仅是一个关系中的元组):
表空间扫描算法:
TableScan(R)–扫描结果未排序:B(R)
SortTableScan(R)–扫描结果排序:3B(R)
索引扫描算法:
IndexScan(R)–扫描结果未排序:B(R)
SortIndexScan(R)–扫描结果排序:B(R) or 3B(R) - 对于非聚簇关系——关系元组不一定集中存放(一个块中不仅是一个关系中的元组):
扫描结果未排序:T(R)
扫描结果排序:T(R)+2B(R)(可以理解为排序后变为聚簇的)
去重复操作:&(R)
- 需要在内存中保存已处理过的元组
- 当新元组到达时,需与之前处理过的元组进行比较
- 建立不同的内存数据结构(比如散列表,B+树),来保存之前处理过的数据,以便快速处理整个关系上的操作(用于快速定位元组)
算法复杂度为B(R)
两趟扫描的去重复操作:复杂度和两趟扫描排序相同,考虑输出是4Bproblem4B_{problem}4Bproblem,不考虑输出是3Bproblem3B_{problem}3Bproblem
分组聚集γL(R)\gamma_{L}(R)γL(R),γ\gammaγ为分组计算符号,L为分组条件
- 需要在内存中保存所有的分组
- 保存每个分组上的聚集信息
- 建立不同的内存数据结构(比如散列表,B+树),来保存之前处理过的数据,以便快速处理整个关系上的操作(用于快速定位元组)
类似去重复操作,算法复杂度为B(R)
比如使用散列,则散列函数可以是分组条件的函数;新分组通过散列插入相应的桶(页)中;新元组通过散列找到相应的桶,并判断是否是新分组
两趟扫描:第一趟分组并子表排序;第二趟,归并阶段,在排序的基础上,将不重复的记录作为新分组输出,将重复的记录进行分组聚集计算。复杂度和两趟扫描排序相同,考虑输出是4Bproblem4B_{problem}4Bproblem,不考虑输出是3Bproblem3B_{problem}3Bproblem
两趟扫描
假设内存只有8块,如何排序70块的数据集?
问题是在全部数据上的操作是否等价于在子集上的操作的并集?
例如:元组在某一子集上无重复即在全集上无重复。
基于散列的两趟扫描算法可以满足这一条件
- 基于散列的两趟扫描算法
大数据集上的操作可以被转换为某个子集上的操作:
第一趟:散列子表。用散列函数hph_{p}hp将原始关系划分成M-1个子表,并存储。
第二趟:处理每个子表。用另一散列函数hrh_{r}hr将子表读入内存并建立内存结构。复杂性与上述两趟扫描的复杂性相同
对于去重复操作:元组在子表上不重复,则在大关系中亦不重复。HpH_{p}Hp将可能重复的元组散列到同一子表,hrh_{r}hr将可能重复的元组散列到同一内存块中。
对于分组操作:第一趟散列时应该同一个分组在同一子表中;同样第二趟散列时同一个分组应散列到同一个内存块中。两趟散列都应该针对“分组属性”的值进行计算。但可以形成区别,如HpH_{p}Hp直接计算分组属性,hrh_{r}hr用“分组属性”的二进制位串。复杂性与上述两趟扫描的相同。
对于基于排序的两趟扫描算法可以满足 在多个已按横向处理的子集上,纵向归并结果等同于在全集上的处理结果。
- 两阶段多路归并排序(two-phase, multiway merge-sort, TPMMS)
- 内排序问题和外排序问题的区别:内排序问题–待排序的数据可一次性地装入内存中,即排序这可以完整地看到和操作所有数据。 外排序问题–所有的数据不能一次性装入内存
- 基本排序策略。假设所有的数据需要磁盘块BproblemB_{problem}Bproblem块,内存有BmemoryB_{memory}Bmemory块,BproblemB_{problem}Bproblem远大于BmemoryB_{memory}Bmemory。则可把BproblemB_{problem}Bproblem块数据划分为N个子集合,使得每个子集合的块数小于内存可用的块数,即Bproblem/N<BmemoryB_{problem}/N<B_{memory}Bproblem/N<Bmemory。每个子集合都可装入内存并采用内排序算法排好并重新写回磁盘,这一过程为第一趟排序,也可以叫横向处理。于是问题转化为:N个已排序子集合的数据怎样利用内存进行总排序,这一步称为第二趟排序,是各子集间的归并排序,每个子集都被读取一部分内容放入内存中
算法的效率:子集合排序阶段读一遍写一遍,代价为2Bproblem2B_{problem}2Bproblem,归并阶段读一遍写一遍,代价为2Bproblem2B_{problem}2Bproblem,总共为4Bproblem4B_{problem}4Bproblem.要求是大数据集块数<Bmemory2B_{memory}^2Bmemory2
更大规模的数据集则考虑一多趟/多阶段排序。假设内存大小Bmemory=3B_{memory}=3Bmemory=3,待排序数据Bproblem=30B_{problem}=30Bproblem=30。
基本策略:
1.将30块的数据集划分为10个子集合,每个子集合3块,排序并存储。
2.将10个已排序的子集合分为5组,每组两个子集合,分别进行二路归并,则可得到5个排好序的集合
3.5个集合再分成3个组:每个组两个子集合,剩余一个单独一组,分别进行二路归并,可得到3个排好序的集合;再分组再归并得到两个排好序的集合;再分组再归并便可完成最终的排序
3.整个关系的二元操作
一趟扫描
先扫描一个关系,再去扫描另一个关系。在集合上的操作(需要去重复)和在包上的操作(需要记录每个元组出现的次数)有所不同。
算法复杂度:B(R)+B(S)
基于有序索引的连接算法–zig-zag连接算法
两趟扫描
包上的操作都无需两趟,直接合并即可。集合上的操作需要两趟。
基于散列的两趟扫描
- 第一趟:使用相同的散列函数散列两个操作对象,如R和S,形成子表RiR_{i}Ri和SiS_{i}Si
- 第二趟:将SiS_{i}Si再整体散列读入到内存中,再依次处理RiR_{i}Ri的每一块。如判断在RiR_{i}Ri,SiS_{i}Si都出现元组t,则只输出t的一个副本,否则输出RiR_{i}Ri和SiS_{i}Si
连接操作:
基于散列的两趟扫描十分适合连接操作,
- 第一趟:使用相同散列函数(散列的属性为连接属性)散列两个操作对象R和S
- 第二趟:将SiS_{i}Si再整体散列读入到内存中,再依次处理RiR_{i}Ri的每一块。进行连接。(散列到相同位置的RiR_{i}Ri和SiS_{i}Si具有相同的连接属性值)