关于SQLite 数据库的工作原理介绍干货分享!

酷酷的小傻子 2021-09-24 16:43:28 浏览数 (4449)
反馈

数据库是构建软件系统的重要组成部分,用于高效地存储和读取数据。在这里,我们将使用 SQLite 的早期版本来讨论数据库实现的一些架构细节。

SQLite 是一个小型数据库应用程序,用于数百万个软件和设备。SQLite 是由 D.Richard Hipp 于 2000 年 8 月发明的。SQLite 是一种高性能、轻量级的关系数据库。如果您愿意在编码级别学习数据库的内部结构,那么 SQLite 是最好的开源数据库,它具有高度可读的源代码和大量文档。阅读更高版本的 SQLite 变得有点困难,因为它包含许多新功能。为了理解数据库内部的基本实现,你应该对数据结构有很好的了解,一些关于计算理论的知识,以及操作系统是如何工作的。

在这里,我们将研究 SQLite 2.5.0 版本。您可以在GitHub找到SQLite 后端的简单实现。 另外,我发现这个存储库 包含用于代码读取的 SQLite 2.5.0 实现。

什么是数据库?

将数据保存在平面文件中读取和存储数据的效率不高。数据库以适当的顺序组织数据,以便数据读取和写入速度更快。数据可以是结构化的、半结构化的或非结构化的。数据库主要用于存储结构化和半结构化数据。可以根据要存储的数据结构类型对数据库进行如下挖掘。

  1. 关系型数据库:常用的具有表结构的数据库类型。表可以与其他表有关系。SQL 语言用于操作此类数据库上的数据。
  2. 键值数据库:与关联的键一起存储的数据。可以使用给定的键检索数据。内存数据库通常是这种类型的数据库。
  3. 对象数据库:数据结构更像是一个对象而不是一个表。
  4. 图数据库:图数据库是节点和边的集合,主要用于数据挖掘和社交媒体应用程序。

SQLite 数据库架构

SQLite 数据库架构分为两个不同的部分,分别命名为核心和后端。核心部分包含接口、分词器、解析器、代码生成器和虚拟机,它们为数据库事务创建执行顺序。后端包含 B-tree、Pager 和 OS 接口来访问文件系统。Tokenizer、Parser 和代码生成器统称为编译器,它生成一组运行在虚拟机上的操作码。

从哪里开始?

要了解数据库的架构,您需要具备以下先决条件。

  1. 对数据结构和算法有很好的理解。尤其是 B 树、链表、Hashmaps 等数据结构。
  2. 对计算机体系结构有一定的了解。如何读写磁盘,分页和缓存如何工作。
  3. 有限自动机、上下文无关语法等理论计算机和一些正则表达式知识。

SQLite 架构

VFS(虚拟文件系统)

Unix 和 Windows 上的文件访问彼此不同。VFS 提供通用 API 来访问文件,而无需考虑其运行的操作系统类型。该 API 包括打开、读取、写入和关闭文件的函数。以下是 VFS 中用于读取、写入数据到文件中的一些 API。

/* 
Create a connection to file to read write 
zFilename : file name 
id : file pointer 
pReadonly : read or write 
*/
int sqliteOsOpenReadWrite(const char *zFilename, OsFile *id,int *pReadonly);
/* 
Acqure the lock to read file. This function should be 
called before caling to any file read function. 
Return 0 if success
id : file pointer 
*/
int sqliteOsReadLock(OsFile *id);
/* 
Get the write lock to write into a file. This function should called before
doing any write operation into the file system.
Return 0 if success
id : file pointer
*/
int sqliteOsWriteLock(OsFile *id);
/* 
Move to the given number of offest to read or write into the file
*/
int sqliteOsSeek(OsFile *id, int offset);
/* 
Read amt bytes from the file with offset pointed by sqliteOsSeek
*/
int sqliteOsRead(OsFile *id, void *pBuf, int amt);
/* 
Write amt bytes from the pBuf into the file
*/
int sqliteOsWrite(OsFile *id, const void *pBuf, int amt);

在这里,您可以使用 sqliteOpenReadWrite 函数开始使用文件。此函数为您提供一个指向文件的指针,该文件可用于读取或写入数据。接下来,应该在执行任何读写操作之前获取锁。如果它只是一个读操作,那么应该获取读锁。应该为读和写事务获取写锁。 

然后可以通过将文件的偏移量提供给 sqliteOsSeek 函数来查找位置来完成读写。偏移量是从文件的起点到数据应该写入或读取的位置的字节数。

Pager

