SQL数据库揭秘

英文原文

SQLite

本文里,我们将会通过SQLite的一个早期版本来讨论一下数据库实现的一些架构细节

简介

    数据库是软件系统里很重要的一个组成部分,它主要用来高效地存储和读取数据。本文里,我们将会通过SQLite的一个早期版本来讨论一下数据库实现的一些架构细节。

    SQLite是一个小型数据库软件,但是它被广泛地用在成千上万的软件系统和硬件设备里。它是在2000年8月份被D.Richard Hipp发明的。SQLite是一个高性能的,轻量级的关系型数据库系统。如果你想在代码层面学习数据库的内部实现的话,SQLite是众多开源数据库里最好的选择,它的文档很多,而且代码的可读性非常高。但是,如果去阅读最新版本的SQLite的代码的话,就显得比较困难了,因为它包含了很多新的特性。另外,为了便于理解数据库的实现原理,你需要熟悉数据结构,了解计算原理以及操作系统原理。

    在这里,我们会使用SQLite 2.5.0版本。你可以在GitHub上找到SQLite的一个简单实现。另外,这个代码仓库里有SQLite 2.5.0版本的代码,可以拿来参考。

为什么需要数据库?

    使用纯文件的方式来存储和读取数据是效率比较低的。数据库会通过合适的方式来组织数据,这样的话,数据的读取和写入都会比较快。数据可以是结构化的,半结构化的或者完全非结构化的。根据存储的数据类型,数据结构可以分为以下几类:

  1. 关系型数据库:常用的数据库,基于表结构的。数据表之间可以有关联关系。这种数据库可以用SQL语言来操作数据。
  2. 键-值数据库:通过键-值对来存储数据。数据可以通过指定的键来读取。内存数据库大多都是这种类型的数据库。
  3. 对象数据库。数据结构更像一个对象而不是一个表。
  4. 图数据库:图数据库存储的是节点和边,常用于数据挖掘和社交媒体应用。

SQLite 数据库架构

    SQLite数据库架构可以分为两个部分,分别叫做内核(core)和后台(backend)。内核部分包含接口,分词器,解析器,代码生成器和虚拟机,它负责生成数据库事务的执行指令。后台包含B-tree,页(Pager)以及用来读取文件的操作系统接口。分词器,解析器和代码生成器合起来成为编译器,它负责生成在虚拟机上执行的指令。

从哪里开始?

    了解一个数据库的架构,你需要有以下预备知识:

  1. 熟悉数据结构和算法。特别是B-tree,链表,哈希表等数据结构。
  2. 对计算机系统结构(computer architecture)有一定的了解。例如文件是如何从磁盘里进行读写的,分页系统以及缓存是如何工作的。
  3. 一些计算原理的知识,例如有限自动机以及正则表达式。

SQLite 架构

image

虚拟文件系统VFS(Virtual File System)

    在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)

    页是数据库在文件系统里的数据存储的最小单元。它是数据库从磁盘读取数据的最小单位,当数据库需要加装数据的时候,它会一次性读取整个页的数据。如果被加载的页被访问比较频繁的话,它就会被保存在数据库引擎的缓存里。页都会被编号,起始号是1。第一页被称为根页,而且一个页的大小是固定的。

    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是二叉树(Binary tree)。数据库使用Btree来保存索引,用以提高其性能。Btree的每个节点都会保存一列用来进行索引的值。而且你可以为数据表的每一列都创建一个Btree索引。每个Btree都包含一个根节点,它是搜索的起始点。

“游标(Cursor)”是用来标识Btree里的一条记录的一个特殊指针。游标通过页码(page id)和位置(offset)来索引一个数据记录。SQLite通过master_table这个表来存储数据表元数据,并且master_table的数据是存放在数据库的第一个数据页里。

    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(虚拟数据库系统,Virtual Database Engine)

    虚拟数据库系统(VDBE)是一个可以运行代码生成器生成的指令的虚拟机。包括insert,delete,update,select在内的SQL指令都会被转换成一系列指令,然后这些指令都会在虚拟机上运行。每个指令包含三个输入参数,分别是p1,p2和p3。你可以把它们看着是函数的输入参数类似。

    以下是我为下面这个select语句构造的执行指令栈。PC是程序计数器的指令id。SQLite中比较有趣的一点是,你可以通过在SQL语句前面加上explain命令来获取该SQL对应虚拟数据库系统(VDBE)的指令。

explain select * from foo;

PC OPCode P1 P2 P3 备注
1 ColumnCount 1 0 设置列数为1
2 ColumnName 0 0 value 设置列名为“value”
3 Open 0 3 foo 打开游标(cursor),并且指向第3页,该页是foo表的根页
4 VerifyCookies 46 0 确认表结构没有变动
5 Rewind 0 11 重置游标到第一个数据项
6 Column 0 0 从Btree里读取数据,并存放在栈里
7 Column 0 0
8 Ne 1 10 取出栈顶的两个元素进行对比。如果相等,就跳到P2的指令;否则的话,继续执行下一条指令。
9 Callback 1 0 从栈顶取出P1个值并且转换成一个数组,这就是SQL表达式的执行结果。
10 Next 0 5 将游标移动到下一条记录上,如果存在数据的话,就跳到P2所对应的指令继续执行;否则,就继续执行下一条。
11 Close 0 0 关闭游标

编译器

    分词器,解析器以及代码生成器组合起来就是编译器,它可以生成在虚拟数据库引擎(VDBE)上执行的指令。分词器通过扫描SQL语句来生成一系列的标识符(Token)。然后,再校验语法并生成语法树(parse tree)。最后,代码生成器将语法树转换成一段SQLite指令的小程序。

结论

    SQLite是一个简单,轻量级,高性能的关系型数据库,它被广泛用于软件系统设计。早期版本的SQLite的实现架构简单,并且代码可读性非常高。页提供了一种将文件读取封装成对固定大小数据块进行操作的抽象层。Btree提供了在内存里高效存储和访问数据的途径。最后,当SQL被加载到SQLite数据库的时候,会被转换成SQLite机器码并在虚拟数据库系统(VDBE)上执行。最终结果会通过API返回给用户。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,744评论 6 502
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,505评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 163,105评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,242评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,269评论 6 389
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,215评论 1 299
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,096评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,939评论 0 274
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,354评论 1 311
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,573评论 2 333
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,745评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,448评论 5 344
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,048评论 3 327
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,683评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,838评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,776评论 2 369
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,652评论 2 354

推荐阅读更多精彩内容