SQLite内部机制和新特性

SQLite内部机制和新特性

一、B-tree和Pager模块

B-tree为SQLite VDBE提供了O(logN)级时间复杂度的插入和查询,通过双向遍历记录实现O(1)级时间复杂度的删除。B-tree是自平衡的,能够对碎片清理和内存再分配进行自动管理。B-tree对如何读写磁盘没有限定,只是关注页之间的关系。

B-tree在需要一个页或修改一个页的时候会通知Pager。修改页时,如果使用传统的回滚日志,pager首先将原始页复制到日志文件,同样,pager在B-tree完成操作时收到通知,并基于所处的事务状态决定如何处理。

1.数据库文件格式
数据库中所有的页都是以1开始编号的。一个数据库是由多个B-tree组成的-每张表以及每个索引各对应一个B-tree(表使用B+tree,索引使用B-tree)。数据库中每张表或索引都以根页作为第一页。所有的索引和表都存储在sqlite_master表中。

数据库的第一页比较特殊,第一页的前100B包含了头文件,用于说明数据库文件,它包含了:库版本、模式版本、页大小、编码方式、是否启用自动清理等所有需要创建数据库所需的数据库持久性设置以及通过编译指示设置的其他参数。

  1. 页重用及清理
    SQLite使用空闲链表回收页,当一个页上的所有记录都被删除时,该页将会被存回到空闲页链表中预留重用。之后如果再添加新信息,最近被回收的页将会被选中,以避免创建新页(创建新页会增加数据库大小)。运行VACUUM命令可以释放空闲链表中的页来减小数据库的大小。实际上VACUUM命令是重新建立了新的数据库并将旧的数据复制到此,在这过程中忽略空闲链表中的页。如果启动数据库自动清理功能,SQLite将不会使用空闲页链表,而是在每个提交行为时自动进行数据库收缩处理。

  2. B-tree记录
    B-tree中的一系列页是由B-tree记录组成,这些记录也称为有效载荷。这些记录并非我们所说的数据库中的记录,而是更为原始的格式。一个B-tree记录由两个原始的域组成:键值域和数据域。键值域是每个数据库表中所包含的ROWID值或主键值;在B-tree中,数据域可以包含任意类型的内容。最终我们所说的数据库记录存储在数据域中。

    B-tree用来保持记录有序并方便记录的2查询,同时,键值域能够完成B-tree的主要工作(定位下一个记录是由B+tree完成的)。此外,记录的大小是可变的,这取决于内部的键值域和数据域的大小。一般而言每个页都拥有多个有效载荷。如果有效载荷超出了一个页,则会出现跨页的情况(blob记录)。

  3. B+tree
    B-tree记录按键值存储。所有的键值在一个B-tree中必须是唯一的,表使用B+tree定义在内部页中。不包含表数据。B+tree的根页面和内部节点页面都用于搜索导航,这些页的数据域指向下一层页,这些页只包含键值。所有数据库记录都存储在叶子页中,在叶子页层,记录和页按记录序排列,以便B-tree游标能够遍历记录。无论是前访还是后访,只需要用叶子页就可以使得在O(1)时间复杂度下遍历记录成为可能。下图是B+tree的组织结构图:

  4. 记录和域
    叶子页的数据域中的数据库记录由VDBE控制,数据库记录以特殊的二进制格式存储,这些记录描述了记录中的所有域。记录格式由连续字节流形式的逻辑头段和数据段组成。逻辑头段包括逻辑头大小(由可变大小的64b整数表示)和用于描述数据段中每个存储类型(也是64b)的数组。如下图所示记录结构。
    在这里插入图片描述

    类型个数与记录域个数一致。此外,类型数组中每个索引对应于域数组的索引。类型描述了对应域值的数据类型大小。下图是域类型值:

    在这里插入图片描述

    例子: 如下图:
    在这里插入图片描述 表中的记录如下图
    在这里插入图片描述

    其中记录头长4B,从头大小可以看出本身采用单字节编码,第一个类型对应id域,为1B的有符号整数。第二各对应season域,同样也为1B的有符号整数。name域类型标识是一个奇数,表明该域数据为text类型,大小为(49-13)/2=18B。通过这些信息VDBE能够解析记录的数据并提取各字段的数据。

  5. 层次化数据组织
    基本上,具体的数据单元由堆栈中的各模块处理,自下而上,数据变得更准确、更详细。自上而下,数据变得更模糊、更集中。C API负责处理域值,VDBE负责处理记录。B-tree负责处理键值和数据,pager负责处理页,操作系统接口负责处理二进制数据和原始数据存储。每个模块负责维护自身在数据库中对应的数据部分,然后依靠底层提供所需提取信息的初始数据,并从中提取内容。结构图如下

  6. 页面溢出
    有效载荷及其内容可有不同的大小,然而页面大小是固定不变的。因此给定的有效载荷总有可能超出单页装载大小。当这种情况发生时,额外的有效载荷将加到溢出页面的链接表上。有效载荷将在有序的链表中显示。