页是文件系统上数据库事务的最小单位。当数据库需要从文件中读取数据时,它会将它作为一个页面来请求。一旦页面被加载到数据库引擎中,如果它经常访问它的缓存,它就可以存储该页面。页数从一页开始编号。第一个页面称为根页面。页的大小是恒定的。

/*
Open pager with the given file name
*/
int sqlitepager_open(Pager **ppPager,const char *zFilename,int nPage,int nEx);
/*
Get page specified by the page number
*/
int sqlitepager_get(Pager *pPager, Pgno pgno, void **ppPage);
/*
Start to write data into a page specified in pData
*/
int sqlitepager_write(void *pData);
/*
Commit page changes into the file
*/
int sqlitepager_commit(Pager*);
/*
Close the connection to the file
*/
int sqlitepager_close(Pager *pPager);

Btree

Btree 是一种数据结构,用于根据数据的值将数据存储为树。BTree 最简单的形式是二叉树。数据库使用 Btree 数据结构来存储索引以提高数据库的性能。Btree 的每个节点都包含一个用于索引的键列表。可以为表中的每一列创建 Btree 索引。每个 Btree 都有根页面,这是任何 Btree 搜索的起点。

为了指向 Btree 上的一行,使用了称为“Cursor”的特殊指针。游标用于指向一个记录,该记录由页面 id 和偏移量指定,称为 idx。SQLite 将数据库模式存储在称为“master_table”的表上。master_table 总是存储在数据库的第一页上。 

了解更多关于SQLite的B树的设计在这个文章。

/*
Open file connection to a page file name specified by zFileName with 
nCache size cache
*/
int sqliteBtreeOpen(const char *zFilename, int mode, int nCache, Btree **ppBtree)
/*
Start transaction. This function should called before any btree modification 
operations
*/
int sqliteBtreeBeginTrans(Btree *pBt)
/*
Insert key pKey with nKey byte and value pData with nData byte put 
into the Btree
*/
int sqliteBtreeInsert(BtCursor *pCur, const void *pKey, int nKey, 
                      const void *pData, int nData)
/*
Write data into the file
*/
int sqliteBtreeCommit(Btree *pBt)
/*
Move cursor to the matching pKey with nKey bytes
*/
int sqliteBtreeMoveto(BtCursor *pCur, const void *pKey, int nKey, int *pRes)

VDBE(虚拟数据库引擎)

VDBE 是运行一组操作的虚拟机,由代码生成器生成。包括插入、删除、更新、选择在内的所有 SQL 命令都转换成一组操作码,然后在虚拟机上运行。每个操作码包含三个名为 p1、p2 和 p3 的输入。您可以将此输入视为函数的输入。

下面我为以下 SQL 选择语句添加了示例执行操作码堆栈。PC 是程序计数器的指令 ID。 对我来说,SQLite 中最有趣的事情是我可以通过在 SQL 查询的开头附加“explain”关键字来查看给定 SQL 代码的一组 VBDE 操作码指令。

explain select * from foo;
个人电脑操作码P1P2P3评论
1列数10将列数设置为 1
2列名00价值将列名设置为“值”
3打开03

打开光标并将其指向第三页,即 foo 表的根页(p3 不重要

4验证Cookies460确保架构未更改
5倒带011将光标移动到第一个条目
6柱子00从 Btree 负载读取数据并将其放入堆栈
7柱子00
8ne110

从堆栈中弹出顶部的两个元素。如果它们不相等,则跳转到指令 P2。否则,继续执行下一条指令。

9打回来10

从堆栈中弹出 P1 值并将它们组成一个数组。这将是此 SQL 表达式的结果行

10下一个05

将光标移动到下一条记录,如果数据退出转到 P2 否则转到下一行

11关闭00关闭光标

编译器

Tokenizer、Parser 和 Code Generator 统称为编译器,可生成在 VBDE 上运行的操作码集。Tokenizer 通过扫描 SQL 代码生成一组令牌。然后,它验证语法并生成解析树。Lemon 解析器用于通过预定义的上下文无关语法来解析给定的 SQL 代码。代码生成器将这个解析树转换成一个用 SQLite 操作码编写的小程序。

结论

SQLite 是一种简单、轻量级、高性能的关系型数据库,广泛用于软件设计。SQLite 的早期版本是用简单的架构和高度可读的代码编写的。Pager 提供了一个抽象层,将数据作为固定大小的块读写到文件系统中。而 Btree 提供了一种在内存中存储数据的高效方式,以更快地访问数据。当 SQL 进入 SQLite 时,它​​会将 SQL 转换为 SQLite 机器码并在 VBDE 上运行。结果通过 API 返回给用户。


SQL

0 人点赞