2021SC@SDUSC SQLite源码分析(二)————B树与内存的页


pager模块把数据库的文件抽象成了基于页的文件。而B+树模块在它之上抽象出基于行的数据项。
然后表中的行可以通过很多种形式组织起来,比如输入顺序、相关性、哈希表、键值序列。
SQLite使用B+树来组织一个表中所有的行,不同的表有不同的B+树。
SQLite还把索引视为一个表,并且把索引存储在一个B树里,不同的索引有不同的B树。
B树和B+树很像,都是一种键值序列数据结构。SQLite不使用其他的组织行的方法。所以一个数据库就是一个B树和B+树的集合 。所有的这些树都是基于数据库页的,不能被分开。但是,不能有数据库页存储来自两个不同表的行。B/B+树模块的职责是组织树中的页,使得可以有效地存储和读取数据项。

一、BtShared结构分析

Btree结构中包含一个BtShared结构,包含了一个打开的数据库的所有页面的相关信息。
在系统运行中,有些数据库文件会被多个连接共享。这时,每个连接都会含有它所占有的这个数据库文件部分。而这个数据库文件的每个部分都会连接到相同的BtShared对象中。BtShared的定义在btreeInt.h中。
其结构如下所示:

struct BtShared {
  Pager *pPager;        /* The page cache */
  BtCursor *pCursor;    /* A list of all open cursors */
  MemPage *pPage1;      /* First page of the database */
  u8 inStmt;            /* True if we are in a statement subtransaction */
  u8 readOnly;          /* True if the underlying file is readonly */
  u8 maxEmbedFrac;      /* Maximum payload as % of total page size */
  u8 minEmbedFrac;      /* Minimum payload as % of total page size */
  u8 minLeafFrac;       /* Minimum leaf payload as % of total page size */
  u8 pageSizeFixed;     /* True if the page size can no longer be changed */
#ifndef SQLITE_OMIT_AUTOVACUUM
  u8 autoVacuum;        /* True if database supports auto-vacuum */
#endif
  u16 pageSize;         /* Total number of bytes on a page */
  u16 usableSize;       /* Number of usable bytes on each page */
  int maxLocal;         /* Maximum local payload in non-LEAFDATA tables */
  int minLocal;         /* Minimum local payload in non-LEAFDATA tables */
  int maxLeaf;          /* Maximum local payload in a LEAFDATA table */
  int minLeaf;          /* Minimum local payload in a LEAFDATA table */
  BusyHandler *pBusyHandler;   /* Callback for when there is lock contention */
  u8 inTransaction;     /* Transaction state */
  int nRef;             /* Number of references to this structure */
  int nTransaction;     /* Number of open transactions (read + write) */
  void *pSchema;        /* Pointer to space allocated by sqlite3BtreeSchema() */
  void (*xFreeSchema)(void*);  /* Destructor for BtShared.pSchema */
#ifndef SQLITE_OMIT_SHARED_CACHE
  BtLock *pLock;        /* List of locks held on this shared-btree struct */
  BtShared *pNext;      /* Next in ThreadData.pBtree linked list */
#endif
};

1:pPager

存储Btree页面缓存信息

2:pCursor

存储Btree中打开的一系列游标

3:pPage1

存放数据库文件的第一个页面

4:maxEmbedFrac:

Btree内部页中一个CELL最多能够使用的空间。255意味着100%,默认值为0x40(25%),这保证了一个页面至少包含4个CELL。

5:minEmbedFrac:

Btree内部页中一个CELL使用空间的最小值。默认值为0x20(12.5%)

6:minLeafFrac:

Btree叶子页中一个CELL使用空间的最小值。默认值为0x20(12.5%)

7:pageSize:

页面大小指示位(字节单位)

8:pageSizeFixed:

页面大小能否改变指示位

9:autoVacuum:

数据库是否支持autoVacuum指示位。