2.B-tree API
B-tree模块有自己的API,它是独立于C API 的,研究B-tree API的目的是比较全面的了解SQLite对B-tree的使用、了解SQLite的内部机制。

  1. 存取及事务函数
    sqlite3BtreeOpen:打开一个数据库文件并返回一个B-tree对象
    sqlite3BtreeClose:关闭数据库
    sqlite3BtreeBeginTrans:开始一个新事务
    sqlite3BtreeCommit:提交当前事务
    sqlite3BtreeRollback:回滚当前事务
    sqlite3BtreeBeginStmt:开始一个语句事务
    sqlite3BtreeCommitStmt:提交一个语句事务
    sqlite3BtreeRollbackStmt:回滚一个语句事务

  2. 表函数
    sqlite3BtreeCreateTable:在数据库文件中新建一个空B-tree,是采用表格式(B+tree)还是索引格式(B-tree),由符号设置决定。
    sqlite3BtreeDropTable:删除数据库中的一个B-tree。
    sqlite3BtreeClearTable:删除B-tree中的所有数据,但保持B-tree结构完整性。

  3. 游标函数
    sqlite3BtreeCursor:创建一个指向特定B-tree的游标。游标可以是读游标,也可以是写游标,但是读游标和写游标不可以在同一个B-tree中存在。
    sqlite3BtreeCloseCursor:关闭B-tree游标。
    sqlite3BtreeFirst:移动游标至B-tree第一条记录。
    sqlite3BtreeLast:移动游标至B-tree最后条记录。
    sqlite3BtreeNext:移动游标至当前游标下一条记录。
    sqlite3BtreePrevious:移动游标至当前游标前一条记录。
    sqlite3BtreeMoveto:移动游标至同指定键值匹配的一条记录。如果没有匹配记录,则移动游标至一条已存在并接近匹配记录的位置。

  4. 记录函数
    sqlite3BtreeDelete:删除游标所指记录
    sqlite3BtreeInsert:在B-tree的适当位置插入一条记录
    sqlite3BtreeKeySize:返回当前游标所指记录关键字长度
    sqlite3BtreeKey:返回当前游标所指记录的关键字。
    sqlite3BtreeDataSize:返回当前游标所指记录数据长度
    sqlite3BtreeData:返回当前游标所指记录数据

  5. 配置管理函数
    sqlite3BtreeSetCacheSize:设置控制页缓存大小以及同步写入
    sqlite3BtreeSetSafetyLevel:改变磁盘数据的同步方式,以增加或减少数据库抵御操作系统崩溃或电源故障的损害能力,1级等同于异步,风险高写入快,synchronous=OFF;2级默认,风险较下synchronous=NORMAL;3级风险接近0,但是写入慢synchronous=FULL。
    sqlite3BtreeSetPageSize:设置数据库页大小
    sqlite3BtreeGetPageSize:获取数据库页大小
    sqlite3BtreeSetAutoVacuum:设置数据库自动清理空闲页属性。
    sqlite3BtreeGetAutoVacuum:获取数据库是否自动清理空闲页属性。
    sqlite3BtreeSetBusyHandler:设置繁忙句柄

二、显示类型、存储类型以及亲缘性介绍

1.显示类型
SQLite支持显示类型。显示类型存在多种解释,显示类型就是如何定义或决定一个变量或值。有以下两种解释:

  • 显示类型是指变量的类型必须在代码中显式地声明,如C/C++、JAVA等语言都是显式类型定义。
  • 显示类型是指变量完全无类型,而只有值有类型,与动态定义语言基本一致。一个变量可以维护任何值,并且变量的类型在任何时候都是由当时他所维护的值所决定的。

SQLite兼用了1和2,它表明:列可以有类型且类型可依据值来判断,类型亲缘性阐述了这两者之间的关系,并且平衡了这两种类型。

2.类型亲缘性
在SQLite中,列没有类型或域的概念。虽然可以定义列的类型,但是内部实现采用了欸写亲缘性。亲缘性决定了SQLite存储列值的存储类。在给定列值的实际列存储类是随值存储类和亲缘性而变的。

列的类型和亲缘性
每个列都有亲缘性,可分为四类:数值、整数、文本、空。列的亲缘性直接由声明的类型决定。因此,当表中声明一个列时,选择的类型最终决定列的亲缘性。SQLite根据以下规则设置列的亲缘性。

  1. 列默认亲缘性类型是数值,如果一个列不是整数、文本或空,将自动赋予该列的亲缘性为数值。

  2. 如果声明类型含 “int” “real”,则亲缘性为数值。

  3. 如果声明类型含 “char” “clob” “text”,则该亲缘性为文本。

  4. 如果列类型含 “blob”,或没有声明类型,则将亲缘性为空。

