揭晓:一条SQL语句的执行过程是怎么样的?

news2025/1/11 14:19:14

 

数据库系统能够接受 SQL 语句,并返回数据查询的结果,或者对数据库中的数据进行修改,可以说几乎每个程序员都使用过它。

而 MySQL 又是目前使用最广泛的数据库。所以,解析一下 MySQL 编译并执行 SQL 语句的过程,一方面能帮助你加深对数据库领域的编译技术的理解;另一方面,由于 SQL 是一种最成功的 DSL(特定领域语言),所以理解了 MySQL 编译器的内部运作机制,也能加深你对所有使用数据操作类 DSL 的理解,比如文档数据库的查询语言。另外,解读 SQL与它的运行时的关系,也有助于你在自己的领域成功地使用 DSL 技术。

那么,数据库系统是如何使用编译技术的呢

本文先带你了解一下如何跟踪 MySQL 的运行,了解它处理一个 SQL 语句的过程,以及 MySQL 在词法分析和语法分析方面的实现机制。

一、编译并调试MySQL

按照惯例,你要下载MySQL 的源代码。我下载的是 8.0 版本的分支。

源代码里的主要目录及其作用如下,我们需要分析的代码基本都在 sql 目录下,它包含了编译器和服务端的核心组件。

 

图 1:MySQL 的源代码包含的主要目录

MySQL 的源代码主要是.cc 结尾的,也就是说,MySQL 主要是用 C++ 编写的。另外,也有少量几个代码文件是用 C 语言编写的。

为了跟踪 MySQL 的执行过程,你要用 Debug 模式编译 MySQL。

如果你用单线程编译,大约需要 1 个小时。编译好以后,先初始化出一个数据库来:

./mysqld --initialize --user=mysql

这个过程会为 root@localhost 用户,生成一个缺省的密码。

接着,运行 MySQL 服务器:

./mysqld &

之后,通过客户端连接数据库服务器,这时我们就可以执行 SQL 了:

./mysql -uroot -p #连接mysql server

最后,我们把 GDB 调试工具附加到 mysqld 进程上,就可以对它进行调试了。

gdb -p pidof mysqld #pidof是一个工具,用于获取进程的id,你可以安装一下

提示:文本是采用了一个 CentOS 8 的虚拟机来编译和调试 MySQL。我也试过在macOS 下编译,并用 LLDB 进行调试,也一样方便。

注意,你在调试程序的时候,有两个设置断点的好地方:

  • dispatch_command:在 sql/sql_parse.cc 文件里。在接受客户端请求的时候(比如一个 SQL 语句),会在这里集中处理。
  • my_message_sql:在 sql/mysqld.cc 文件里。当系统需要输出错误信息的时候,会在这里集中处理。

这个时候,我们在 MySQL 的客户端输入一个查询命令,就可以从雇员表里查询姓和名了。在这个例子中,我采用的数据库是 MySQL 的一个示例数据库 employees,你可以根据它的文档来生成示例数据库。

mysql> select first_name, last_name from employees; #从mysql库的user表中查询信

这个命令被 mysqld 接收到以后,就会触发断点,并停止执行。这个时候,客户端也会老老实实地停在那里,等候从服务端传回数据。即使你在后端跟踪代码的过程会花很长的时间,客户端也不会超时,一直在安静地等待。给我的感觉就是,MySQL 对于调试程序还是很友好的。

在 GDB 中输入 bt 命令,会打印出调用栈,这样你就能了解一个 SQL 语句,在 MySQL中执行的完整过程。为了方便你理解和复习,这里我整理成了一个表格:

 

我也把 MySQL 执行 SQL 语句时的一些重要程序入口记录了下来,这也需要你重点关注。它反映了执行 SQL 过程中的一些重要的处理阶段,包括语法分析、处理上下文、引用消解、优化和执行。你在这些地方都可以设置断点。

 

图 2:MySQL 执行 SQL 语句时的部分重要程序入口

好了,现在你就已经做好准备,能够分析 MySQL 的内部实现机制了。不过,由于 MySQL执行的是 SQL 语言,它跟我们前面分析的高级语言有所不同。所以,我们先稍微回顾一下SQL 语言的特点。

