Sql的执行过程

 上篇文章从整体上讲了一下sql波澜壮阔的历史,这一篇深入到sql底层的执行过程,看看sql怎么将晦涩难懂的机器代码和偏自然的查询语句结合起来的。

图片      

       整体上一条 SQL 语句的处理流程,是从网络上接收数据,SQL 协议解析和转换,SQL 语法解析,查询计划的制定和优化,查询计划执行,到最后返回结果。

       其中,SQL Parser 的功能是把 SQL 语句按照 SQL 语法规则进行解析,将文本转换成抽象语法树(AST),这部分功能需要些背景知识才能比较容易理解,我尝试做下相关知识的介绍,希望能对读懂这部分代码有点帮助。

     解析一般分为两个阶段:词法分析(Lexical Analysis)和语法分析(Syntactic Analysis)

      词法分析:将源代码分解为一系列词法单元,如标识符、关键字、运算符等。这可以被称为Token,Token 是一个数组,由一些代码语句的碎片组成,它们可以是数字、标签、标点符号、运算符或者其它任何东西。
    语法分析:将词法单元(Token)转化为语法树,检查代码是否符合语法规则。抽象语法树是一个嵌套程度很深的对象,用一种更容易处理的方式代表了代码本身,也能给我们更多信息。

要理解词法分析和语法分析的原理还需要补充一点知识就是:

有限状态机

有限状态机是一种计算模型,它可以接受一串输入并根据一组状态转移规则进行状态转移,最终输出一个结果。有限状态机可以分为两种类型:确定性有限状态机(DFA)和非确定性有限状态机(NFA)

确定性有限状态机(DFA)

DFA 是一种状态机,它的每个状态都有一条出边对应每个输入符号,而每个输入符号只能对应一条出边。在实际的编译器实现中,通常使用 DFA 进行词法分析。DFA 能够快速匹配输入的字符串,并且不需要回溯的操作,因此能够有效地提高编译器的词法分析效率。

非确定性有限状态机(NFA)

NFA 的每个状态都可以有多条出边对应同一个输入符号。在实际应用中,NFA 通常是通过转换为 DFA 来实现的。NFA 可以描述一些 DFA 不能描述的语言,但是其转换为 DFA 的过程可能需要消耗大量的时间和空间。

有限状态机的应用

有限状态机在编译原理中有着广泛的应用。在编译器的词法分析阶段,词法分析器将源代码作为输入,通过有限状态机匹配出其中的词法单元。此外,在其他领域,有限状态机也有一些应用,比如网络协议、自然语言处理、图像处理等等。

有限状态机的实现方式有很多种,比如使用编程语言直接实现、使用专门的工具生成等等。在实际应用中,我们需要根据具体的问题来选择最适合的实现方式和优化策略。

实现一个简易版本的分词:

let tokens = []; // 存储分词数据
let NUMBER = /[0-9]/; // 校验数字
let currentToken; // 存每一次的token
const Numeric = "Numeric"; // 数字类型
const Punctuator = "Punctuator"; // 运算符类型
/**
* 开始状态函数
* @param {*} char 传递的参数 1,0,+,2,0...
* @return 下一个状态函数
*/
function start(char) {
 if (NUMBER.test(char)) {
   currentToken = { type: Numeric, value: "" };
}
 return number(char);
}

function number(char) {
 if (NUMBER.test(char)) {
   // 如果传进来的时数字,就给value赋值
   currentToken.value += char;
   return number;
} else if (char === "+" || char === "-") {
   // 如果是运算符,就将当前的token传递出去,存到tokens中
   emit(currentToken);
   // 将运算符也存起来
   emit({ type: Punctuator, value: char });
   // 重新开始分词
   currentToken = { type: Numeric, value: "" };
   return number;
}
}

function emit(token) {
 // 重新计算currentToken
 currentToken = { type: "", value: "" };
 // 更新tokens
 tokens.push(token);
}

function tokenlizier(input) {
 // 开始是start的状态
 let state = start;
 for (let char of input) {
   // 保证每次state都更新
   state = state(char);
}
 if (currentToken.value.length > 0) {
   // 如果传递的长度大于1则再次开始
   emit(currentToken);
}
}

tokenlizier("10+20-20");

console.log(tokens);

词法分析

