sqlite源码分析之路(二) 数据库文件格式探索(6) 索引B树内部节点分析

26 篇文章 2 订阅
9 篇文章 1 订阅

引子

书接上回。前面了解了数据表B树的内部节点与叶子节点的结构。接下来要分析索引B树的节点结构。下面先看一下索引B树的内部节点。

数据表B树与数据结构中的B+树对应,索引B树与数据结构中的B树对应。B树与B+树的最大不同在于,在B树的内部节点上除了存放关键字和孩子指针之外,还存放有其它相关的数据。在查找B树时,很有可能在查到内部节点之后,查找就结束了;而B+树中内部节点只存放关键字和孩子指针,有效数据全部存放在叶子节点上,所以查找只会在抵达叶子节点后才会结束。

对于索引来说,被索引的字段就是关键字,因此要判断sqlite中的索引树是不是B树,就是要检查其内部节点除了存放被索引的字段与孩子指针之外,是否还存放有其它的内容。sqlite的索引B树的内部节点,其实还存放有记录的rowid,方便在找到索引字段的内容后通过rowid找到数据表记录。也就是说,不必每次查找都一定要抵达叶子节点,才能取回rowid。在接下来的分析中,咱们可以验证这一点。

再回过头说一下sqlite的数据表B树,有人说它是一个B+树,但有一个特性它不具备,就是所有的叶子节点都按顺序用指针连接起来组成一个顺序链表。我在数据表B树的叶子节点中没有发现指向下一个叶子节点的指针域。在sqlite的官方文档中把两种B树分别叫做TableBTree 与 IndexBTree,而没有叫做B+Tree与BTree,这也可能是一个原因吧。

正式开始

为了产生可供分析的索引B树内部节点,咱们需要先在数据表上建一个索引,特别是要产生索引B树内部节点,那么这个索引还不能太小,所以咱们接着用前一篇的例子,在customer表的idCard列上建立索引。

注意,新建了索引之后,数据库的内容发生了变化,但是咱们分析的是分割开的一个一个的页文件。所以在做这个实验时,咱们还需要再做一次数据库文件切分工作。

还是先从第1页开始分析,因为咱们一开始也不知道到底哪一页才是索引节点,总不能一页一页地去试吧。其实看一看数据库的第一页,就能发现线索。

下图是数据库第1页的分析结果,这个工作前面做个多次了。

PageNo -> 1
0x0000 -> Flags : 13[0x000d][TableBTreeLeaf]
0x0001 -> offseFirstFreeblock : 789
0x0003 -> numberOfCells : 4
0x0005 -> firstByteOfCellContent : 472[0x01d8]
0x0007 -> numberOfFragmentedFreeBytes : 0
0x0008 -> RightChildPageNo : 0

cellPointArrayOffset->108[0x006c]
cellCount->4

col1 type:text     size:5      -> table
col2 type:text     size:15     -> sqlite_sequence
col3 type:text     size:15     -> sqlite_sequence
col4 type:i8       size:1      -> 3
col5 type:text     size:38     -> CREATE TABLE sqlite_sequence(name,seq)

col1 type:text     size:5      -> table
col2 type:text     size:8      -> customer
col3 type:text     size:8      -> customer
col4 type:i8       size:1      -> 4
col5 type:text     size:203    -> CREATE TABLE customer
	        (id INTEGER PRIMARY KEY AUTOINCREMENT ,
	        customerId TEXT   NOT NULL,
	        customerName TEXT   NOT NULL,
	        idCard TEXT ,
	        remark TEXT  
	        )

col1 type:text     size:5      -> table
col2 type:text     size:12     -> tmp_customer
col3 type:text     size:12     -> tmp_customer
col4 type:i8       size:1      -> 2
col5 type:text     size:88     -> CREATE TABLE tmp_customer(customerId text,customerName text,logTime text,netWeight text)