ps: autoVacuum数据库:当一个事务从数据库中删除了数据并提交后,数据库文件的大小保持不变。即使整页的数据都被删除,该页也会变成“空闲页”等待再次被使用,而不会实际地被从数据库文件中删除。执行vacuum操作,可以通过重建数据库文件来清除数据库内所有的未用空间,使数据库文件变小。但是,如果一个数据库在创建时被指定为auto_vacuum数据库,当删除事务提交时,数据库文件会自动缩小。使用auto_vacuum数据库可以节省空间,但却会增加数据库操作的时间。

10:usableSize

指示每个页面可用的字节数。一个页面 尾部可能保留一部分空间作为扩展,实际可以使用的页面字节数就是“页面大小-保留空间”。

11:maxLocal\ minLocal\maxLeaf\minLeaf

页面的一个CELL可以使用的最大。最小空间实际大小,通过下面公式可计算出来。

pBt->maxLocal = (pBt->usableSize-12)*pBt->maxEmbedFrac/255 - 23;
pBt->minLocal = (pBt->usableSize-12)*pBt->minEmbedFrac/255 - 23;
pBt->maxLeaf = pBt->usableSize - 35;
pBt->minLeaf = (pBt->usableSize-12)*pBt->minLeafFrac/255 - 23;
其中 12个字节为每个页面头的大小,23个字节为每个CELL内部的保存其它管理信息

2-byte 保存CELL的地址
4-byte 保存孩子节点的页号
9-byte 最多可以使用9个字节来保存Btree中的关键字值
4-byte 保存4个字节的数据值
4-byte 保存overflow 页面号

我们可以看到23个字节是最大可能使用的管理空间,根据配置不同实际不会使用到23个字节。

12:pBusyHandler:

保存客户端提供的数据库忙的回调函数句柄,用于忙时的回调。

13:inTransaction

存在TRANS_NONE\TRANS_READ\TRANS_WRITE三种状态,由于可能存在多个用户共享一个Btree结构,在TRANS_WRITE状态下只能有一个用户能够进行写事务。在TRANS_READ 状态下任意多个用户能进行读事务。

14:pLock

一个表级锁的链表

15:pNext

一个线程可能打开不同的数据库,该结构表示该线程保存的一系列的Btree数据。

二、内存中的页

数据库中加载到内存中的页的定义位于btreeInt.h,其结构如下:

struct MemPage {
  u8 isInit;           /* True if previously initialized.MUST BE FIRST! */
  u8 nOverflow;        /* Number of overflow cell bodies inaCell[] */
  u8 intKey;           /* True if table b-trees.  False for index b-trees */
  u8 intKeyLeaf;       /* True if the leaf of an intKey table*/
  u8 noPayload;        /* True if internal intKey page (thusw/o data) */
  u8 leaf;             /* True if a leaf page */
  u8 hdrOffset;        /* 100 for page 1.  0 otherwise */
  u8 childPtrSize;     /* 0 if leaf==1.  4 if leaf==0 */
  u8 max1bytePayload;  /* min(maxLocal,127) */
  u8 bBusy;            /* Prevent endless loops on corruptdatabase files */
  u16 maxLocal;        /* Copy of BtShared.maxLocal orBtShared.maxLeaf */
  u16 minLocal;        /* Copy of BtShared.minLocal orBtShared.minLeaf */
  u16 cellOffset;      /* Index in aData of first cell pointer*/
  u16 nFree;           /* Number of free bytes on the page*/
  u16 nCell;           /* Number of cells on this page,local and ovfl */
  u16 maskPage;        /* Mask for page offset */
  u16 aiOvfl[5];       /* Insert the i-th overflow cell beforethe aiOvfl-th
                       ** non-overflow cell */
  u8 *apOvfl[5];       /* Pointers to the body of overflowcells */
  BtShared *pBt;       /* Pointer to BtShared that this page ispart of */
  u8 *aData;           /* Pointer to disk image of the page data */
  u8 *aDataEnd;        /* One byte past the end of usable data*/
  u8 *aCellIdx;        /* The cell index area */
  u8 *aDataOfst;       /* Same as aData for leaves.  aData+4 for interior */
  DbPage *pDbPage;     /* Pager page handle */
  u16 (*xCellSize)(MemPage*,u8*);             /* cellSizePtr method */
  void (*xParseCell)(MemPage*,u8*,CellInfo*);/* btreeParseCell method */
  Pgno pgno;           /* Page number for this page */
};