词法分析器通常是编译器的第一个组件,它会将源代码分解为一系列词法单元,然后将这些词法单元传递给语法分析器进行语法分析。词法分析器的设计和实现对编译器的性能和正确性都有重要的影响。

词法分析是将源代码分解为一系列词法单元的过程。

  • 词法单元包括标识符关键字运算符等。词法分析器会读取源代码的每一个字符根据预定义的规则将它们组成一系列词法单元

  • 词法分析器通常使用有限状态机来实现。

语法分析

语法分析是将词法单元转化为语法树的过程,检查代码是否符合语法规则。语法分析器通常使用自上而下或自下而上的方法来构建语法树。自上而下的方法通常是基于上下文无关文法(Context-Free Grammar,CFG)的,而自下而上的方法通常是基于移进-归约(Shift-Reduce)操作的。

语法分析器通常会在词法分析器之后执行,并将词法分析器产生的词法单元作为输入。语法分析器将词法单元转化为语法树,并检查代码是否符合语法规则。语法树是编译器的一个重要数据结构,它将代码的语法结构以树的形式表示出来。

自上而下语法分析

自上而下语法分析器从语法的高层次开始分析,即从语法树的根节点开始。自上而下分析器基于一组上下文无关文法(Context-Free Grammar CFG),通过递归下降的方式构建语法树。这种方法通常比自下而上的方法更容易理解和实现,因为自上而下的方法直接反映了语法规则的结构。

CFG 中有四个基本元素:终结符号、非终结符号、产生式和开始符号。终结符号是 CFG 中的最基本元素,它表示语言中的一个基本单元,如数字、标识符、运算符等。非终结符号表示语言中的一个复合单元,它可以由一个或多个终结符号或其他非终结符号组成。产生式描述如何将一个非终结符号替换为一个符号序列,这个符号序列可以由终结符号或非终结符号组成。开始符号是 CFG 中的一个特殊非终结符号,它表示 CFG 的起始符号。

自上而下语法分析器通常使用的方法有:

递归下降分析器(Recursive Descent Parser):这种方法使用递归函数来构建语法树。每个函数对应文法中的一个非终结符号,递归下降分析器从语法树的根节点开始,逐步向下扩展直到叶子节点。预测分析器(Predictive Parser):这种方法使用一个预测表来决定下一个要匹配的符号。预测表是由文法的非终结符号和终结符号组成的,每个表项包含一个终结符号或一个产生式。

终结符

在编译原理中,终结符是指不能再被分解的符号,也即是语法树中的叶子节点。在具体的代码中,终结符可以是变量名、数字、运算符等符号,具体取决于编程语言的语法规则。例如,在以下的文法中:

expr -> term {('+'|'-') term}
term -> factor {('*'|'/') factor}
factor -> '(' expr ')' | number
number -> '0' | '1' | ... | '9'

其中,+-*/()012...9 等符号都是终结符。

在实际应用中,我们需要根据具体的编程语言来定义终结符,并在语法分析器中使用这些终结符进行解析。通过终结符,我们可以识别并处理源代码中的关键符号,从而将其转换为语法树中的叶子节点。

非终结符

在编译原理中,非终结符是指可以被分解为其他符号的符号,也即是语法树中的非叶子节点。在具体的代码中,非终结符可以是表达式语句函数等,具体取决于编程语言的语法规则。例如,在以下的文法中:

function -> 'function' identifier '(' parameter_list ')' '{' statement_list '}'
parameter_list -> identifier ( ',' identifier )*
statement_list -> statement*
statement -> 'if' '(' expression ')' '{' statement_list '}' 'else' '{' statement_list '}' | ...

其中,function、identifier、(、)、,、{、}、if、else 等符号都是非终结符。

在实际应用中,我们需要根据具体的编程语言来定义非终结符,并在语法分析器中使用这些非终结符进行解析。通过非终结符,我们可以识别并处理源代码中的复杂语法结构,从而将其转换为语法树中的非叶子节点。

自下而上语法分析

自下而上语法分析器从语法的低层次开始分析,即从语法树的叶子节点开始。自下而上分析器基于一组产生式,通过移进和归约的方式构建语法树。这种方法通常比自上而下的方法更高效,因为自下而上的方法只需要查看输入的符号,而不需要将所有的非终结符号展开为文法树。

