再读simpledb 之 元数据管理(2)
续前节。
2、(StatMgr)统计信息管理
统计信息管理器主要维护了一个表的三种统计信息:表文件包含Block的个数,包含记录的条数,以及每个字段非重复记录数。
图1 统计信息管理
注:
StatInfo中,每个字段的非重复记录数是一个经验估计值,大约是1 + (numRecs / 3)。
如前面所说,统计信息不是持久化到磁盘上,而是在系统启动之初,新建StatMgr对象的时候,自动计算得到。
private void refreshStatistics(Transaction tx)
{
lock (threadLock)
{
tablestats = new Dictionary<string, StatInfo>();
numcalls = 0;
TableInfo tcatmd = tblMgr.getTableInfo("tblcat", tx);
RecordFile tcatfile = new RecordFile(tcatmd, tx);
while (tcatfile.next())
{
string tblname = tcatfile.getString("tblname");
TableInfo md = tblMgr.getTableInfo(tblname, tx);
StatInfo si = calcTableStats(md, tx);
tablestats.Add(tblname, si);
}
tcatfile.close();
}
}
在计算指定表的统计信息的时候,通过扫描记录文件,获取记录的相关参数,进而统计出整个表的统计信息:
a. numRecords通过遍历整个表中的记录,计数可得
b. numBlocks通过扫描最后一条记录的RID中的blockNumber得到。
附:Record的存储
record类图见前节中的图1。
上面StatMgr中用到了RecordFile提供的方法,所以这里附上simpledb中表记录的存储细节。
前面描述了simpledb的存储实现,还记得,在buffer下面,有一个PageFormatter的接口,当初提到说,整个系统只有两个接口实现,其中有一个就是RecordPageFormatter。实际上说,数据库中的表,不是简单地按照顺序的形式写在文件中,而是有其自身的格式限制。表文件有着自己的存储格式,新插入记录按照指定的格式写入到表文件中。
> RecordPageFormatter。
RecordPageFormatte有一个TableInfo对象,利用这个TableInfo对象包含的表中的字段信息,在表文件被写入真实数据之前,对其进行格式化。
public void format(Page page)
{
int recsize = ti.recordLength() + Page.INT_SIZE;
for (int pos = 0; pos + recsize <= Page.BLOCK_SIZE; pos += recsize)
{
page.setInt(pos, RecordPage.EMPTY);
makeDefaultRecord(page, pos);
}
}
代码比较简单,通过格式化的方式,可以看出,simpledb是以定长的形式保存记录的。
makeDefaultRecord方法根据TableInfo提供的schema信息,给字段设置默认值,并将默认值写入文件。
a. int默认值为0
b. 字符串默认值为""
这里要注意下的,是表记录的保存格式:
是否被使用(IS_USED) | 表记录(record) |
IS_USED初始为0,当新添加记录时,会将IS_USED置为1。
> RecordPage
联想前存储中提到的Page和Block的对应关系,RecordPage可以看做是一种特殊的Page,管理着对应Block中数据记录的放置和访问。略有不同的是,RecordPage不维护一个contents数组,因为RecordPage只是管理宏观放置和访问,处理的粒度是Record,具体的数据读写,交给Transaction对象来完成。
RecordPage中主要包含了5个对象:
blk,当前正在访问的磁盘文件块;
ti,当前表文件的字段信息;
tx,当前查询操作所在的事务;
slotsize,当前Block中,一条记录实际占用的长度;
currentslot,当前可访问的记录的指针。
主要从放置和访问(读写插删)两个角度来介绍下RecordPage的功能:
a. 记录的放置
我理解就是要插入的新纪录的定位问题:放在那里?
前面说过,被格式化过的表文件,就如下图所示(还是以studens表为例):
图2 格式化的空表文件
图3 插入过数据得到表文件
关键是一个searchFor(IS_USED)方法,给定参数IS_USED=0时,将currentSlot指针移向可用的位置。
另外moveToId(id),将currentSlot指针指向指定的位置。
b. 记录的访问
读
首先是定位:记录在整个表中的偏移+字段在记录中的偏移
然后用tx的对应方法读出数据
写
同样先是定位,然后用tx的方法在指定位置写入数据
插
首先定位,利用searchFor(0),移动currentSlot到合适的位置
标记已占用,IS_USED为1,然后交给后面的写方法,写入记录的内容
删
使用了标记删除法,不抹去数据,只是将IS_USED标记位设置为0,表示未使用即可。
> RecordFile
上面的RecordPage只是负责对一个磁盘数据块的内容的访问,RecordFile则是对整个表记录文件的遍历和访问。
首先说RecordFile和RecordPage的关系,RecordFile被访问时,不是一次读进来整个文件,而是分成若干个Block,一个BLock一个Block地读,这样在每次访问一个Block的时候,就需要一个RecordPage来访问Block。
一个RecordFile下面,只维护一个RecordPage对象,指向当前访问的Block。
另外还维护着一个currentBlocknum,作为一个指针,指向当前访问块的块号。
RecordFile中包含多个Block,因此,遍历时用到的next()方法:
首先是检查当前Block是否有next记录
否则检测是否有下一个可用的Block
以此类推,直到找到可用的记录,或者到达文件末尾退出。
public bool next()
{
while (true)
{
if (rp.next())
return true;
if (atLastBlock())
return false;
moveTo(currentblknum + 1);
}
}
其他的方法,都是对RecordPage方法的包装,考虑了块的移动造成的影响。