col1 type:text     size:5      -> index
col2 type:text     size:6      -> idcard
col3 type:text     size:8      -> customer
col4 type:i8       size:1      -> 5
col5 type:text     size:51     -> CREATE INDEX "idcard"
ON "customer" ("idCard" ASC)

注意查看上面结果中的最后一部分内容

col1 type:text     size:5      -> index
col2 type:text     size:6      -> idcard
col3 type:text     size:8      -> customer
col4 type:i8       size:1      -> 5
col5 type:text     size:51     -> CREATE INDEX "idcard"
ON "customer" ("idCard" ASC)

注意到最后一行的sql语句。 对数据库有所了解的人应该很快明白了,第一页对应着sql_master表,表中存放中所有表,视图的结构定义,其实索引的定义也是做为一条记录存放在sql_master表中。
接着再往上看一行:col4 type:i8 size:1 -> 5
这说明第5号页是这个索引的根页。我们有充分的理由相信这是一个索引B树内部节点。

验证一下吧

索引B树内部节点

PageNo -> 5
0x0000 -> Flags : 2[0x0002][IndexBTreeInterior]
0x0001 -> offseFirstFreeblock : 0
0x0003 -> numberOfCells : 2
0x0005 -> firstByteOfCellContent : 966[0x03c6]
0x0007 -> numberOfFragmentedFreeBytes : 0
0x0008 -> RightChildPageNo : 10

cellPointArrayOffset->12[0x000c]
cellCount->2

leafPageNo->8[0x0008]
col1 type:text     size:20     -> 43240219900103600066
col2 type:i8       size:1      -> 66

leafPageNo->9[0x0009]
col1 type:text     size:20     -> 43240219900103800040
col2 type:i8       size:1      -> 40

看看上面的分析结果,第二行清楚显示这是一个IndexBTreeInterior ,索引B树内部节点。这个节点的最右孩子是10号页
它有两个单元,分别指向8号页与9号页。因此这个内部节点共有3个孩子,分别是8,9,10号页。

另外注意到这两个单元中的第2列 ,这是存放的对应记录的rowid,比如第一个单元的第2列

col1 type:text size:20 -> 43240219900103600066
col2 type:i8 size:1 -> 66

这说明 这个idCard值43240219900103600066 对应的rowid 是 66。除了关键字和孩子指针之外的这个rowid ,也存放在了索引B树的内部节点中。所以如果查找idCard值为43240219900103600066的记录,对索引的查询到达这个内部节点就可以结束了,不用再继续下去查找叶子节点。剩下的事情就是拿着rowid去数据表B树中查找对应的记录。 B树的查找可以在内部节点上结束,而B+树的查找,一定要在叶子节点上结束。这是B树与B+树非常不一样的地方。

另外通过这一点也可以看出,如果我们的sql查询中要求返回的列就在索引字段上,就不用再跑去数据表B+树上查找数据了,也就是通常所说的不用“回表”了。这样查找效率就会大大提升。

说了一些题外话,还是来看索引B树内部节点的分析代码吧,由于单元中包含有payload的内容,所以代码也要分成两块,一块是分析单元基本信息,一块是分析payload的内容,但前文已经交待过,payload的分析与节点类型无关。这里为了方便查阅,还是再贴了一次。

索引B树内部节点的分析代码

单元分析部分

