查询处理及优化是关系数据库得以流行的根本原因,也是关系数据库系统最核心的技术之一。sqlite的查询处理模块非常的精致,而且很容易移植到不支持sql的存储引擎,Berkeley DB最新的版本已经将其完整的移植过来。本文将简要的讨论一下sqlite的查询处理及优化。
查询处理一般来说,包括词法分析、语法分析、语义分析、生成执行计划以及计划的执行几个部分。sqlite的词法分析器是手工写的,语法分析器由Lemon生成,语义分析主要的进行语义方面的一些检查,比如table是否存在等。而执行计划的生成及执行是最核心的两部分,也是相对比较复杂、有点技术含量的东西。sqlite的执行计划采用了虚拟机的思想,实际上,这种基于虚拟机的思想并非sqlite所独有,但是,sqlite将其发挥到了极致,它生成的执行计划非常详细,而且很容易读(在这里,我不得不佩服D. Richard Hipp在编译理论方面的功底)。
1、语法分析——语法树
词法分析本身比较简单,这里就不谈了。语法分析的主要任务就是对用户输入的sql语句进行语法检查,然后生成一个包含所有信息的语法树。对于SELECT语句,这个语法树最终由结构体Select表示:
ExprList * pEList; /* Thefieldsoftheresult */
u8op; Oneof:TK_UNIONTK_ALLTK_INTERSECTTK_EXCEPT */
char affinity; MakeRecordwiththisaffinityforSRT_Set */
u16selFlags; VarIoUsSF_*values */
SrcList * pSrc; TheFROMclause */
Expr * pWhere; TheWHEREclause */
ExprList * pGroupBy; TheGROUPBYclause */
Expr * pHaving; TheHAVINGclause */
ExprList * pOrderBy; TheORDERBYclause */
Select * pPrior; Priorselectinacompoundselectstatement */
Select * pNext; Nextselecttotheleftinacompound */
Select * pRightmost; Right-mostselectinacompoundselectstatement */
Expr * pLimit; LIMITexpression.NULLmeansnotused. */
Expr * pOffset; OFFSETexpression.NULLmeansnotused. int iLimit,iOffset; MemoryregistersholdingLIMIT&OFFSETcounters int addrOpenEphm[ 3 ]; OP_OpenEphemopcodesrelatedtothisselect */
};
该结构体比较简单,但要注意几个字段。pEList输出结果列的语法树;pSrc为FROM子句语法树;pWhere为WHERE部分的语法树。
select语法分析在最终在sqlite3SelectNew中完成:
Parse * pParse, Parsingcontext */
ExprList * pEList,0)">whichcolumnstoincludeintheresult */
SrcList * pSrc,0)">theFROMclause--whichtablestoscan */
Expr * pWhere,0)">theWHEREclause */
ExprList * pGroupBy,0)">theGROUPBYclause */
Expr * pHaving,0)">theHAVINGclause */
ExprList * pOrderBy,0)">theORDERBYclause int isDistinct,0)">trueiftheDISTINCTkeywordispresent */
Expr * pLimit,0)">LIMITvalue.NULLmeansnotused */
Expr * pOffset OFFSETvalue.NULLmeansnooffset */
){
Select * pNew;
Selectstandin;
sqlite3 * db = pParse -> db;
pNew = sqlite3DbMallocZero(db,255)">sizeof ( * pNew));
assert(db -> mallocFailed || ! pOffset || pLimit); OFFSETimpliesLIMIT if (pNew == 0 ){
pNew = & standin;
memset(pNew, 0 ,255)">sizeof ( * pNew));
}
if (pEList == 0 ){
pEList = sqlite3ExprListAppend(pParse,sqlite3Expr(db,TK_ALL,128)">0 ));
}
pNew -> pEList = pEList;
pNew -> pSrc = pSrc;
pNew -> pWhere = pWhere;
pNew -> pGroupBy = pGroupBy;
pNew -> pHaving = pHaving;
pNew -> pOrderBy = pOrderBy;
pNew -> selFlags = isDistinct ? SF_Distinct: 0 ;
pNew -> op = TK_SELECT;
pNew -> pLimit = pLimit;
pNew -> pOffset = pOffset;
assert(pOffset == 0 || pLimit != 0 );
pNew -> addrOpenEphm[ 0 ] = - 1 ;
pNew -> addrOpenEphm[ 1 ] = - 2 ] = - 1 ;
if (db -> mallocFailed){
clearSelect(db,pNew);
if (pNew !=& standin)sqlite3DbFree(db,pNew);
pNew = 0 ;
}
return pNew;
}
它主要就是将之前得到的各个子语法树汇总到Select结构体,并根据该结构,进行接下来语义分析及生成执行计划等工作。
来看个例子,这个例子贯穿于全文:
2 0 |Trace| 0 | 0 || 00 |
3 1 |Goto| 35 | 4 //////////////////////////( 1 )////////////////////////////
5 2 |OpenRead| 3 | 2 | 00 |students#打开students表
6 3 |OpenRead| 1 | 7 | 00 |sc#打开sc表
7 4 |OpenRead| 8 | 0 |keyinfo( 2 ,BINARY,BINARY)| 00 |sqlite_autoindex_sc_1#sc的索引
8 5 |OpenRead| 5 | 00 |course#course表
9 6 |OpenRead| 4 | 6 | 1 ,128)">00 |sqlite_autoindex_course_1#course的索引
10 //////////////////////////( 2 )//////////////////////////////
11 7 |Rewind| 29 | 00 |#将游标p0定位到students表的第一条记录
12 8 |Column| 1 || 00 |students.sid#取出第0列,写到r1
13 9 |IsNull| 28 | 14 10 |Affinity| 0 |d| 15 11 |SeekGe| 00 |#将游标p3定位到sc索引的>=r1的记录处
16 12 |IdxGE| 01 |
17 13 |IdxRowid| 18 14 |Seek| 19 15 |Column| 3 || 00 |sc.cid#读取sc.cid到r3
20 16 |IsNull| 27 | 21 17 |Affinity| 22 18 |SeekGe| 00 |#将游标p4定位到course索引的>=r3的记录处
23 19 |IdxGE| 24 20 |IdxRowid| 25 21 |Seek| 26 ///////////////////////////( 3 )//////////////////////////////
27 22 |Column| 5 || 00 |students.sname#从游标p0取出第1列(sname)
28 23 |Column| 6 || 00 |course.cname#从游标p2取出第1列(cname)
29 24 |Column| 7 || 00 |sc.grade#从游标p1取出第2列(grade)
30 25 |ResultRow| 31 ///////////////////////////( 4 )///////////////////////////////
32 26 |Next| 19 | 33 27 |Next| 12 | 34 28 |Next| 35 29 |Close| 36 30 |Close| 37 31 |Close| 38 32 |Close| 39 33 |Close| 40 //////////////////////////( 5 )//////////////////////////////////
41 34 |Halt| 42 35 |Transaction| 43 36 |VerifyCookie| 44 37 |TableLock| 0 |students| 45 38 |TableLock| 0 |sc| 46 39 |TableLock| 0 |course| 47 40 |Goto| 48
FROM部分:
第一个表项:
表名zName =”stduents”,zAlias=”s”,jointype = 0。
第二个表项:
注意,jointype = 1(JT_INNER)。
第三个表项:
注意,jointype = 1(JT_INNER)。
WHERE部分(结点类型为Expr的一棵二叉树):
2、生成执行计划(语法树到OPCODE)
Select的执行计划在sqlite3Select中完成:
Parse * pParse, /* Theparsercontext */
Select * p,0)">SELECT语法树 */
SelectDest * pDest 如果处理结果集 */
)
其实,该函数先对sql语句进行语义分析,然后再进行优化,最后生成执行计划。
对于上面要sql语句,生成的执行计划(虚拟机opcode)大致分成5部分,前4部分都在sqlite3Select()中生成,它主要调用了以下几个函数:
其中(1)、(2)在sqlite3WhereBegin()中生成,(2)即所谓的查询优化处理;(3)在 selectInnerLoop中生成;(4)在sqlite3WhereEnd中生成;(5)是sqlite3FinishCoding中完成的。后续章节,我将分别分析每一部分。
3、sqlite3WhereBegin
该函数是查询处理最为核心的函数,它主要完成where部分的优化及相关opcode的生成。
pTabList是由分析器对FROM部分生成的语法树,它包含FROM中表的信息;pWhere是WHERE部分的语法树,它包含WHERE中所有表达式的信息;ppOrderBy 对应ORDER BY子句(暂不考虑)。
sqlite的查询优化做得简单又精致。在一个简单的sqlite3WhereBegin函数中,完成所有的优化处理。查询优化的基本理念就是嵌套循环(nested loop),select语句的FROM子句的每个表对应一层循环(INSERT和UPDATE对应只有一个表SELECT语句)。例如,
SELECT * FROM t1,t2,t3 WHERE ...;
进行如下操作:
而对于每一层的优化,基本的理念就是对于该层循环的表,分析WHERE子句中是否有表达式能够使用其索引。
sqlite有三种基本的扫描策略:
(1)全表扫描,这种情况通常出现在没有WHERE子句时;
(2)基于索引扫描,这种情况通常出现在表有索引,而且WHERE中的表达式又能够使用该索引的情况;
(3)基本rowid的扫描,这种情况通常出现在WHERE表达式中含有rowid的条件。该情况实际上也是对表进行的扫描。可以说,sqlite以rowid为聚簇索引。
第一种情况比较简单,第三种情况与第二种情况没有什么本质的差别。所以,这里只对第二种情况进行详细讨论。
先来看看sqlite3WhereBegin的代码(去掉了一些无关紧要的代码):
* 3 exprAnalyzeAll(pTabList,pWC);
4
5 WHERETRACE(( " ***OptimizerStart***\n " ));
6 // 优化开始
7 for (i = iFrom = = pWInfo -> a;i < nTabList;i ++ ,pLevel ++ ){
8 WhereCostbestPlan; Mostefficientplanseensofar 9 Index * pIdx; IndexforFROMtableatpTabItem 10 int j; ForloopingoverFROMtables 11 int bestJ = - 1 ; Thevalueofj 12 Bitmaskm; BitmaskvalueforjorbestJ 13 int isOptimal; Iteratorforoptimal/non-optimalsearch 14
15 memset( & bestPlan,255)">sizeof (bestPlan));
16 bestPlan.rCost = sqlITE_BIG_DBL;
17
18 进行两次扫描: 19 如果第一次扫描没有找到优化的扫描策略,此时,isOptimal==0,bestJ==-1,则进行第二次扫描 20 for (isOptimal = 1 ;isOptimal >= 0 && bestJ < 0 ;isOptimal -- ){
21 第一次扫描的mask==0,表示所有表都已经准备好 22 Bitmaskmask = (isOptimal ? 0 :notReady);
23 assert((nTabList - iFrom) > 1 || isOptimal);
24
25 for (j = iFrom,pTabItem =& pTabList -> a[j];j < nTabList;j ++ ,pTabItem ++ ){
26 int doNotReorder; Trueifthistableshouldnotbereordered 27 WhereCostsCost; Costinformationfrombest[Virtual]Index() 28 ExprList * pOrderBy; ORDERBYclauseforindextooptimize 29
30 对于左连接和交叉连接,不能改变嵌套的顺序 31 doNotReorder = (pTabItem -> jointype & (JT_LEFT | JT_CROSS)) != 0 ;
32
33 if (j != iFrom && doNotReorder) 如果j==iFrom,仍要进行优化处理(此时,是第一次处理iFrom项) 34 break ;
35 m = getMask(pMaskSet,pTabItem -> iCursor);
36 if ((m & notReady) == 0 ){ 如果该pTabItem已经进行处理,则不需要再处理 37 if (j == iFrom)
38 iFrom ++ ;
39 continue ;
40 }
41 pOrderBy = ((i == 0 && ppOrderBy) ?* ppOrderBy: 0 );
42
43 {
44 对一个表(pTabItem),找到它的可用于本次查询的最好的索引,sCost返回对应的代价 45 bestBtreeIndex(pParse,pWC,pTabItem,mask,pOrderBy, & sCost);
46 }
47 if ((sCost.used & notReady) == 0
48 && (j == iFrom || sCost.rCost < bestPlan.rCost)
49 ){
50 bestPlan = sCost;
51 bestJ = j; 如果bestJ>=0,表示找到了优化的扫描策略 52 }
53 if (doNotReorder) 54 } endfor 55 } 56 WHERETRACE(( ***Optimizerselectstable%dforloop%d\n " ,bestJ,
57 pLevel - pWInfo -> a));
58
59 if ((bestPlan.plan.wsFlags & WHERE_ORDERBY) != 不需要进行排序操作 60 * ppOrderBy = 61 }
62 设置该层选用的查询策略 63 andFlags &= bestPlan.plan.wsFlags;
64 pLevel -> plan = bestPlan.plan;
65
66 如果可以使用索引,则设置索引对应的游标的下标 67 if (bestPlan.plan.wsFlags & WHERE_INDEXED){
68 pLevel -> iIdxCur = pParse -> nTab ++ ;
69 } else {
70 pLevel -> iIdxCur = - 71 }
72 notReady &= ~ getMask(pMaskSet,pTabList -> a[bestJ].iCursor);
73 该层对应的FROM的表项,即该层循环是对哪个表进行的操作. 74 pLevel -> iFrom = (u8)bestJ;
75
76 }
77 优化结束 78 ***OptimizerFinished***\n 79