二、SQL语言:数据库领域的DSL

SQL 是结构化查询语言(Structural Query Language)的英文缩写。举个例子,这是一个很简单的 SQL 语句:

select emp_no, first_name, last_name from employees;

其实在大部分情况下,SQL 都是这样一个一个来做语句执行的。这些语句又分为 DML(数据操纵语言)和 DDL(数据定义语言)两类。前者是对数据的查询、修改和删除等操作,而后者是用来定义数据库和表的结构(又叫模式)。

我们平常最多使用的是 DML。而 DML 中,执行起来最复杂的是 select 语句。所以,本文都是用 select 语句来给你举例子。

那么,SQL 跟我们前面分析的高级语言相比有什么不同呢?

第一个特点:SQL 是声明式(Declarative)的。这是什么意思呢?其实就是说,SQL 语句能够表达它的计算逻辑,但它不需要描述控制流。

高级语言一般都有控制流,也就是详细规定了实现一个功能的流程:先调用什么功能,再调用什么功能,比如 if 语句、循环语句等等。这种方式叫做命令式(imperative)编程

更深入一点,声明式编程说的是“要什么”,它不关心实现的过程;而命令式编程强调的是“如何做”。前者更接近人类社会的领域问题,而后者更接近计算机实现。

第二个特点:SQL 是一种特定领域语言(DSL,Domain Specific Language),专门针对关系数据库这个领域的。SQL 中的各个元素能够映射成关系代数中的操作术语,比如选择、投影、连接、笛卡尔积、交集、并集等操作。它采用的是表、字段、连接等要素,而不需要使用常见的高级语言的变量、类、函数等要素。

所以,SQL 就给其他 DSL 的设计提供了一个很好的参考:

  • 采用声明式,更加贴近领域需求。比如,你可以设计一个报表的 DSL,这个 DSL 只需要描述报表的特征,而不需要描述其实现过程。
  • 采用特定领域的模型、术语,甚至是数学理论。比如,针对人工智能领域,你完全就可以用张量计算(力学概念)的术语来定义 DSL。

好了,现在我们分析了 SQL 的特点,从而也让你了解了 DSL 的一些共性特点。那么接下来,顺着 MySQL 运行的脉络,我们先来了解一下 MySQL 是如何做词法分析和语法分析的。

三、词法和语法分析

词法分析的代码是在 sql/sql_lex.cc 中,入口是 MYSQLlex() 函数。在 sql/lex.h 中,有一个 symbols[]数组,它定义了各类关键字、操作符。

MySQL 的词法分析器也是手写的,这给算法提供了一定的灵活性。比如,SQL 语句中,Token 的解析是跟当前使用的字符集有关的。使用不同的字符集,词法分析器所占用的字节数是不一样的,判断合法字符的依据也是不同的。而字符集信息,取决于当前的系统的配置。词法分析器可以根据这些配置信息,正确地解析标识符和字符串。

MySQL 的语法分析器是用 bison 工具生成的,bison 是一个语法分析器生成工具,它是GNU 版本的 yacc。bison 支持的语法分析算法是 LALR 算法,而 LALR 是 LR 算法家族中的一员,它能够支持大部分常见的语法规则。bison 的规则文件是 sql/sql_yacc.yy,经过编译后会生成 sql/sql_yacc.cc 文件。

sql_yacc.yy 中,用你熟悉的 EBNF 格式定义了 MySQL 的语法规则。我节选了与 select语句有关的规则,如下所示,从中你可以体会一下,SQL 语句的语法是怎样被一层一层定义出来的:

select_stmt:
    query_expression
    |...
    |select_stmt_with_into
    ;
    query_expression:
    query_expression_bodyopt_order_clauseopt_limit_clause
    |with_clausequery_expression_bodyopt_order_clauseopt_limit_clause
    |...
    ;
query_expression_body:
    query_primary
    |query_expression_bodyUNION_SYMunion_optionquery_primary
    |...
    ;
query_primary:
    query_specification
    |table_value_constructor
    |explicit_table
    ;
