sqlite源码分析之路(二) 数据库文件格式探索(3) B树页头分析

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

引子

书接前回,上次说到sqlite的变长整数是数据格式分析中绕不过去的坎,所以只好花些功夫用php实现了变长整数的编码与解码功能,现在只需查看数据库文件中的数据,所以只需要变长整数的解码功能就够了。

这几天又花了些时间对sqlite数据库的页文件做了较为仔细的学习。我的学习办法就是用php,按照官方文档格式将一个实际的数据库文件的内容提取出来,变成文本内容展现出来。这样做的好处,既骗不了别人,更骗不了自已。真弄懂了,代码自然编得出来,数据库文件的内容也就会一目了然地呈现在眼前。

如果只是粗粗地看一遍格式文档,好像是懂了,又好像是没懂。这种状态要不得。希望对sqlite数据库底层感兴趣的朋友,也用你最喜欢的一种语言,将sqlite的数据库文件格式解析出来。一句话:纸上得来终觉浅,须知此事要躬行。

这些天为了成功地解析sqlite的页格式,也是碰到了n多的错误。包括我最喜欢犯的一个错误,忘了写句末的分号。想到这里忽然冒出一个想法,python语言的作者是不是也因为经常忘了写分号,痛恨之余,创作了一门不要分号的编程语言。

正式开始

Btree页是sqlite数据库中最重要的页类型。
每个Btree由4部分组成
1.页头
2.单元指针数组
3.未分配空间
4.单元内容区

下面先来分析页头内容:
bTreeInt.h中说明如下:

** The page headers looks like this:
**
**   OFFSET   SIZE     DESCRIPTION
**      0       1      Flags. 1: intkey, 2: zerodata, 4: leafdata, 8: leaf
**      1       2      byte offset to the first freeblock
**      3       2      number of cells on this page
**      5       2      first byte of the cell content area
**      7       1      number of fragmented free bytes
**      8       4      Right child (the Ptr(N) value).  Omitted on leaves.
**

页头的格式如下:
偏移量 大小 说明
0 1 页类型标志。1: intkey, 2: zerodata, 4: leafdata, 8: leaf。
1 2 第1个自由块的偏移量。
3 2 本页的单元数。
5 2 单元内容区的起始地址。
7 1 碎片的字节数。
8 4 最右儿子的页号(the Ptr(n) value)。仅内部页有此域。

为了方便用php处理页头,本人对照sqlite源文件btreeInt.h中的页头描述手工编写了json格式的页头描述对象

页头的描述

下面是页头的描述文件,json格式

{
"pageHeader":
{"size":"unused",
"elements":
{
"Flags":{"offset":0,"size":1,"type":"u8"},
"offseFirstFreeblock": {"offset":1, "size":2,"type":"u16"},
"numberOfCells": {"offset":3,"size":2,"type":"u16"},
"firstByteOfCellContent": {"offset":5,"size":2,"type":"u16"},
"numberOfFragmentedFreeBytes": {"offset":7,"size":1,"type":"u8"},
"RightChildPageNo": {"offset":8,"size":4,"type":"u32"}
}
}
}

本人创建了一个sqlite数据库文件,为了查看它的内部格式,用16进制编辑器打开,页头部分8个字节,用黑色标记出来了,如下图所示:
在这里插入图片描述
这些天书一样的东西你可是要打起十二分的精神才能看得下去的。对照sqlite的官方文档的说明,或者对照“空转”先生所写的中文文档,这里的内容也不是很难理解。我不打算逐个字节地讲解其含义了。前面的页头描述的josn文件也对每个字段的含义做了说明。

用于解析页头的代码

所谓源码之下,秘密全无

<?php
//将sqlite数据库文件的数据页最前面的12个(或8个)字节的页头解析成文本形式

//导入与sqlite取值相关的功能函数
require_once PHP_SRC_PATH."function/sqliteValue.php";

