引子
书接前回,上次说到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先生当初是不是这样考虑的呢?我没有他的微信,无法求证。