数据库系统实现 选择投影连接SPJ实验报告Word格式文档下载.docx
《数据库系统实现 选择投影连接SPJ实验报告Word格式文档下载.docx》由会员分享,可在线阅读,更多相关《数据库系统实现 选择投影连接SPJ实验报告Word格式文档下载.docx(13页珍藏版)》请在冰豆网上搜索。
thtupofthecurpage*/
intlen;
/*tuplen*/
};
structiteratoropen_iter(constchar*tbname);
intgetnext_iter(structiterator*iter,char*rec);
/*fetchnexttuple,storeinrec*/
intclose_iter(structiterator*iter);
由于我们对记录的存储是聚簇的,所以我们的迭代器是一次取出一个块,然后一条一条的抛出记录,这个块中的记录用完之后再读下一个块,直到读出了这个表中的所有的块,得到了表中的所有记录。
在iterator结构中,extent,blk,t分别表示当前的区间,块,记录位置;
b[PAGE_SIZE]存储了当前取出的块,len是记录的长度。
open_iter()是打开一个迭代器,并进行初使化;
close_iter()是关闭这个迭代器;
getnext_iter()是得到下一条元组。
具体的实现在scantable.c里面。
2.SPJ结果的输出:
在进行完选择和连接操作之后,得到的结果我们想返回给用户。
我们实现了Oracle里里面SQL*plus界面里的返回结果的样式,即把结果表用字符界面显示出来。
也在头文件scantable.h里面,如下所示:
intprint_tuple(char*rec,structtable_def*td);
intprint_tbhdr(structtable_def*td);
print_tbhdr()是显示结果表的表头,print_tuple()是显示结果表里的所有记录。
在实现上,因为我们的每一条记录都是以char[]存储在数据文件里面,所以这个函数就是从记录头里面得到记录的模式信息,然后按记录模式逐个解析这条元组,然后按一定的形式显示。
3.内存查找结构的实现:
由于在下面的实验(基于块的嵌套循环连接以及一些其它的一元操作)中要用到内存的查找结构:
能在接近常量的时间内增加一个新元组,查找一个元组是否存在。
所以我们需要实现一个这样的数据结构。
这样的结构很多,像hash和平衡搜索树等等。
我们选择了使用AVL树,也即平衡搜索树。
因为我们觉得虽然时间性能上面AVL不如hash,但是在这个结构的管理上面,AVL还是有优势的。
具体实现的接口如下(头文件avl.h中):
structBNode//definetypeofthenode
{
intnum;
//numberofnodes
intbf;
//balance-factor(1,0,-1:
thetreeisbalance)
KeyTypekey;
//keyword
InfoTypedata;
//storedata
structnode*lchild,*rchild;
//leftchild,rightchild
structnode*same;
//具有相同的key,但是其他信息不同
/*若平衡二叉树b中不存在与e相同的节点,则插入,并返回1;
否则返回0
**@b:
在b指向的平衡二叉树中插入数据
**@e:
待插入的关键字
**@taller:
表示这颗树是否增高*/
structBNode*InsertBT(structBNode*b,structRecorde,int*taller);
/*@b:
平衡二叉树据
**@k:
待查找的关键字
找到节点后返回该节点*/
structResult*SearchBT(structBNode*b,KeyTypek);
/*回收二叉树的内存空间*/
voidClearBT(structBNode*b);
BNode是AVL树的结点的定义,每个结点里面保存了相应的key值和相应的元组数据和其它的一些控制信息(像左右孩子节点的指针);
InsertBT()函数实现了向AVL树中插入一条元组的功能;
SearchBT()函数是返回AVL树中具有相应key值的所有元组;
ClearBT()函数负责清理申请的内存空间。
4.select:
无条件选择的实现:
所谓无条件选择,就是取出表中的所有记录。
实现上很简单,用表扫描算法,一条一条的得到记录就可以了。
因为我们已经实现了迭代器,所以就是从迭代器中得到每一条记录,把记录放到结果集里面。
因为我们的数据库存储,在每一个索引上面都不是聚簇的,所以我们在进行全表扫描的时候并没有用索引扫描(因为当索引不是聚簇时,进行索引扫描的代价是很高的)。
代码见下,在源文件select.c里面:
inttot_sel(constchar*tbname)
......
print_tbhdr(td);
structiteratoriter=open_iter(tbname);
while(getnext_iter(&
iter,rec)==0){
print_tuple(rec,td);
}
close_iter(&
iter);
return0;
}
5.select:
等值条件选择的实现:
当选择条件是等值的情况时,当选择条件上面建了索引的时候,可以用索引进行选择操作。
即在索引里面查找条件里的键值,得到记录里面满足条件的所有记录的地址,然后从这些地址里面取出所对应的各条记录。
我们不需要扫描整个表。
intequal_sel(intkey,constchar*tbname)
if((res=btree_search(key,table,&
nid,&
idx))!
=-1){
print_tbhdr(tdef);
//对每一条满足条件的记录
for(;
idx<
bnode->
d;
idx++){
print_tuple(rec,tdef);
}
对于不等值查询,我们没有别的办法,只能进行全表扫描,然后对每一条记录进行条件判定,若它满足相应的不等值条件,就把它放到结果集里面。
6.select:
范围条件选择的实现:
对于范围查询,当我们在查找条件上面建立了B+树索引的时候,可以用索引来进行。
首先从索引里面找到范围里面最左边的值的记录,若没有的话,找到比该值大的最小的key值的记录。
然后在索引树上面向右逐个取出记录对应的地址,从数据里面得到相应的记录,直到索引树上的键值不在这个范围内时停止。
对应这个查找过程,我们需要一个有效的索引扫描算法,为此我们修改了一下B+树索引(btree.h)的一点实现,修改了一下里面查找的实现,使它能像迭代器一样一个个地返回当前位置右边的记录地址。
然后范围查询的实现如下,也在select.c里面:
intrange_sel(intleft,intright,constchar*tbname)
print_tbhdr(tdef);
btree_search(left,table,&
idx);
while(nid!
//对每一个满足条件的值
if(bnode->
key[idx]>
right)//不满足条件,跳出
break;
7.product:
笛卡尔积实现:
对于product操作,我们没有其它的办法,只能对两个表的任何两条记录分别进行连接。
所以实现上很简单:
取出第一个表的每一条记录,分别与第二个表中记录做product。
具体实现在join.c里面,如下:
intproduct(constchar*tb1,constchar*tb2)
itertb1=open_iter(tb1);
//对tb1里的每一条记录
itertb1,rec1)==0){
itertb2=open_iter(tb2);
//对tb2里的每一条记录
while(getnext_iter(&
itertb2,rec2)==0){
/*outputrec1xrec2*/
print_tuple(rec,td);
close_iter(&
itertb2);
itertb1);
两个表做product的时候,对CPU时间和I/O时间的花费非常大,而且不能够进行优化,所以实际对数据库表进行操作的时候应该避免这方面的操作。
8.join:
嵌套循环连接实现:
对于最简单的嵌套循环连接,我们实现了两种算法:
一种是基于元组的,一种是基于块的。
下面分别介绍。
(1)基于元组的嵌套循环连接:
即取出第一个表的每一个元组,然后取出第二个表的每一个元组,判断第二个表的每一个元组是不是能和这第一个表的元组连接,若能连接,则把结果放入结果表中。
实现上很简单(join.c),见下。
intnest_tuple_join(constchar*tb1,constchar*tb2)
/*outputrec1|x|rec2*/
intkey1=*(int*)(rec1+TUPHDR_SIZE);
intkey2=*(int*)(rec2+TUPHDR_SIZE);
if(key1==key2){/*canjoin*/
print_tuple(rec,td);
}
这种方法的好处是能对任何大小的表进行连接操作;
但是缺陷也很明显,这种算法需要的I/O代价是非常非常大的。
需要对它进行一下改进,就是下面的基于块的嵌套循环连接。
(2)基于块的嵌套循环连接:
我们对基于元组的嵌套循环连接进行一下改进:
每次将尽可能多的第一个表中的元组放在内存,然后对它们建立一个内存查找结构,即我们之前建立的AVL树;
然后依次取出第二个表中的元组,根据这个元组连接属性上的值查询我们建立的第一个表的内存查找结构,若能找出能和这个元组连接的第一个表中的元组,则把连接结果加入到结果表中。
不断重复这样的步骤,直到我们取出了第一个表中的所有元组为止。
很明显,当第一个表能一次都放进内存时,这个算法就成了等值连接的一遍算法。
基于块的嵌套循环算法的具体的实现如下:
也在源文件join.c中:
intnest_blk_join(constchar*tb1,constchar*tb2)
BNode*avl=NULL;
//建立内存查找结构
avl=InsertBT(avl,e,&
taller);
itertb2=open_iter(tb2);
//在内存查找结构中查找能进行连接的元组
res=SearchBT(avl,key);
while(res!
=NULL){
//进行连接并输出
res=res->
next;
9.join:
索引连接实现:
若我们在某一个表上面的连接属性上建立了索引,我们可以进行使用索引的连接,算法是:
对第一个表的每一条元组,使用索引查找第二个表中具有这个元组连接属性上的值的所有元组,然后分别和这些符合条件的元组进行连接。
当第一个表很小时,而且第二个表索引上的重复值不是太多时,这种算法可以达到很高的效率。
具体实现如下(injoin.c):
intindex_join(constchar*tb1,constchar*tb2)
//查找第二个表中能和rec1连接的元组
if((res=btree_search(key,table2,&
while(nid!
for(;
if(bnode->
key[idx]!
=key)
break;
//查找到了,join输出
print_tuple(rec,td);
}
10.join:
排序连接实现:
当两个表都比较大时,我们可以考虑用排序连接:
首先对两个表分别根据连接属性上的值创建缓冲区大小的排序子表;
然后将每个子表的第一块调进缓冲区;
重复查找所有的子表上面最小的连接属性值;
分别找出两个表中具有这个最小值的元组,然后进行连接,并删除这些元组;
如果一个子表内存中的块被处理完毕,则装入这个子表的下一块;
直到所有的子表都被处理完毕。
具体实现如下(在源文件sortjoin.c中):
intsort_join(constchar*tb1,constchar*tb2)
/*sorttwotable*/
ftb1=fopen("
tmp1"
"
w+b"
);
ftb2=fopen("
tmp2"
initsort(tb1,ftb1,&
loop1);
initsort(tb2,ftb2,&
loop2);
/*actualjoin*/
sjoin(tb1,tb2);
/*closetmpfile*/
initsort()是对一个表进行排序,生成排序子表,sjoin()是根据生成的两个表的排序子表进行排序连接,具体实现的伪代码如下:
intsjoin(constchar*tb1,constchar*tb2)
将tb1,tb2的所有排序子表装入内存;
for(;
;
){
查找所有子表中最小的连接属性上的值min;
for(tb1中每一个连接属性上值为min的元组rec1)
for(tb2中每一个连接属性上值为min的元组rec2)
print_tuple(rec);
//输出连接后的元组rec=rec1|x|rec2
if(某个缓冲区空了)
装载下一个子表块;
if(所有的子表都已经处理完)
break;
11.join:
散列连接的实现:
同样,当要连接的两个表规模比较大时,可以用散列来把关系进行划分然后分别进行连接:
首先分别将两个关系按照连接属性上的值划分到静态的散列桶中并写回磁盘;
然后分别加载两个关系相对应的桶,对两个桶中的元组进行嵌套循环连接。
具体实现如下(在源文件hashjoin.c中):
inthash_join(constchar*tb1,constchar*tb2)
"
hashtb(tb1,ftb1,buckinfo1);
hashtb(tb2,ftb2,buckinfo2);
hjoin(tb1,tb2);
hashtb()函数是把一个表进行hash,并把每个桶写回到磁盘;
hjoin()函数是对两个表每相应的两个桶进行连接操作,具体实现的伪代码如下:
inthjoin(constchar*tb1,constchar*tb2)
//对每一个相应的桶
for(k=0;
k<
NBUCT;
k++){
将两个表中该桶中的所有元组加载进缓冲区;
for(第一个表桶中的每个记录rec1)
for(第二个表桶中的每个记录rec2)
if(rec1canjoinwithrec2)
print_tuple(rec);
//输出连接后的结果元组
12.测试及分析:
我们的main.c文件是做具体的测试的程序,我们实现了一个文字控制台界面的测试程序(类似于Oracle的SQL*plus),可以根据用户输入的命令完成相应的spj的操作。
首先程序启动时,如果还没有建立相应的数据库文件,它会根据该实验所给的测试文件中的数据(scan.txt,join1.txt等等),建立相应的表,一共会建立相应的五个表(表名分别为:
scan,join1,join2,cross1,cross2)。
然后程序会出现一个提示符,提示你输入命令,你可以用命令来进行spj操作,目前支持的命令格式如下(其中的表为上面5个表名之一;
所有的连接、等值查询只针对测试数据中的第一列):
total表//无条件查询
equal表key//查询表中关键字为key的所有元组
range表key1key2//对表进行范围查询[key1,key2]
product表1表2//表1表2进行笛卡尔积
nestblkjoin表1表2//进行基于块的嵌套循环连接
indexjoin表1表2//进行索引连接
sortjoin表1表2//进行排序连接
hashjoin表1表2//进行散列连接
quit//退出
其中所用的机器参数为:
CPU:
IntelCore2DuoCPUE44002.00GHZ;
RAM:
1G;
硬盘:
SATA7200转;
OS:
Gentoo-kernel-2.6.25-r9
(1)测试应用数据文件初使化数据库的时间,分别如下所示
Scan.txt插入1000000条元组64.590seconds
join1.txt插入20000条元组0.140seconds
join2.txt插入300000条元组7.070seconds
(2)对scan这个表(共1000000条记录)进行全表扫描,所用时间为21.580seconds。
运行结果如下图所示:
(3)进行等值查询,运行结果如下图所示:
(4)进行范围查询,运行结果如下图所示:
(5)测试各种连接算法所用的时间,如下所示:
笛卡尔积38.460seconds
索引连接3.950seconds
基于块的嵌套循环4.290seconds
排序连接4.480seconds
散列连接4.550seconds
其中一次连接的运行结果如下图所示:
四、心得与体会:
1) 在实验的过程中,发现测试文件中的key值是有重复的,而我们之前实现的B+树索引是不允许键值重复的。
于是我们重新修改了一下B+树的实现,让它支持重复值。
主要是引入了一个所谓的空键值的概念,当进行索引查找的时候,遇到这个空值直接跳到后面去。
修改的地方还是挺多的,也挺复杂的。
一个教训就是:
当初设计的时候一定要把需求彻底弄清楚(以后要修改需求时,代码的维护修改是很复杂的),最好避免需求的改变。
2) 总结一下选择操作:
进行等值查找的时候,若查找属性上面建了索引的话,最好能用索引来做,但是若是在这个索引上面重复值太多的时候,而且索引不是聚簇的,就应该用全表扫描的方法来做;
同样进行范围查找的时候,若是范围比较大而且索引不聚簇的时候,就应该用全表扫描的方法,其它的情况下可以考虑使用索引扫描的方法。
3) 比较一下各个连接算法:
基于元组的嵌套循环连接不管在时间还是I/O上都是比较花费的,在不对这种算法进行改进的时候尽量不要用这种连接方法;
当其中一个表较小而另一个非常大而且在连接属性上有索引的时候,可以考虑使用索引连接,效率有很大的提升;
同样上述的情况,但是在连接属性上没有索引的时候,可以考虑使用基于块的嵌套循环连接,而且当其中一个表非常小,能全部放入缓冲区的时候,相当于一遍算法;
当两个表都比较大的时候,可以使用基于排序的连接或是基于hash的连接,这两种算法都能把一个大的关系给划分为小的关系,再在这小的关系上面进行相应的连接,从来提高连接算法的效率。