3.亲缘性和存储
每个亲缘性影响了值在关联列中如何存储。存储管理规则如下:

  1. 数值列可能有五种存储类。数值列偏向于数值存储类。当一个文本插入数值列时,会试图将其转变为整数存储类,如果失败,则试图转为实数存储类,再次失败则采用文本存储类。

  2. 整数列将尽可能参照数值列。整数列实际将存储实数值,但如果实数值没有小数部分,将采用整数存储类存储。整数列也会试图存储文本值,并试图以整数形式存储,如果失败,文本将以文本类型存储。

  3. 文本列将所有整数值和实数值转为文本类型。

  4. 空列不会试图转成任何值,所有的值都将采用指定存储类型存储。

  5. 列不会试图转换空值或blob值,不管亲缘性如何,空和blob值始终存储于每个列中。

4.执行中的亲缘性
参考以下例子理解亲缘性

	create table stu(i int, n real, t text, b blob);
	1. insert into stu values(3.14, 3.14, 3.14, 3.14);
	2. insert into stu values('3.14', '3.14', '3.14', '3.14');
	3. insert into stu values(314, 314, 314, 314);
	4. insert into stu values(x'314', x'314', x'314', x'314');
	5. insert into stu values(null, null, null, null);	
	
	select rowid, typeof(i), typeof(n), typeof(t), typeof(b) from stu;

查询结果如下:

	7. real       real      text  real
	8. real       real      text  text
	9. integer    integer   text  integer
	10. blob       blob      blob  blob
	11. null       null      null  null

存储类排序:空 > real > integer > text > blob

类型亲缘性和严格类型间的主要区别是,类型亲缘性从不对不兼容类型进行冲突约束,SQLite将为所有列中存入的任何值寻找一个数据类型。

存储类及类型转换:存储类有时会影响值得比较方式。比较前,SQLite有时将在数值存储类(整数和实数)和文本存储类之间进行值转换。对于二进制比较采用以下规则:

  1. 一个列值于表达式比较时,列的亲缘性在比较前应用于结果表达式。

  2. 当两个列值比较时,如果一个是整数或者数值亲缘性列。而另一个不是,则数值亲缘性将用于非数值列的文本值。

  3. 当两个表达式比较时,SQLite不会采取任何转换,而时对结果进行比较。如果表达式类似于存储类,则用于该存储类关联的比较函数进行值比较。否则采用基本的存储类进行比较。

三、预写日志

1.WAL工作原理
传统的操作模式中,SQLite使用回滚日志,从SQL状态集中捕获预修改数据,然后修改数据库文件,使用WAL时,将反过来处理。取代写入原始信息模式,预修改数据放入回滚日志,利用WAL放弃原始未关联数据库文件的数据。WAL文件用来记录给定事务导致的数据变化。一个提交操作变为特殊的写进WAL记录,以表明前面修改确实完成,并实现ACID原则。

数据库文件和日志文件之间的角色变化会立刻改变事务的动态性能,不用在数据库文件中竞争相同的数据库页。在WAL中,多事务可以同时记录他们数据的改变,并能够从数据库中读取未改变的数据。

1.检查点:一个正在写入不断增长的WAL文件的没有结束的数据变化流,无法能扩展或承担难以预测的文件系统故障。WAL使用检查点函数将修改写回数据库。这个过程自动处理。默认情况下,当WAL文件发现有1000页修改时,将调用检查点。可以修改检查点的参数。

2.并非
因为现在修改是写入WAL文件,而不是数据库。从启用WAL的数据库进行数据读取操作会在WAL文件开始查找最后一条记录。这点成为只读一致性结束标志,这样就不用考虑写数据操作超越文件的该点,参考结束标记不用考虑提交后的变化。每个操作的访问都是独立的,所以可以有多个线程,每个线程在WAL文件中都有专属的独立结束标志符。

访问一个数据页,读操作采用WAL索引结构来扫描WAL文件,以确认修改页是否存在,如果可以找到,则采用最近的版本。如果找不到,则意味着上个检查点至今为止没有发生修改。并且读操作是从数据库文件访问数据页。WAL索引结构在共享内存中实现,意味着所有线程和进程访问相同的内存空间。

2.激活和配置WAL
激活命令:PRAGMA journal_mode=WAL;
成功返回字符串wal。

3.WAL优缺点

优点:
1.读操作不会阻塞写操作,写操作也不会阻塞读操作。
2.大多数场景中,与回滚日志相比,WAL会更快。
3.磁盘I/O变得更可预见,并且调用系统函数fsync()会更少,因为所有的WAL写操作是线性写入日志文件,很多I/O变得连续并能够按计划执行。
缺点:
1.只能单机操作
2.对于非常大的事务(GB级别),WAL性能将会降低,运行非常大的事务会引入额外的开销

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

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值