sqlite源码分析之路(二) 数据库文件格式探索(4) 数据表B树叶子节点分析

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

引子

书接前回,上次分析了B树节点的页头,不论是数据表对应的B+树,还是索引对应的B树,树节点分成两大类。一类是叶子节点,一类是内部节点。叶子节点的页头大小是8个字节,内部节点的页头大小是12个字节。为什么?
内部节点要在页头的最后面加上4个字节存放最右孩子的页号。所以多出来4个字节。

4个字节可以看作一个32的无符号整数。所以sqlite数据库中最多有2^32 - 1页。如果一页是1k.则数据库最大容量是4T。试了一下。我的电脑上php5.4对应的sqlite3.8 ,默认的页大小是1K;php7.2对应的sqlite3.20,默认的页大小是4k。

sqlite数据库从第1页开始计数。并且第1页的前面100个字节是文件头。后面的页就都不用包含文件头了。但所有的页都有一个页头。下面咱们就开始分析数据表B树的叶子节点。

正式开始

如果打开sqlite数据库,并简单地创建一张表,那么第1页会是一个数据表B树叶子节点。当然随着后来数据越来越多,一个叶子节点装不下,它就会升格为内部节点。也就是说它可以带小弟了。带着小弟或者说孩子就可以装上更多的数据了。

取一个很小的数据库文件。为了便于分析,先将它分割为每页一个的单独文件。这样做的好处是分析起来更方便。因为页中的各种偏移量都是相对页的起始地址的。如果用一个整体的文件做分析,那么每次都要记得加上页的偏移量。

于是有了下面这个小工具

数据文件分割代码

<?php
//将数据库文件按页大小切分成一个一个的小文件
//通过workBench传递需要一些设置信息
function splitIntoPages($workBench){

//参数区开始------------------------------------
	$dbFileName=$workBench['dbFileFullName'];
	$outPath = $workBench['outPath'];
//参数区结束------------------------------------

	echo "start splitting ... \n";

	$dbSize =dbFileSize($dbFileName);
	echo $dbFileName,'->size = ',$dbSize," bytes\n";

	$pageSize=dbPageSize($dbFileName);
	echo 'pageSize','->',$pageSize," bytes\n";

	$pageCount =$dbSize/$pageSize;
	$pageCount = $pageCount== floor($pageCount)?$pageCount:floor($pageCount)+1;
	echo 'page count:',$pageCount,"\n";

	$pageNo = 1;
	$dbFile = fopen($dbFileName,'rb');
	for($i=1;$i<=$pageCount;$i++){
		$offset = ($i-1)*$pageSize;
		fseek($dbFile,$offset,SEEK_SET);
		$pageContent = fread($dbFile,$pageSize);
		$outFileName = $outPath.'/page'.$i.'.dat';
		file_put_contents($outFileName, $pageContent);
	}
	fclose($dbFile);

	//将page1再切分为header 和 body
	$i = 1;
	$page1File = fopen($outPath.'/page'.$i.'.dat','rb');

	$blockContent = fread($page1File,100);
	$outFileName = $outPath.'/page'.$i.'-header.dat';
	file_put_contents($outFileName, $blockContent);

	$blockContent = fread($page1File,$pageSize);
	$outFileName = $outPath.'/page'.$i.'-body.dat';
	file_put_contents($outFileName, $blockContent);

	fclose($page1File);

	echo "split over!\n";
}

//返回数据库文件的大小,以字节为单位
function dbFileSize($dbFileFullName){
	return filesize($dbFileFullName);
}

//返回数据库文件页的大小,以字节为单位
function dbPageSize($dbFileFullName){
	$fin = fopen($dbFileFullName,'rb');
	//移动到记录数据库页大小的位置
	fseek($fin,16);
	$val = readValueFromFile($fin,2,'u16');
	fclose($fin);
	return $val;	
}

这个小工具将数据库文件按页大小进行切分,效果如下:
在这里插入图片描述
可以看出这个文件大小为3k,页大小为1k。所以切分成了3个文件,分别叫做page1.dat、page2.dat、page3.dat。
特别针对page1还将文件头切分了出来。

第一页内容分析。

第一页的内容的16进制效果如下图所示
在这里插入图片描述
由于可以根据页头标志分析其页类型。可以直接在16制文件中查看偏移量为 0x64的内容,为0x0D。根据前文所述,这是一个数据表B树的叶子节点。
用前一篇文章中讲的页头分析工具,可得结果如下:


PageNo -> 1
0x0000 -> Flags : 13[0x000d][TableBTreeLeaf]
0x0001 -> offseFirstFreeblock : 0
0x0003 -> numberOfCells : 2
0x0005 -> firstByteOfCellContent : 419[0x01a3]
0x0007 -> numberOfFragmentedFreeBytes : 0
0x0008 -> RightChildPageNo : 0

可以很清楚地看出这是一个数据表B树的叶子节点。该页中包含两个单元。

接下来直接上数据表B树的叶子节点的分析代码,对照分析结果再来讲页上的内容格式。
数据表B树的叶子节点的分析分成两个步骤:
第1步分析单元内容
第2步分析单元中的payload内容。
因此代码也相应地分成两部分

数据表B树的叶子节点单元分析

//解包表B树(B+树)叶子节点的单元内容
function unpackTableBTreeLeafCell($workBench){

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

	fseek($fin, $cellOffset,SEEK_SET);
	//找到cell入口后,还要读取两个变长整数,
	$PayloadOffset = 0;
	$PayloadSize = readValueFromFile($fin, 'var','varInt',$info);

	//echo 'PayloadSize->',intWithHexStr($PayloadSize),"\n";
	
	$PayloadOffset +=$info['size'];
	$NumberOfBytes_key = readValueFromFile($fin, 'var','varInt',$info);
	$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);
	
}

在上面的代码中调用了分析B树节点payload的代码。payload的结构与与页类型无关,数据表B树内部结点不包含payload的内容。其余三种包含payload的内容。但是payload的分析都是一样的,与页类型无关。

B树节点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));
}


接下来看针对例子数据库第1页的分析结果

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

col1 type:text     size:5      -> table
col2 type:text     size:3      -> log
col3 type:text     size:3      -> log
col4 type:i8       size:1      -> 2
col5 type:text     size:501    -> CREATE TABLE log
	        (id INTEGER PRIMARY KEY AUTOINCREMENT ,
	        stationId TEXT   NOT NULL,
	        userId TEXT   NOT NULL,
	        customerId TEXT   NOT NULL,
	        customerName TEXT   NOT NULL,
	        idCard TEXT ,
	        logTime TEXT   NOT NULL,
	        logDay TEXT   NOT NULL,
	        grossWeight TEXT   NOT NULL,
	        tareWeight TEXT   NOT NULL,	      
	        goodsName TEXT   NOT NULL,	     
	        goodsGrade TEXT   NOT NULL,	     
	        remark TEXT  
	        )

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)

对照解释一下。
单元指针数组的偏移量是108[0x006c]。文件头100+页头8。
每个单元指针占两个字节。页头信息中已经看出该页有两个单元。
接下来是两大块内容,分部对应着两个单元的内容。
每一部分都用col1…col2…说明一行记录的各个字段(列)的内容。一个单元就是一条记录。

可以看出第1个单元,也就是sqlite_master表中的记录内容。顺便说明,cell单元的内容是从底向上生长的,其实排在下面的才是表中第1条记录,排在上面的是表中第2条记录。

以排在下面的第一条记录为例进行说明

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)

这1条记录有5个字段(5列)
第1列是text ,长度是5,值是“table"
第2列是text ,长度是15,值是“sqlite_sequence"
第3列是text ,长度是15,值是“sqlite_sequence"
第4列是整型 ,长度是1,值是“3"。 它的含义是 sqlite_sequence这张表的根页的页号是3。
第5列是text ,长度是38,值是“CREATE TABLE sqlite_sequence(name,seq)"。 它存放的是创建sqlite_sequence这张表的sql语句。

你问我为什么知道这些具体含义的,因为sqlite规定了第1页就是sqlite_master这张表的根页。sqlite_master这张表中存放了所有其它表的结构信息。唯独没有存放sqlite_master它自已的结构信息。因为不需要。这张表的结构在sqlite设计者的头脑中,它体现在sqlite的代码中,不需要一条记录来存放它。

按照官方文档
The SQLITE_MASTER table looks like this:
CREATE TABLE sqlite_master (
type TEXT,
name TEXT,
tbl_name TEXT,
rootpage INTEGER,
sql TEXT
);
所以以上5个字段的含义对照解析出来的文本,就可以得到很好的理解了。

至于数据表B树叶子节点的单元与payload的具体结构,看一下php分析代码就可以很清楚地了解了。还是那句话,源码之下,秘密全无。不想看代码的朋友,请移步sqlite官方文档

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值