query_specification:
    ...
    |SELECT_SYM    /*select关键字*/
    select_options    /*distinct等选项*/
    select_itemlist    /*select项列表*/
    opt_from_clauseopt    /*可选:from子句*/
    opt_where_clauseopt    /*可选:where子句*/
    opt_group_clauseopt    /*可选:group子句*/
    opt_having_clauseopt    /*可选:having子句*/
    opt_window_clause    /*可选:window子句*/
    ;

其中,query_expression 就是一个最基础的 select 语句,它包含了 SELECT 关键字、字段列表、from 子句、where 子句等。

你可以看一下 select_options、opt_from_clause 和其他几个以 opt 开头的规则,它们都是 SQL 语句的组成部分。opt 是可选的意思,也就是它的产生式可能产生ε。

opt_from_clause:
        /*Empty.*/
    |from_clause
    ;

另外,你还可以看一下表达式部分的语法。在 MySQL 编译器当中,对于二元运算,你可以大胆地写成左递归的文法。因为它的语法分析的算法用的是 LALR,这个算法能够自动处理左递归。

一般研究表达式的时候,我们总是会关注编译器是如何处理结合性和优先级的。那么,bison 是如何处理的呢?

原来,bison 里面有专门的规则,可以规定运算符的优先级和结合性。在 sql_yacc.yy 中,你会看到如下所示的规则片段:

 

你可以看一下 bit_expr 的产生式,它其实完全把加减乘数等运算符并列就行了。

bit_expr:
...
    |bit_expr'+'bit_expr%prec'+'
    |bit_expr'-'bit_expr%prec'-'
    |bit_expr'*'bit_expr%prec'*'
    |bit_expr'/'bit_expr%prec'/'
    ...
    |simple_expr

如果你只是用到加减乘除的运算,那就可以不用在产生式的后面加 %prec 这个标记。但由于加减乘除这几个还可以用在其他地方,比如“-a”可以用来表示把 a 取负值;减号可以用在一元表达式当中,这会比用在二元表达式中有更高的优先级。也就是说,为了区分同一个 Token 在不同上下文中的优先级,我们可以用 %prec,来说明该优先级是上下文依赖的。

好了,在了解了词法分析器和语法分析器以后,我们接着来跟踪一下 MySQL 的执行,看看编译器所生成的解析树和 AST 是什么样子的。

在 sql_class.cc 的 sql_parser() 方法中,编译器执行完解析程序之后,会返回解析树的根节点 root,在 GDB 中通过 p 命令,可以逐步打印出整个解析树。你会看到,它的根节点是一个 PT_select_stmt 指针(见图 3)。

解析树的节点是在语法规则中规定的,这是一些 C++ 的代码,它们会嵌入到语法规则中去。

下面展示的这个语法规则就表明,编译器在解析完 query_expression 规则以后,要创建一个 PT_query_expression 的节点,其构造函数的参数分别是三个子规则所形成的节点。对于 query_expression_body 和 query_primary 这两个规则,它们会直接把子节点返回,因为它们都只有一个子节点。这样就会简化解析树,让它更像一棵 AST。

query_expression:
    query_expression_body
    opt_order_clause
    opt_limit_clause
    {
        $$=NEW_PTNPT_query_expression($1,$2,$3);    /*创建节点*/
    }
    |...
query_expression_body:
    query_primary
    {
        $$=$1;  /*直接返回query_primary的节点*/
    }
    |...
query_primary:
    query_specification
    {
        $$=$1;  /*直接返回query_specification的节点*/
    }
    |...

最后,对于“select first_name, last_name from employees”这样一个简单的 SQL 语句,它所形成的解析树如下:

 

图 3:示例 SQL 解析后生成的解析树

而对于“select 2 + 3”这样一个做表达式计算的 SQL 语句,所形成的解析树如下。你会看到,它跟普通的高级语言的表达式的 AST 很相似:

 

图 4:“select 2 + 3”对应的解析树

图 4 中的 PT_query_expression 等类,就是解析树的节点,它们都是 Parse_tree_node的子类(PT 是 Parse Tree 的缩写)。这些类主要定义在 sql/parse_tree_nodes.h 和parse_tree_items.h 文件中。

