你的位置:首页 > 数据库

[数据库]SQLite学习笔记(十一)虚拟机原理


前言
      我们知道任何一种关系型数据库管理系统都支持SQL(Structured Query Language),相对于文件管理系统,用户不用关心数据在数据库内部如何存取,也不需要知道底层的存储结构,熟悉SQL,就能熟练使用数据库。SQL的引入,使得数据库系统需要将SQL转换为内部的数据结构,然后与底层的存储结构打通,达到用户存取数据的目的。所谓的SQL对应的数据结构,我们通常称之为执行计划,每个SQL执行前,都需要生成执行计划,然后执行。SQL如何变化到等价的执行计划?我们熟悉的数据库,Oracle,Sqlserver,Mysql等通过对SQL进行词法分析,语法分析,语义分析,生成执行计划等步骤,最终生成执行计划,这个计划一般是一个复杂的数据结构。SQLite也通过以上几步生成执行计划,但特别的是,SQLite的执行计划是一串指令流,这个指令流是由代码生成器生成,代码生成器将语法树翻译成一种SQLite专用的内部指令,通过虚拟机来解析执行。指令流相当于SQL与虚拟机的中介,由于指令流是扁平的,SQLite提供方法(PRAGMA vdbe_trace=ON)让用户可以看到执行SQL的每一条指令,清楚地知道数据在SQLite内部是如何流转的。本文主要讲SQLite的虚拟机(Virtual Database Engine,简称VDBE)的原理以及相关的内部指令。

虚拟机
     所谓虚拟机是指对真实计算机资源环境的一个抽象,它为语言程序提供了一套完整的计算机接口。比如我们熟悉的JAVA语言,我们在跑JAVA程序时,其实是运行在JVM(JAVA Virtual Machine)环境中,所有的JAVA程序首先被编译为.class类文件,这种类文件在虚拟机上执行,也就是说class文件并不与操作系统指令对应,而是经过虚拟机间接与操作系统交互。SQLite的虚拟机也是如此,编译SQL产生的指令流只有SQLite虚拟机(Virtual Database Engine,简称VDBE)能识别,由虚拟机与底层的存储(表,索引)交互,这种方式使得SQLite内部模块分工非常清晰,耦合度很低。如下图所示,我们可以看到VDBE的位置,它处于编译器与Btree模块的中间,是SQLite的核心,负责SQL到数据存取的交互。后面我提到的虚拟机都是指SQLite虚拟机(Virtual Machine,VM),VM模块将底层存储看作是记录维度的文件系统,通过执行指令流,来读写表上的记录。

                                      
VDBE数据结构和API

struct Vdbe{sqlite3 *db;   /* The database connection that owns this statement */Op *aOp;     /* Space to hold the virtual machine's program */int nOp;     /* Number of instructions in the program */Mem **apArg;  /* Arguments to currently executing user function */Parse *pParse; /* Parsing context used to create this Vdbe */int pc;     /* The program counter */Mem *aMem;   /* The memory locations */int nMem;    /* Number of memory locations currently allocated */Mem *aColName; /* Column names to return */u16 nResColumn; /* Number of columns in one row of the result set */char *zSql;   /* Text of the SQL statement that generated this */}

      我从源码中选取了比较重要的对象,主要包括数据库对象(db),指令流对象(aOp,nOp),绑定输入的参数值(apArg),解析SQL的对象(pParse),指令流计数器(pc),存储临时变量的寄存器(aMem,nMem),返回结果集集的列名和列信息(aColName,nResColumn)以及执行的产生虚拟机指令的SQL(zSql)等。这些基本就是虚拟机对象的全部,有指令,有寄存器,有指令计数器,与汇编语言非常相似,只不过VDBE里面的指令是sqlite内部识别的指令,而汇编语言指令是与机器指令对应的。如果想了解VDBE所有的对象,可以参考vdbeInt.h中关于该结构的定义,另外关于sqlite3结构和Parse结构可以参考sqliteInt.h文件。
     了解了Vdbe数据结构,我们再来看看我们平时常用的API是如何与VDBE交换数据的。通常我们要执行一个语句,会执行如下几个步骤。
1.调用sqlite3_prepare_*来编译生成指令流,返回一个sqlite3_stmt对象,其实这个对象就是vdbe对象。
2.调用sqlite3_bind_*来将参数传递给vdbe,
3.调用sqlite3_step进行执行,这时候会启动虚拟机执行一条条指令,直到遇到中断或者停止指令为止
4.调用sqlite3_column_*来获取上一步准备好的结果集
5.调用sqlite3_finalize,销毁vdbe对象,结束这次执行。
此外我们还可能用到sqlite3_reset接口,这个接口将指令流回退到第一条指令,用户可以调用sqlite3_step重新执行。有关API的详细说明,可以参考文件vdbeapi.c。 

虚拟机指令
      虚拟机核心就是扁平化指令,SQLite定义了一系列指令语言,每个指令做一小部分动作,虚拟机通过执行一些列指令达到查询,修改数据库的目的。每一条指令包含一个操作符和5个操作数,形式如下:<opcode,P1,P2,P3,P4,P5>。P1,P2,P3是一个32位有符号整数,P1一般是游标编号,P2一般是指令需要跳转的指令位置,P4是一个32位/64位整数,64位的浮点数,或者是指向字符串的指针,或者是二进制等,P5是一个无编号的字符。不是每条指令都使用了全部5个操作数,有的指令只需要2到3个操作数。后面一篇文章我会结合实例详细讲解指令的作用,以及对应操作数的含义。

虚拟机执行流程
      虚拟机的核心流程在sqlite3VdbeExec函数中,我们调用sqlite3_step时就会调用到该函数。由于这个函数比较大,大概有6000行代码,里面包含了每条指令的执行过程,为了方便说明,我会简化函数内容来说明这个函数的逻辑,抽象的代码如下。从代码流程来看,逻辑非常简单,通过循环遍历指令数组中的每条指令逐一执行,直到遇到中断或终止指令为止。如果需要逐条了解每条指令的含义,还需要仔细阅读代码。

sqlite3VdbeExec(Vdbe *p){  Op *aOp = p->aOp; /* Copy of p->aOp */  Op *pOp = aOp; /* Current operation */  for(pOp=&aOp[p->pc]; rc==SQLITE_OK; pOp++){    switch(pOp->opcode){    case OP_Goto: //jump to P2指向的指令    {      pOp = &aOp[pOp->p2 - 1];      break;    }    case OP_Integer: // value P1 is written into register P2.    {      pOut = out2Prerelease(p, pOp);      pOut->u.i = pOp->p1;      break;    }    case OP_Real:    {      ......      break;    }    case OP_Halt:    {      ......      break;    }    ...    }// end of switch } // end of for
}

小结
     本文介绍了SQLite虚拟机以及对应的指令流。通过介绍vdbe的存储结构,我们了解到vdbe对象所包含的内容;通过介绍API,我们了解到API与虚拟机的关系;通过介绍函数sqlite3VdbeExec的实现,我们知道虚拟机执行流程非常清晰,通过执行一系列指令流,就可以实现查询,更新数据。