引子
书接上回。前面了解了数据表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));
}