其中,Item 代表了与“值”有关的节点,它的子类能够用于表示字段、常量和表达式等。你可以通过 Item 的 val_int()、val_str() 等方法获取它的值。

 

图 5:解析树的树节点(部分)

由于 SQL 是一个个单独的语句,所以 select、insert、update 等语句,它们都各自有不同的根节点,都是 Parse_tree_root 的子类。

 

图 6:解析树的根节点

好了,现在你就已经了解了 SQL 的解析过程和它所生成的 AST 了。前面我说过,MySQL采用的是 LALR 算法,因此我们可以借助 MySQL 编译器,来加深一下对 LR 算法家族的理解。

四、重温LR算法

你在阅读 yacc.yy 文件的时候,在注释里,你会发现如何跟踪语法分析器的执行过程的一些信息。

你可以用下面的命令,带上“-debug”参数,来启动 MySQL 服务器:

mysqld --debug="d,parser_debug"

然后,你可以通过客户端执行一个简单的 SQL 语句:“select 2+3*5”。在终端,会输出语法分析的过程。这里我截取了一部分界面,通过这些输出信息,你能看出 LR 算法执行过程中的移进、规约过程,以及工作区内和预读的信息。

 

我来给你简单地复现一下这个解析过程。

第 1 步,编译器处于状态 0,并且预读了一个 select 关键字。你已经知道,LR 算法是基于一个 DFA 的。在这里的输出信息中,你能看到某些状态的编号达到了一千多,所以这个DFA 还是比较大的。

第 2 步,把 select 关键字移进工作区,并进入状态 42。这个时候,编译器已经知道后面跟着的一定是一个 select 语句了,也就是会使用下面的语法规则:

query_specification:
    ...
    |SELECT_SYM /*select关键字*/
    select_options  /*distinct等选项*/
    select_item_list    /*select项列表*/
    opt_from_clauseopt  /*可选:from子句*/
    opt_where_clauseopt /*可选:where子句*/
    opt_group_clauseopt /*可选:group子句*/
    opt_having_clauseopt    /*可选:having子句*/
    opt_window_clause   /*可选:window子句*/
    ;

为了给你一个直观的印象,这里我画了 DFA 的局部示意图(做了一定的简化),如下所示。你可以看到,在状态 42,点符号位于“select”关键字之后、select_options 之前。select_options 代表了“distinct”这样的一些关键字,但也有可能为空。

 

图 7:移进 select 后的 DFA

第 3 步,因为预读到的 Token 是一个数字(NUM),这说明 select_options 产生式一定生成了一个ε,因为 NUM 是在 select_options 的 Follow 集合中

这就是 LALR 算法的特点,它不仅会依据预读的信息来做判断,还要依据 Follow 集合中的元素。所以编译器做了一个规约,也就是让 select_options 为空。

也就是,编译器依据“select_options->ε”做了一次规约,并进入了新的状态 920。注意,状态 42 和 920 从 DFA 的角度来看,它们是同一个大状态。而 DFA 中包含了多个小状态,分别代表了不同的规约情况。

 

图 8:基于“select_options->ε”规约后的 DFA

你还需要注意,这个时候,老的状态都被压到了栈里,所以栈里会有 0 和 42 两个状态。栈里的这些状态,其实记录了推导的过程,让我们知道下一步要怎样继续去做推导。

 

图 9:做完前 3 步之后,栈里的情况

第 4 步,移进 NUM。这时又进入一个新状态 720。

 

图 10:移进 NUM 后的 DFA

而旧的状态也会入栈,记录下推导路径:

 

图 11:移进 NUM 后栈的状态

第 5~8 步,依次依据 NUM_literal->NUM、literal->NUM_literal、simple_expr->literal、bit_expr->simple_expr 这四条产生式做规约。这时候,编译器预读的 Token是 + 号,所以你会看到,图中的红点停在 + 号前。

 

图 12:第 8 步之后的 DFA

第 9~10 步,移进 + 号和 NUM。这个时候,状态又重新回到了 720。这跟第 4 步进入的状态是一样的。

 

图 13:第 10 步之后的 DFA

而栈里的目前有 5 个状态,记录了完整的推导路径。

 

图 14:第 10 步之后栈的状态