为了方便原地操作和从数据库读写数据,SQLite把每个数据库(包括内存数据库)分成大小固定的多个页。页的大小是2的指数,可以在512到32,768之间,默认大小是1024B。上界是由存储页大小的2-byte signed int 变量决定的。
数据库(可以扩展和压缩)是一个页组成的数组。页的索引叫做页号(page number)。页号从1开始,上界是2,147,483,674(231-1)。(上界可以更大,这取决与文件系统的限制)。第0页是虚页。第1页和之后的页存储在数据库文件中,存储偏移量是0,即从文件开头开始存储第1页。

页的种类有4种:叶子页,内部页,溢出页,自由页。自由页是非激活页(还没有使用的页),其他的是激活页。B+树内部页包含搜索信息(B树内部页包含搜索信息和数据)。B+树的叶子页存储实际的数据(例如:表中每行的数据)。如果一页不能盛放一行数据,数据的一部分会存储在B+树的叶子页,另一部分存储在溢出页中。

文件头

此部分参考资料:《Inside SQLite》

SQLite可以使用任何类型的页,除了页1,页1永远是B+树的内部页,保存有100byte的文件头信息,存储偏移量是0。文件头信息决定了该数据库文件的结构。当数据库文件被创建时,SQLite初始化文件头信息。文件头的格式如下表所示,前两列的存储单位是byte。
在这里插入图片描述
Header string:
这是一个16byte的string:”SQLite format 3.”
Page size:
页的大小。
File format:
偏移量为18、19的两个byte用来存储文件格式版本,在当前版本中,这两个值都是1,否则将返回一个error值。如果将来文件格式发生改变,这两个值会增长以表示新的文件格式版本。
Reserved space:
处于一些原因,SQLite在每一页底部保留一小块固定空间(<=255bytes),空间的大小保存在reserveed space变量中,默认值是0。使用SQLite内建加密模式时,这个值会发生变化。
Embedded payload:
最大碎片负载(偏移量是21)指的是每页能够用于存储B/B+树内部节点的空间。255表示100%。默认值是64(25%),这个值用来限定cell的最大值,即每个节点最少有4个cell。如果cell的大小超过允许的最大值,SQLite就创建溢出页,把尽量多的byte移动到溢出页中,但是不能在分割的过程中导致cell的大小小于最小embedded payload(偏移量是22,默认值是32,即12.5%)。
叶子页最小负载(偏移量23)和最小embedded payload相似,但是是针对B+树的叶子页来说的。(默认值是32,12.5%)。叶子页的最大负载百分比永远是100%,文件头中并没有保存该值的变量。
File change counter:
文件更改次数(偏移量24)被用于事务管理。每个事务都会使这个值加1.这个值用来指示什么时候数据库被改变,辅助pager模块避免盲目刷新cache。截止到目前为止,这个功能还没有实现。Pager模块负责增加这个值。
Free list:
自由页链表(偏移量32)存储未使用的页。自由页页数存储在偏移量36。

在偏移量40的地方,有15个关于B+树和虚拟机模块的4bytes的整型变量。它们存储许多元信息,包括模式缓存号(偏移量40),每次模式变化会改变这个值。视图层的文件格式信息(偏移量44),page缓存大小(偏移量48),自动清空标志(偏移量52),文本编码(偏移量56,1:UTF-8, 3:UTF-16 LE, 4:UTF-16 BE),用户版本号(偏移量60)。在源文件btree.c中有关于这些变量更多的信息。

另外值得注意的是,所有的多字节整型变量都是大端存储的,这允许将数据库文件安全地从一个平台移动到另一个平台。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值