function unpackSqlitePageHeader($workBench){

//参数区开始------------------------------------
	$pageFileName=$workBench['pageFileName'];
	$pageNo =$workBench['pageNo'];
	$headerJsonDefineFileName=$workBench['headerJsonDefineFileName'];	
	$resultFileName=$workBench['resultFileName'];
//参数区结束------------------------------------

	$fin = fopen($pageFileName,'rb');

	//如果是每第一页,则pageHeaderOffset为100,其它情况下则为0
	$pageHeaderOffset = $pageNo==1?100:0;
	fseek($fin,$pageHeaderOffset,SEEK_SET);

	//根据页头描述json,将页头内容解析成文本
	$jsonArray = json_decode(file_get_contents($headerJsonDefineFileName),true);
	$elements = $jsonArray["pageHeader"]["elements"];

	//print_r($elements );

	echo "PageNo -> ".$pageNo."\n";

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

	$resultStr='';
	$resultArray=[];

	$isLeafPage = false;
	foreach ($elements as $name => $info) {	
		$resultArray[$name]=[];		
		//fseek($fin, $info['offset']*1);
		$rawVal = fread($fin, $info['size']*1);
		$val = unpackValue($rawVal,$info['size']*1,$info['type']);
		$resultArray[$name]['val']=$val;
		if($name=='Flags'){
			$isLeafPage = pageHeaderSize($val) == 8;
		}
		if($name=='RightChildPageNo'){
			//叶子节点是没有此字段的,此处用0表示无此页
			if($isLeafPage){
				$val = 0;
			}
		}
		$showVal = $val;
		//检查是否有附加说明
		if(array_key_exists('extraInfo', $info)){
			if(array_key_exists('hexValue', $info['extraInfo'])){
				$hexVal = int2HexStr($val,$info['type']);
				$resultArray[$name]['hexVal']=$hexVal;
				$showVal = intWithHexStr($val);
			}
			if(array_key_exists('typeName', $info['extraInfo'])){
				$typeName = pageTypeName($val);
				$resultArray[$name]['typeName']=$typeName;
				$showVal = $showVal.'['.$typeName.']';
			}
		}
		$hexOffset = int2HexStr($info['offset']);
		$resultStr .= '0x'.$hexOffset.' -> '.$name .' : '.$showVal."\n";
	}

	fclose($fin);

	echo $resultStr;

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

	$jsonArray["pageHeader"]["size"] = pageHeaderSize($resultArray['Flags']['val']);
	$jsonArray["pageHeader"]["elements"]=$resultArray;

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

//求出页头大小
function pageHeaderSize($flags){
	if($flags==13){ //B+树叶子页
		 return 8;
	}
	if($flags==5){ //B+树内部页
		return  12;
	}
	if($flags==10){ //B树叶子页
		return  8;
	}
	if($flags==2){ //B树内部页
		return 12;
	}
	return 8;
}

//求出页类型
function pageTypeName($flags){
	if($flags==13){ //B+树叶子页
		 return 'TableBTreeLeaf';
	}
	if($flags==5){ //B+树内部页
		return  'TableBTreeInterior';
	}
	if($flags==10){ //B树叶子页
		return  'IndexBTreeLeaf';
	}
	if($flags==2){ //B树内部页
		return 'IndexBTreeInterior';
	}
	return 8;
}


下面是解析出来的结果:


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

对比16进制文件的内容与此处解析得到的文本结果,是不是有一种雨过天晴的感觉,这才是人类可以阅读的内容。
现在将它翻译成中文

我是一个可怜的数据库页,被Hipp先生压得扁扁的,幸亏littleZhuHui将我解放了出来,
对不起,说得太多了,第一行只有一个意思,我是第1页,并且这个信息页头里面没有,我硬要说的。
页标志是13, 一个数据表B树,叶子节点
自由块的偏移量是789
这一页里有4个单元
第一个单元的起始地址(相对于页起始位置)是472(16进表示为0x01d8)
碎片字节数为0。哈哈,没有碎片,我真高兴。
右孩子的页号为0,对不起,我是一个叶子接点,我的真实意思是我根本就没有这个字段。

补充一下,sqlite中的Btree页有四种类型:数据表B树叶子节点、数据表B树内部节点、索引B树叶子节点、索引B树内部节点。

再补充一点,没有B-树,我是说有的人大声念出来的那种“B减树" 。老外写出来的B-Tree中的-是一个分隔符,不是减号。不过有些人的想象力实在丰富。比如微软公司那个windows xp .念作windows 叉屁 是不是更有趣,中文的谐音可是丰富得很。

索引B树对应着一般意义上的B树(或者B-Tree) ,数据表B树对应着B+树。B+树是B树的变种。mysql对B+树喜欢得不得了,恨不得到处都用上。不过B+树更费空间,所以做为一个嵌入式数据库系统出现的sqlite就选了B树做索引。Hipp先生当初是不是这样考虑的呢?我没有他的微信,无法求证。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值