数据库最大页数 2^31
如何找到一个指针所指向的内容?
1、找到指针映射页面的页码(PTRMAP_PAGENO)
2、找到对应指针的偏移(PTRMAP_PTROFFSET)
指针映射的目的是什么?
将页面从文件中的一个位置移动到另一个位置,旧页更新后,必须更新原旧页的父级中的指针指向新位置
每个SQLite表必须设置btree_intkey或者btree_blobkey。
对于btree_intkey,表建是64位整数,数据仅仅存在叶子页中,内部也仅仅用来索引,也就是说,其用于SQL表。
对于btree_blobkey,密钥是任意的BLOB,没有内容存储在任何地方,因为密钥就是内容。也就是说,btree_blobkey用于SQL索引。
将整数值分配给常量,可以通过下面的公式找到SQLite数据库头中相应字段的偏移量(第一页的前100个字节表示数据库头): offset = 36 + (idx * 4)
BtreePayload对象的实例描述索引或表btree中单个条目的内容。
索引btree(用于索引和WITHOUT ROWID)包含任意键,没有数据。pKey,nKey(键的长度)作为键,pData,nData,nZero未初始化。
表btree(用于rowid表)包含一个整数rowid用作键,使用nKey字段来表示。pKey字段为0。pData,nData保存内容。
关于hash表
哈希表的所有元素都在一个双向链表上,Hash.first指向此列表的头部。
对于字符串类型和整型的数据,对应着不同的hash函数进行哈希映射的处理。
对于可变长整数的处理方式
sqlite采用大端的方式存储数据,即数据的高字节存放在内存中的低地址。这种方式存储,就能够非常方便地计算出可变长整数的值。
sqlite_uint64 x = 0, y = 1;
132 while( (*q & 0x80) == 0x80 ){
133 x += y * (*q++ & 0x7f);
134 y <<= 7;
135 if( q - (unsigned char *)p >= VARINT_MAX ){ /* bad data */
136 assert( 0 );
137 return 0;
138 }
139 }
140 x += y * (*q++);
141 *v = (sqlite_int64) x;
q是一个指向一个字节的指针,*q++,表示在内存中向高地址便宜一个字节,对应到可变长整数的稍低一位的数据,基数*2^7,就能很方便地计算出可变长整数的值。
B+树内部页(B+树的内部页只是用来表示索引,不存储数据),每个B+树内部页都有一个最右孩子页号,对应Ptr(n)
指针必不可少,用于指向子页,Key必不可少,用于比较
每个Ptr和每个Key组成一个Pair(一对)
指针(Ptr)就是页号,键(Key)就是rowid(当使用默认的int型作为键时)
B+树内部页的部分PayLoad分析:
0FF4H 地址:8180
00000004 834D Ptr(1)=00000004 Key(1)=834D
0FFAH 地址:8186
0000 0003816A Ptr(0)=00000003 Key(0)=816A
B+树叶子页
指针数组所指向的PayLoad内容
01DFH 地址:8671
0D 816A0300 217A6861 6E677361 6E697A 分别是Payload大小,rowid值,header长度…
B-树内部页
0FD8H 地址:24536
00000008 0F032102 7A68616E 6773616E 727401CE 依次对应:指针(页号)、Payload大小、header长度,type1,type2,rowid
可知,对于B-树来说,其内部页和叶子页格式区别不大。内部页中有指针和最右孩子指针(页号),而内部页没有。
B+树的rowid(键值)采用变长整数存储,B-树的rowid(键值)采用十六进制存储。
sqlite_master表
空闲页链表
溢出页链表组织结构
完整的哈希表是以下结构的实例。
此结构的内部结构是不透明的 - 客户端代码不应尝试直接访问或修改此结构的字段。
仅使用下面的例程更改此结构。
但是,用于修改和访问此结构的一些“过程”和“函数”实际上是宏,因此我们无法真正使此结构不透明。
哈希表的所有元素都在一个双向链表上。
Hash.first指向此列表的头部。
有Hash.htsize存储桶。
每个桶指向全局双向链表中的一个点。
存储桶的内容是指向的元素加上列表中的下一个_ht.count-1元素。
Hash.htsize和Hash.ht可能为零。
在那种情况下,通过线性搜索全局列表来完成查找。
对于小型表,永远不会分配Hash.ht表,因为如果表中的元素很少,则执行线性搜索比管理哈希表更快。
计划 | 成果 |
弄明白btree_intkey和btree_blobkey的区别 | 明白了btree_intkey用于SQL表,而btree_blobkey用于SQL索引 |
搞清楚Hash表组织方式 | 清楚了采用拉链法解决冲突,所有元素都在一个双向链表中,Hash.first指向列表的头部 |
理解可变长整数求值 | 理解了利用大端方式存储,从内存的低字节向高字节过渡,利用字节首位标志是否为1来作为结束依据 |
探索数据库首页前100字节的寓意 | 明白了从36字节往后的计算方式是36 + (idx * 4),通过计算出来的偏移来取出标记 |
学习索引btree的pKey,nKey | 明白了索引btree的pKey代表键,nKey代表键的长度 |
学习表btree的pKey,nKey | 清楚了表btree的使用rowid作为键,使用nKey字段来表示。PKey字段为0 |
学习B+树内部页的格式 | 理解了标志是05H,payload用于索引子叶。ptr(0),pkey(0),ptr(1),pkey(1),…,ptr(n-1),pkey(n-1)。 ptr(n)是最右孩子页号。 |
学习树叶子页的格式分析 | 明白了标志是0DH。Payload内容存储的是数据库中的数据。分别表示payload大小,rowid值,header长度,type1…typen,data1…datan。1…n表示n个字段中的开始和结束的字段下标。 |
研究SQLite数据库中首页的编号是从1开始的还是从0开始的 | 理解了数据库的首页是从1开始编号的。但是内存地址的下标都是从0开始的。所以牵涉到数据库页的表示的时候,下标都是从1开始的。当涉及到从数据库中,也就是内存中查找一个页面时,要将页面的下减去1,这样才能找到该页对应在内存中的地址。 |
探索B-树的叶子页和内部页的区别和联系 | 明白了B-树的叶子也和内部页的格式几乎是一样的,B-树的内部页和外部页都存储数据,所以数据冗余会小一点。区别是内部页中会有最右孩子页号。 |
探索B+树和B-树的区别和联系 | 明白了它们之间的: 区别是:B+树只有叶子页才会真正存储数据,而B+树的内部页只用来索引其子页; 联系是:它们都相当于一张独立的表,即对于索引表来讲,是讲被建立索引的字段的值都提取出来并进行排序之后形成的节点来组成的B-树就是最终的索引表。 |
探索B-树组织数据的方式 | 理解了B-树的所有节点都存储数据,这些数据仅仅是作为索引的字段所代表的数据,如果在某些字段上没有建立索引,那么索引表就不会存储这些字段代表的数据。所以,当数据库字段比较多的时候,为查询频率比较高的字段建立索引是一个明智的选择。 |
探究SQLite溢出页的工作机制 | 明白了当一个payload较大或者而导致一个页面不能存放得下它时或者当一个页面剩余的空间太小而不足以存放一个paylaod时,该Payload的其余的数据就会存放到溢出页当中。也就是说,非溢出的payload内容在非溢出页的数据库页中只占据一页,而剩余的内容不管有多少都存放在溢出页当中。一般溢出页存了数据,那么它会尽量多地存放payload的内容,前提是该payload在数据库页中的内容所占的空间不小于minPayloadSize |
计划 | 完成情况 |
探究SQLite如何在自由块链表中找到一个自由块来存储一个nByte大小的单元? | 所有的自由块按照地址递增的方式连接成一个单链表。 首先选择第一个自由块,根据标志求出其长度size。然后可以得到剩余空间 size – nByte == x。然后需要对x的大小进行判断,在x<4的情况下会产生碎片,这时涉及到和该页本身的碎片总大小进行相加,在该页原碎片总大小>57时,SQLite就会认为x的其中一个取值如3会导致相加(57 + 3)之后的碎片总大小>60,而SQLite容忍的每页最大碎片大小是60字节,所以会选择空闲块链表中的下一个空闲块。如果不大于60字节,那么前面x个字节会被预留作为空闲块,只不过空闲块的大小变小了。后面部分的nByte字节会被作为存储payload的一个cell。在x<0的情况下,说明本空闲块的空间不能满足本次分配,所以也会根据其头部的2B的指针找到其所指向的下一个空闲块尝试分配。 综上: x<4时:若满足分配的条件(<=60),则需要做两件事情:1,本空闲块消失,需要对相应空闲块链表的指针进行调整。2,本页的碎片总数量要加上x。 在x >=4时,剩余空间步不形成碎片,本空闲块不消失,但是需要改变本空闲块的大小,要减去nByte。 |
探索memcpy和memmove函数的异同? | memcpy和memmove都可以实现将源内存处的n字节大小的数据移动到目标内存处。所以这两个函数的前两个参数分别是目标内存地址,源内存地址,第三个参数是复制的内容的大小。但是memcpy认为两块内存是独立的,所以当两块内存处的数据有重叠时,memcpy函数可能不能完成任务,但是memmove函数对这种情况会有所考虑和处理。所以,当处理复杂的拷贝时,可以使用memmove函数来完成。 |
探索SQLite如何对给定的页面进行碎片整理? | 经过碎片整理之后的页面将不存在任何自由块和碎片,而且所有的单元都紧凑地排列在页面的底部。 碎片整理总共有两个步骤。第一步对空闲块进行整理,第二步才真正对碎片进行整理。 进行空闲块整理的示意图:这时只对空闲块和非空闲块进行区分,碎片在此时是透明的。
这是有两个空闲块的情况,即首先将两个空闲块之间的部分往后移动,移动的总字节数是L2,即iFree2-(iFree+sz),偏移是sz2。然后,对第一个空闲块之前的部分往后移动,移动的总字节数是L1,即iFree-top,偏移是sz+sz2。如果只有一个空闲块,则只需要一个memmove函数即可。 第二步对碎片进行整理:这时碎片不透明。这时与其说是对碎片进行整理,不如说是对各个cell进行紧凑操作。经过第一步的空闲块整理之后,任何两个cell之间将不存在空闲块,但是存在两个cell之间存在碎片的情况。解决的办法就是根据指针数组中的指针,按照地址递减的方式依次找到每个cell的起始地址,根据一个函数计算出该cell的长度。但这还不够,还需要一个cbrk变量,cbrk刚开始时设为usableSize,随后cbrk根据每个cell的sz进行地址递减,这样就会得到很多个地址,每个地址都作为一个cell的新的起始地址,最终这些地址要保存到指针数组中。所以之前就需要将该页面的数据取出放在另外一个变量中充当原始的数据,即src,接着通过函数memcpy(&data[cbrk], &src[pc], size);完成单元内容区域的更新。 最后,对中间的未使用区域清零:memset(&data[iCellFirst], 0, cbrk-iCellFirst); |
探索SQLite如何在一个B树页中为nByte大小的单元分配空间? | 首先查找自由块链表,看看能不能在某一个自由块中找到符合nByte大小的空间。当在自由块链表中查找失败之后,首先看看有没有必要对页面进行碎片整理,当gap+2+nByte>top时才进行碎片整理。之后,在单元指针数组和单元内容区域之间的未分配空间中为nByte大小的内容分配内存。分配:top -= nByte; //相当于分配nByte大小内存 put2byte(&data[hdr+5], top);//对单元内容区域起始地址进行更新 |
探索SQLite如何计算一个页面上的剩余可用空间量? | SQLite计算一个页面上的剩余可用空间由两个部分构成,分别是碎片和自由块。碎片的大小通过data[hdr+7]可以计算得到。自由块的大小通过遍历自由块链表,计算出所有自由块的size之后,相加在一起可以得到自由块链表所占用的大小。需要注意的是,任何两个自由块之间的空间大小不小于4,因为小于4是代表是碎片,而SQLite不允许两个自由块之间存在碎片。可以通过反证法,假设两个自由块之间存在碎片,那么在生成两个自由块之中的任一个的时候就已经和另外一个自由块进行了合并操作。所以两个自由块之间不存在碎片。而当两个自由块之间的空间大于4时,则代表他们之间存在一个或多个cell。 |
探索在删除一个cell之后,SQLite如何释放该cell占用的空间? | 在删除一个cell时,首先需要定位到该cell的前一个自由块和后一个自由块。 在删除cell之前,cell占用的空间代表一个cell,但是在删除cell之后,在这之前cell占用的空间就会变成自由块。如上图,第一步:首先计算自由块2和cell之间空间的大小iFreeBlk-iEnd,如果iFreeBlk-iEnd<4,那么删除cell之后自由块2和其左边的自由块需要进行合并操作(因为SQLite不允许两个自由块之间直接存在碎片,如果存在,那么这两个自由块就会进行合并)。合并之后,需要更新iEnd。
第二步:如果自由块1和cell之间空间的大小iStart-iPtrEnd<4,那么同样,自由块1和其右边的自由块需要进行合并操作(该合并操作并不受之前cell和自由块2之间的合并操作(若有))。
第三步:更新空闲块链表的指针。 单元内容区域起始地址 x = get2byte(&data[hdr+5]); 如果iStart<=x,这时需要更新第一个空闲块的起始地址:put2byte(&data[hdr+1], iFreeBlk); 更新单元内容区域起始地址:put2byte(&data[hdr+5], iEnd);//删除的cell的起始地址和单元内容区域起始地址相等 如果iStart>x(cell的起始地址大于单元内容区域起始地址),这时需要put2byte(&data[iPtr], iStart);//让自由块1指向的下一个自由块的起始地址是iStart 必要时对之前cell占用的空间进行清零memset(&data[iStart], 0, iSize); 最后,让之前cell占用的空闲块指向下一个空闲块:put2byte(&data[iStart], iFreeBlk);设置之前cell占用的空闲块的大小:put2byte(&data[iStart+2], iSize); 需要注意的是,前面的合并操作会减少本页面碎片的总字节数,因为合并操作会使得碎片变成更大的空闲块的一部分。而且,还需要更新该页面的剩余可用空间的大小:pPage->nFree += iOrigSize;//iOrigSize是最原始的iSize。 |
探索SQLite如何计算B树页中某一个cell的size? | SQLite计算cell的大小总共有两个函数。分别是cellSizePtr(适用于B+树叶子页、B+树内部页和B-树叶子页)和cellSizePtrNoPayload(适用于B+树内部页)。 |
探索SQLite如何解析B树页中的某一个cell? | 对于B+树内部页,使用函数btreeParseCellPtrNoPayload函数。对于B+树叶子页,使用函数btreeParseCellPtr。对于B-树叶子页和内部页,使用函数btreeParseCellPtrIndex(因为B-树的内部页和叶子页都存储数据,所以B-树的内部页和叶子页的cell结构相似,区别是B-树内部页的cell的前4个字节是指向其子页的指针)。 |