//解包表索引B树(B树)内部节点的单元内容
function unpackIndexBTreeInteriorCell($workBench){

//参数区开始------------------------------------
	$fin = $workBench['fin'];
	$pageNo = $workBench['pageNo'];
	$cellNo = $workBench['cellNo'];
	$cellOffset = $workBench['cellOffset'];
	$outputPath = $workBench['outputPath'];
//参数区结束------------------------------------

	fseek($fin, $cellOffset,SEEK_SET);
	//找到cell入口后,第一部分是4字节整数,表示孩子节点页号
	$PayloadOffset = 0;
	$leafPageNo = readValueFromFile($fin, 4,'u32',$info);
	echo 'leafPageNo->',intWithHexStr($leafPageNo),"\n";
	$PayloadOffset +=4;

	//第二部分是变长整数,代表payload的大小	
	$PayloadSize = readValueFromFile($fin, 'var','varInt',$info);

	//echo 'PayloadSize->',intWithHexStr($PayloadSize),"\n";
	
	$PayloadOffset +=$info['size'];	
	//echo 'PayloadOffset(in cell)->',intWithHexStr($PayloadOffset),"\n";

	//设置输出结果的文件名
	$resultFileName = $outputPath .'/page'.$pageNo.'-cell'.$cellNo.'-payload.json';

	//--------------------------------------------------------------------------
	//设置工作区参数
	$newWorkBench['fin'] = $fin;
	$newWorkBench['PayloadOffset'] = $cellOffset + $PayloadOffset;		
	$newWorkBench['resultFileName'] = $resultFileName;
	
	//解包B树节点的payload内容(payload结构与页类型无关)
	unpackBTreePayLoad($newWorkBench);
}

payload分析部分

//解包一个B树节点的payLoad(不区分B树与B+树,不区分内部节点与叶子节点)
//$workBench提供了解包payLoad所需要的一些基础信息
function unpackBTreePayLoad($workBench){

//参数区开始------------------------------------
	$fin = $workBench['fin'];
	$PayloadOffset = $workBench['PayloadOffset'];
	$resultFileName = $workBench['resultFileName'];
//参数区结束------------------------------------

	//echo 'PayloadOffset(in page)->',intWithHexStr($PayloadOffset),"\n";
	//将文件指针移到偏移量处
	fseek($fin, $PayloadOffset,SEEK_SET);

	//payload的存储逻辑比较复杂,直接编写程序来读取

	//第一步先读取headerSize,这是一个变长整数,读取完成后,$info['size']中存放的是这个headerSize本身的大小(所在字节数)
	$info=[]; 
	$headerSize = readValueFromFile($fin, 'var','varInt',$info);
	if(!is_numeric($headerSize)){
		echo 'the headerSize is not valid:',$headerSize,"\n";
		var_dump($workBench);
		return;
	}
	//echo 'payload-headerSize->',$headerSize,"\n";

	//实际的headerSize,由于headerSize包含了自身所占的字节数,此处应减去。
	$realHeaderSize = $headerSize - $info['size'];
	//echo 'realHeaderSize->',$realHeaderSize,"\n";

	//echo "start unpacking...\n\n";

	//记录中的列数目,初始值设为0
	$col =0;
	$colInfoArray=[];
	//实际发现的headersize,随着一个接一个变长整数的读取,实际发现的headerSize将等于realHeaderSize
	$foundHeaderSize =0;

	//逐一读取列信息
	while ($foundHeaderSize < $realHeaderSize){
		$colVarint = readValueFromFile($fin, 'var','varInt',$info);
		//echo 'colVarint->',$colVarint,'[',int2HexStr($colVarint),']',"\n";
		$foundHeaderSize +=$info['size'];
		$col+=1;
		//分析列的的类型与宽度
		$colInfoArray[$col] =sqliteInt2ColInfo($colVarint);
	}

	//var_dump($colInfoArray);

	//读取每一列的值
	for($i=1;$i<=$col;$i++){	
		$val = readValueFromFile($fin, $colInfoArray[$i]['size'],$colInfoArray[$i]['type'],$info);
		//echo 'col',$i,' -> ',$val,"\n";
		$colInfoArray[$i]['val']=$val;
	}	

	//输出结果信息
	$resultStr = '';
	foreach ($colInfoArray as $colNo => $colInfo) {
		$resultStr .= 'col'.$colNo.' type:'.str_pad($colInfo['type'],8,' ').' size:'.str_pad($colInfo['size'],6,' ').' -> '.$colInfo['val']."\n";
	}

	//将列信息放入结果数组
	$resultArray = $colInfoArray;

	echo $resultStr;

	//echo "\nunpack over!";
	echo "\n";	

	//将结果以json形式写入文件
	file_put_contents($resultFileName, json_encode($resultArray));
}


  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值