SQLite的词法分析器的代码逻辑其实很简单,但是其中也不乏SQLite对于执行效率的优化。甚至可以说SQLite在追求高效的每一个细节点,都无所不用其极。文章从源码的角度分别看具体的实现逻辑与优化的细节
关于前端的大致结构
Tokenize是SQLite的最前端,它的职责就是,将用户输入的一个SQL字符串,分割为一个一个独立的词。并且以特征ID的形式一个一个输入给Parser,而不是一个一个单词。因为parser只认识这些特征ID。
先说一下Parser
Parser是SQLite开发的一个语法分析器。在$root/tool/目录下有一个lemon.c的工具。SQLite是通过cmake来配置源码的编译,在autoconf配置下产生的Makefile中有几个关于这个的指令:
1 | # 编译lemon.c,生成lemon可执行文件 |
1 | # parse.h 和 parse.c文件都依赖于lemon可执行文件 |
1 | # fts5也是使用的lemon程序,通过fts5parse.y的定义来生成解析器 |
Parser的核心代码都是使用lemon这个工具生成的,通过不断地输入在parse.h中定义的特征ID来生成一个语法树。
那特征ID哪来呢?就是上面已经提到的Tokenize。
在SQLite API中的int sqlite3_prepare();int sqlite3_prepare_v2();int sqlite3_prepare_v3();
三个SQL预处理方法都是通过先调用tokenize,然后调用parser,通过vm的编译生成字节码
大致的函数调用时序:

词法分析器源码
tokenize.c的源码结构非常简单。只有两个核心函数 int sqlite3GetToken(const unsigned char *z, int *tokenType) 与 int sqlite3RunParser(Parse *pParse, const char *zSql, char **pzErrMsg)
prepare函数会直接调用sqlite3RunParser函数,这个函数虽然很长,但是把核心代码提取出来后,就一目了然。
1 | int sqlite3RunParser(Parse *pParse, const char *zSql, char **pzErrMsg){ |
这样似乎看起来还是很多,再精简一下。
1 | while( 1 ){ |
只有两个步骤,取词,调用parser,然后不断地循环,一直到全部读完为止。这也印证了上面的话。
从SQL语句中取词无非也就是根据分割符,不断的裁剪分割罢了。但是开头提到,SQLite为了追求效率,做了很多优化。
后面详细说一下优化的细节。
词法分析器效率的优化
sqlite3RunParser函数主要是流程化的代码,主要优化都在sqlite3GetToken函数内,看下源码。
1 | /* |
从代码可以看到,sqlite3GetToken输入一个SQL字符串,然后根据这个串的第一个字符判断是什么类型(CC_SPACE,CC_NUL,…)等等,用一个巨大的switch-case来处理不同的词,因为这个函数需要依次返回各种词的类型(tokenType),也就是上文提到的在Parser.h中定义的类型。
但是…aiClass[*z]这个是干什么用的?为什么不直接使用switch(*z)?
这里就不得不讲到 Switch-Case 的编译实现了。
switch-case在使用的时候编译器会根据case中的constant值来决定使用哪种方式做检索。具体了解的话可以看这篇文章关于C/C++ switch语句你也许不知道的一些事
- 编译器使用二级表的方式,将case下的代码块与一个维表索引形成映射关系,以及将case的值与这个一维表再形成索引关系。
- 编译器使用一级表的方式,将case下的代码块直接和case的值形成映射关系。
- 编译器使用二分查找的方式,通过O(logn)的时间复杂度来查找。
根据case下不同的值,或者值不同的大小,编译器会选择不同的实现方式。前两种是O(1)的时间复杂度,第三种的时间复杂度是O(logn)。SQLite为了保证它能够在能高的效率下执行,也做了一次类似的”二级映射”。
只考虑ASCII码的情况:
ASCII码可以使用7bit或者8bit来表示128或者256种字符,后128个是扩展ASCII。SQLite对这256个ASCII做了一个一维映射。构建了一个256个元素的一维表,而这个表就是上面看到的aiClass表。表内的值,就代表了这个ASCII码对应的符号的类型。SQLite将所有符号的类型归纳为29种,可以在tokenize.c的前半部分看到所有的29个宏定义。之所以这样做,就是为了能够在switch-case执行的时候让编译器可以在O(1)的时间复杂度内完成索引与比较。
下面列出了ASCII码对应的 aiClass 表 以及对应的类型。
1 | /* Character classes for tokenizing |
在case CC_KYWD的情况下,获取到一个完整的单词之后,通过调用keywordCode函数,还要再次判断,这个单词是否是SQLite的关键字,并且返回具体的关键字的特征ID。关于keywordCode函数的原理和优化,可以看后续的SQLite–KeywordHash