到这里,其实你就已经了解了 LR 算法做移进和规约的思路了。不过你还可以继续往下研究。由于栈里保留了完整的推导路径,因此 MySQL 编译器最后会依次规约回来,把栈里的元素清空,并且形成一棵完整的 AST。

总结

本文已经带你初步探索了 MySQL 编译 SQL 语句的过程。你需要记住几个关键点:

  • 掌握如何用 GDB 来跟踪 MySQL 的执行的方法。你要特别注意的是,我给你梳理的那些关键的程序入口,它是你理解 MySQL 运行过程的地图。
  • SQL 语言是面向关系数据库的一种 DSL,它是声明式的,并采用了领域特定的模型和术语,可以为你设计自己的 DSL 提供启发。
  • MySQL 的语法分析器是采用 bison 工具生成的。这至少说明,语法分析器生成工具是很有用的,连正式的数据库系统都在使用它,所以你也可以大胆地使用它,来提高你的工作效率。我在最后的参考资料中给出了 bison 的手册,希望你能自己阅读一下,做一些简单的练习,掌握 bison 这个工具。
  • 最后,你一定要知道 LR 算法的运行原理,知其所以然,这也会更加有助于你理解和用好工具。

最后把文本的内容给你整理成了一张知识地图,供你参考和复习回顾:

 

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/54003.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

seata在nacos上注册IP为内网,启动时加了 -h 外网ip还是显示内网?

版本: 部署位置:Linux seata版本:1.5.1 问题: seata在nacos上注册IP为内网,启动时加了 -h 外网ip还是显示内网? 解决: 该版本存在-h失效问题,后面1.5.2就修掉-h失效的问题了。 可以在sea…

Web前端大作业——城旅游景点介绍(HTML+CSS+JavaScript) html旅游网站设计与实现

👨‍🎓学生HTML静态网页基础水平制作👩‍🎓,页面排版干净简洁。使用HTMLCSS页面布局设计,web大学生网页设计作业源码,这是一个不错的旅游网页制作,画面精明,排版整洁,内容…

更新UpdatePanel外部控件