自下而上语法分析器通常使用的方法有:

移进-归约分析器(Shift-Reduce Parser):这种方法使用两个栈来维护语法树的状态。分析器从输入中读取一个符号,然后根据当前状态和下一个符号的组合执行移进或归约操作。移进操作将符号推入状态栈中,而归约操作将栈中的符号转换为一个非终结符号。我们可以使用移进-归约操作来实现语法分析器。具体来说,我们可以定义一个栈来保存符号,然后从左到右扫描源代码,并按照以下规则进行移进和归约操作:

如果栈顶是终结符,将源代码中的终结符压入栈中。如果栈顶是非终结符,根据语法规则进行归约操作。如果当前符号和栈顶符号不匹配,进行错误处理。

例如,在处理表达式 1 + 2 * 3 时,我们可以按照以下步骤进行移进-归约操作:

1. 将 1 移进栈中。
2. 将 + 移进栈中。
3. 将 2 移进栈中。
4. 将 * 移进栈中。
5. 将 3 移进栈中。
6. 根据语法规则进行归约操作,得到一个新的表达式。
7. 根据语法规则进行归约操作,得到最终的表达式。

通过移进-归约操作,我们可以快速、准确地解析源代码,并将其转换为语法树。在实际应用中,我们需要根据具体的编程语言来定义文法,并使用移进-归约操作来实现语法分析器。通过语法分析器,我们可以检查源代码是否符合语法规则,并提供有用的错误信息和建议。

以下是一个使用移进-归约操作解析算数表达式的例子:

const expr = 'term ( [+-] term )*';
const term = 'factor ( [*/] factor )*';
const factor = '( expr ) | number';
const number = 'digit+';

在该例子中,我们定义了一个算数表达式的文法,包括加、减、乘、除和括号等运算符。通过该文法,我们可以使用移进-归约操作解析符合该文法的算数表达式,并将其转换为语法树。

具体来说,我们可以按照以下步骤进行移进-归约操作:

1. 将 `term` 移进栈中。
2. 将 `(` 移进栈中。
3. 将 `expr` 移进栈中。
4. 将 `)` 移进栈中。
5. 根据语法规则进行归约操作,得到一个新的表达式。
6. 将 `term` 移进栈中。
7. 将 `` 移进栈中。
8. 将 `factor` 移进栈中。
9. 将 `number` 移进栈中。
10. 根据语法规则进行归约操作,得到一个新的表达式。
11. 根据语法规则进行归约操作,得到最终的表达式。

LR 分析器(LR Parser):这种方法使用一个 LR 分析表来决定下一个要执行的操作。LR 分析表是由状态和终结符号组成的,每个表项包含一个动作或一个状态转移。

Lex & Yacc 介绍

       Lex & Yacc 是用来生成词法分析器和语法分析器的工具,它们的出现简化了编译器的编写。Lex & Yacc 分别是由贝尔实验室的 Mike Lesk 和 Stephen C. Johnson 在 1975 年发布。对于 Java 程序员来说,更熟悉的是 ANTLR,ANTLR 4 提供了 Listener+Visitor 组合接口, 不需要在语法定义中嵌入 actions,使应用代码和语法定义解耦。Spark 的 SQL 解析就是使用了 ANTLR。Lex & Yacc 相对显得有些古老,实现的不是那么优雅,不过我们也不需要非常深入的学习,只要能看懂语法定义文件,了解生成的解析器是如何工作的就够了。我们可以从一个简单的例子开始:

   

图片

      上图描述了使用 Lex & Yacc 构建编译器的流程。Lex 根据用户定义的 patterns 生成词法分析器。词法分析器读取源代码,根据 patterns 将源代码转换成 tokens 输出。Yacc 根据用户定义的语法规则生成语法分析器。语法分析器以词法分析器输出的 tokens 作为输入,根据语法规则创建出语法树。最后对语法树遍历生成输出结果,结果可以是产生机器代码,或者是边遍历 AST 边解释执行。

      从上面的流程可以看出,用户需要分别为 Lex 提供 patterns 的定义,为 Yacc 提供语法规则文件,Lex & Yacc 根据用户提供的输入文件,生成符合他们需求的词法分析器和语法分析器。这两种配置都是文本文件,并且结构相同:

图片

      文件内容由 %% 分割成三部分,我们重点关注中间规则定义部分。对于上面的例子,Lex 的输入文件如下:

图片

       上面只列出了规则定义部分,可以看出该规则使用正则表达式定义了变量、整数和操作符等几种 token。例如整数 token 的定义如下:

图片

       当输入字符串匹配这个正则表达式,大括号内的动作会被执行:将整数值存储在变量 yylval 中,并返回 token 类型 INTEGER 给 Yacc。

      再来看看 Yacc 语法规则定义文件:

图片

        第一部分定义了 token 类型和运算符的结合性。四种运算符都是左结合,同一行的运算符优先级相同,不同行的运算符,后定义的行具有更高的优先级。

       语法规则使用了 BNF 定义。BNF 可以用来表达上下文无关(context-free)语言,它具有语法简单,表示明确,便于语法分析和编译的特点。大部分的现代编程语言都可以使用 BNF 表示。

BNF表示语法规则的方式为:

  • 非终结符用尖括号括起。

  • 每条规则的左部是一个非终结符,右部是由非终结符和终结符组成的一个符号串,中间一般以::=分开。

  • 具有相同左部的规则可以共用一个左部,各右部之间以直竖“|”隔开。

BNF中常用的元字符及其表示的意义如下:

在双引号中的字 "word" 代表着这些字符本身。而double_quote用来代表双引号;
在双引号外的字(有可能有下划线)代表着语法部分;
尖括号 < > 内包含的为必选项;
方括号 [ ] 内包含的为可选项;
大括号 { } 内包含的为可重复0至无数次的项;
圆括号 ( ) 内包含的所有项为一组,用来控制表达式的优先级;
竖线 | 表示在其左右两边任选一项,相当于"OR"的意思;
::= 是“被定义为”的意思;
... 表示术语符号;
斜体字: 参数,在其它地方有解释;

图片

      Yacc 语法规则定义产生了三个产生式。产生式冒号左边的项(例如 statement)被称为非终结符, INTEGER 和 VARIABLE 被称为终结符,它们是由 Lex 返回的 token 。终结符只能出现在产生式的右侧。可以使用产生式定义的语法生成表达式:

图片

       表达式一般是人为定义的,那么解析表达式就是具体sql规则的体现了,解析表达式本质上是生成表达式的逆向操作,我们需要归约表达式到一个非终结符。Yacc 生成的语法分析器使用自底向上的归约(shift-reduce)方式进行语法解析,同时使用堆栈保存中间状态。还是看例子,表达式 x + y * z 的解析过程:

图片

   点(.)表示当前的读取位置,随着 . 从左向右移动,我们将读取的 token 压入堆栈,当发现堆栈中的内容匹配了某个产生式的右侧,则将匹配的项从堆栈中弹出,将该产生式左侧的非终结符压入堆栈。这个过程持续进行,直到读取完所有的 tokens,并且只有启始非终结符(本例为 program)保留在堆栈中。

产生式右侧的大括号中定义了该规则关联的动作,例如:

图片

    我们将堆栈中匹配该产生式右侧的项替换为产生式左侧的非终结符,本例中我们弹出 expr '*' expr,然后把 expr 压回堆栈。我们可以使用 $position 的形式访问堆栈中的项,$1 引用的是第一项,$2 引用的是第二项,以此类推。$$ 代表的是归约操作执行后的堆栈顶。本例的动作是将三项从堆栈中弹出,两个表达式相加,结果再压回堆栈顶。

上面例子中语法规则关联的动作,在完成语法解析的同时,也完成了表达式求值。一般我们希望语法解析的结果是一棵抽象语法树(AST),可以这么定义语法规则关联的动作:

图片

    上面是一个语法规则定义的片段,我们可以看到,每个规则关联的动作不再是求值,而是调用相应的函数,该函数会返回抽象语法树的节点类型 nodeType,然后将这个节点压回堆栈,解析完成时,我们就得到了一颗由 nodeType 构成的抽象语法树。对这个语法树进行遍历访问,可以生成机器代码,也可以解释执行。

至此,我们大致了解了 Lex & Yacc 的原理。其实还有非常多的细节,例如如何消除语法的歧义,但我们的目的是读懂,掌握这些概念已经够用了。

至此,用一个更简洁的图来概况本文。

图片

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值