目前处理项目问题的时候,发现有个功能有问题。 界面大致如下 版本radiobuttonlist(在UpdatePanel外) UpdatePanel 上传按钮 文件列表 UpdatePanel 正常逻辑: 上传文件后,文件列表会刷新。(这块没问…

[附源码]Python计算机毕业设计Django家庭教育app

项目运行 环境配置: Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术: django python Vue 等等组成,B/S模式 pychram管理等等。 环境需要 1.运行环境:最好是python3.7.7,我…

[附源码]Python计算机毕业设计Django惠农微信小程序论文

项目运行 环境配置: Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术: django python Vue 等等组成,B/S模式 pychram管理等等。 环境需要 1.运行环境:最好是python3.7.7,…

B树(BTree)与B+树(B+Tree)

B树是什么? B树是一种多路平衡查找树 平衡,指的是子树高度相同(即所有叶子结点均在同一层),即每个结点的平衡因子均等于0 多路,就是它除了根结点外(之所以根结点的分叉数不限定,是…

【java】多线程

文章目录进程和线程继承Thread类的方式实现多线程设置和获取线程的名称线程优先级 线程调度控制线程线程的生命周期多线程的实现方式案例--卖票同步方法解决数据安全问题线程安全的类Lock锁生产者消费者模式概述案例进程和线程 继承Thread类的方式实现多线程 MyThread.java pa…

懵了,阿里一面就被虐了,幸获内推华为技术四面,成功拿到offer

上个月,哥们从某小厂离职,转投阿里云,简历优秀,很顺利地拿到了面试通知,但之后的进展却让哥们怀疑人生了,或者说让哥们懵逼的是,面试阿里云居然第一面就被吊打?让哥们开始怀疑自己&a…

【OpenCV-Python】教程:3-12 模板匹配

OpenCV Python 模板匹配 【目标】 利用模板匹配的方法寻找目标cv2.matchTemplate(), cv2.minMaxLoc() 【理论】 模板匹配是一个寻找大图像中目标位置的方法。OpenCV提供了函数 cv2.matchTemplate() 函数,通过在输入图像上滑动模板,将目标与滑动处的图…

[附源码]计算机毕业设计JAVA校园淘宝节系统

[附源码]计算机毕业设计JAVA校园淘宝节系统 项目运行 环境配置: Jdk1.8 Tomcat7.0 Mysql HBuilderX(Webstorm也行) Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。 项目技术: SSM mybatis …

【c++】STL--string

前言 最开始我们学习c语言的时候,我们发现刷题或者写代码都是比较麻烦的,如果说用c语言造一辆车,那么我需要用c语言先把轮子造好--各个零件,当我们造好之后再组装。那么c则是造好了轮子,只需要我们组装就好了。这里的的…

岩藻多糖-聚乙二醇-过氧化氢酶,Catalase-PEG-Fucoidan,过氧化氢酶-PEG-岩藻多糖

岩藻多糖-聚乙二醇-过氧化氢酶,Catalase-PEG-Fucoidan,过氧化氢酶-PEG-岩藻多糖 中文名称:岩藻多糖-过氧化氢酶 英文名称:Fucoidan-Catalase 别称:过氧化氢酶修饰岩藻多糖,过氧化氢酶-岩藻多糖 过氧化氢…

LiteFlow v2.9.4发布!一款能让你系统支持热更新,编排,脚本编写逻辑的国产规则引擎框架

前言 上海的天气降温让人猝不及防,但是我们的迭代速度却井然有序。 今天我们带来了LiteFlow v2.9.4版本。 我们每次的发布的issue有很大一部分依托于我们的使用者社区,社区人越来越多。我看到了使用者在使用过程中遇到的问题,也收集了很多…

【Java实战】这样写SQL语句性能嘎嘎好

目录 一、前言 二、SQL语句 1.【强制】不要使用 count(列名) 或 count(常量) 来替代 count(*),count(*) 是 SQL92 定义的标准统计行数的语法,跟数据库无关,跟 NULL 和非 NULL 无关。 2.【强制】count(distinct col) 计算该列除 NULL 之外的…

如何实现网站首页变为黑白色?

某些时候,网站会根据要求将页面调成黑白色,一开始我还以为是将连夜把图片和文字都搞成黑白色,但是转念一想,像推送产品的京东、淘宝,以及展示up内容的B站、CSDN等,刷新之后可能展示的内容均不同&#xff0c…

从上帝视角认识SpringMVC预览

前言 SpringMVC提供了很多可拓展的组件,例如:参数解析器、拦截器、异常处理器等等。但是如果想要理解/找到这些组件工作的位置/时机,很多时候总是容易迷失在其层层调用的源码之中。因此才想从上帝视角来剖析它。而所谓上帝视角,就…

[附源码]Python计算机毕业设计Django海南琼旅旅游网

项目运行 环境配置: Pychram社区版 python3.7.7 Mysql5.7 HBuilderXlist pipNavicat11Djangonodejs。 项目技术: django python Vue 等等组成,B/S模式 pychram管理等等。 环境需要 1.运行环境:最好是python3.7.7,…

22.12.1打卡 漫步校园 记忆化搜索

题目里很显然只走最短路, 直接用bfs从终点到起点搜一遍将每一步到终点所需要的最短的时间存在一个dis数组中, 然后你就会发现原来的地图变成了这样 上面是地图下面是dis数组, 再看看经典记忆化搜索模板题滑雪的地图 对的, 非常地相似, 接下来的操作和滑雪基本一样, 只不过起点是…

SQL创建新的输出字段

SQL创建新的输出字段1、准备数据2、对单个字段或者多个字段进行数值计算3、数值计算4、字段拼接5、字段使用别名6、 CASE WHEN逻辑转换case when 语法一case when 语法二case when 注意点查询的值可以为任何值(例如可以: select *)可以重命名…

Docker 快速安装Jenkins完美教程 (亲测采坑后详细步骤)(转)

转载至:https://www.cnblogs.com/fuzongle/p/12834080.html Docker 快速安装Jenkins完美教程 (亲测采坑后详细步骤) Jenkins是一个开源软件项目,是基于Java开发的一种持续集成工具,用于监控持续重复的工作&#xff0…