博客园  :: 首页  :: 新随笔  :: 订阅 订阅  :: 管理

MySQL 源码解读之-语法解析(四)

Posted on 2022-11-19 13:25  面具下的戏命师  阅读(1000)  评论(0编辑  收藏  举报

MySQL 源码解读之-语法解析(四)

在上篇文章中,我们分析了一条 sql 语句 select * from bank; 警告bison 语法解析器(MYSQLparser 函数)生成的AST 树的结构,如下图所示:

mysql 需要对这个AST 做进一步的处理,调用 LEX::make_sql_cmd 函数将当前的 AST树实例化Sql_cmd对象并将其分配给Lex。

相关的数据结构

Sql_cmd 类是 sql 命令的表现形式,此类是解析器和运行时之间的接口。解析器构建相应的Sql_cmd派生类,以表示已解析树中的Sql语句。Sql_cmd派生类中的execute()方法包含运行时实现。请注意,此接口用于最近实现的SQL语句,旧语句的代码倾向于加载具有更多属性的LEX结构。

通过对Sql_cmd进行子类化来实现新语句,因为这提高了代码的模块性(参见dispatch_command()中的“大开关”),并减少了LEX结构的总大小(因此节省了存储程序中的内存)。Sql_cmd派生类的推荐名称为Sql_cmd_<derived>。

class Sql_cmd {
 public:
  virtual enum_sql_command sql_command_code() const = 0;      // 返回当前语句的命令码,如 SQLCOM_SELECT

  // 如果对象表示可准备语句,即用PREPARE语句准备并用EXECUTE语句执行的查询,则返回true。对于直接执行的常规语句(非可准备语句),返回False。如果语句是存储过程的一部分,则也是false
  bool needs_explicit_preparation() const {
    return m_owner != nullptr && !m_part_of_sp;
  }

  // 如果语句是正则的,则返回true. 既不是 prepare 语句也不是存储过程的一部分
  bool is_regular() const { return m_owner == nullptr && !m_part_of_sp; }
  
  // 判断一个语句是是否是 prepare 语句
  bool is_prepared() const { return m_prepared; }

  // prepare 这个语句
  virtual bool prepare(THD *) {
    assert(!is_prepared());
    set_prepared();
    return false;
  }

  // 执行这个语句
  virtual bool execute(THD *thd) = 0;

  // Command-specific reinitialization before execution of prepared statement
  virtual void cleanup(THD *) { m_secondary_engine = nullptr; }

  // 设置所属的prepare语句
  void set_owner(Prepared_statement *stmt) {
    assert(!m_part_of_sp);
    m_owner = stmt;
  }

  // 设置所属的prepare语句
  Prepared_statement *owner() const { return m_owner; }

  // 将语句标记为过程的一部分。这样的语句可以执行多次,第一次execute()调用也会准备它
  void set_as_part_of_sp() {
    assert(!m_part_of_sp && m_owner == nullptr);
    m_part_of_sp = true;
  }
  // 判断语句是否是存储过程的一部分
  bool is_part_of_sp() const { return m_part_of_sp; }

  // 判断一个语句是否DML
  virtual bool is_dml() const { return false; }

  // 如果实现为单表执行计划,则返回true,仅限DML语句
  virtual bool is_single_table_plan() const {
    assert(is_dml());
    return false;
  }

  virtual bool accept(THD *, Select_lex_visitor *) { return false; }

  virtual const MYSQL_LEX_CSTRING *eligible_secondary_storage_engine() const {
    return nullptr;
  }

  void disable_secondary_storage_engine() {
    assert(m_secondary_engine == nullptr);
    m_secondary_engine_enabled = false;
  }

  // 此语句是否禁用了辅助存储引擎的使用?
  bool secondary_storage_engine_disabled() const {
    return !m_secondary_engine_enabled;
  }

  void use_secondary_storage_engine(const handlerton *hton) {
    assert(m_secondary_engine_enabled);
    m_secondary_engine = hton;
  }

  bool using_secondary_storage_engine() const {
    return m_secondary_engine != nullptr;
  }

  // 获取用于执行此语句的辅助引擎的handlerton,如果未使用辅助引擎,则获取nullptr
  const handlerton *secondary_engine() const { return m_secondary_engine; }

  void set_optional_transform_prepared(bool value) {
    m_prepared_with_optional_transform = value;
  }

  bool is_optional_transform_prepared() {
    return m_prepared_with_optional_transform;
  }

 protected:
  Sql_cmd() : m_owner(nullptr), m_part_of_sp(false), m_prepared(false) {}

  virtual ~Sql_cmd() {
    // Sql_cmd对象在thd->mem_root中分配。在MySQL中,从未调用C++析构函数,而是简单地销毁底层MEM_ROOT。不要依赖析构函数进行任何清理。
    assert(false);
  }

  /// 设置语句为 prepare 语句
  void set_prepared() { m_prepared = true; }

 private:
  Prepared_statement *m_owner;  // prepare 语句,如果不是 prepate 值为 NULL
  bool m_part_of_sp;            // 是否是存储过程的一部分
  bool m_prepared;              // 已经被 prepare 的语句为 true

  // 指示辅助存储引擎是否可用于此语句。如果为false,则不会考虑使用辅助存储引擎来执行此语句。
  bool m_secondary_engine_enabled{true};

  // 跟踪语句是否准备了可选转换。
  bool m_prepared_with_optional_transform{false};

  // 用于执行此语句的辅助存储引擎(如果有),如果使用主引擎,则为nullptr。此属性在每次执行开始时重置。
  const handlerton *m_secondary_engine{nullptr};
};

Query_tables_list类

该类表示语句使用的所有表的列表以及打开和锁定其表所需的其他信息,类似语句的SQL command。 还包含有关语句使用的存储函数的信息,因为在语句执行期间,我们可能必须将其存储函数/触发器使用的所有表添加到此列表中,以便预先打开和锁定它们。LEX::reset_n_backup/restore_backup_query_tables_list()两个函数也用于保存和还原此信息。

该类的部分公用成员如下:其他成员和函数请参考源码,sql_lex.h  源码中写了大量的注释。

enum_sql_command sql_command;  // 此语句的SQL命令。该类的一部分,因为为语句打开和锁定表的过程需要这些信息来确定某些表的正确锁类型
TABLE_LIST *query_tables;      // 此语句使用的所有表的全局列表(通过next_global和prev_global构成所有talbe的双向链表)
TABLE_LIST **query_tables_last;   // 指向query_tables中最后一个元素
TABLE_LIST **query_tables_own_last; //如果非0,则表示查询需要预锁定,并指向查询表列表中最后一个自己元素的next_global成员(即作为预锁定准备的一部分未添加到其中的最后一个表)0-表示此查询不需要预锁定
LEX结构
结构体LEX继承自 Query_tables_list 类。LEX对象目前有三种不同的用途:
1、它包含SQL命令的一些通用属性,如SQL_command、数据更改语句语法中的IGNORE以及表列表(query_tables)

2、它包含一些执行状态变量,如m_exec_started(执行开始时设置为true)、插件(语句使用的插件列表)、insert_update_values_map(某些insert语句使用的对象映射)等

3、它包含许多应该是Sql_cmd子类的本地成员,如purge_value_list(对于purge命令)、kill_value_list(针对kill命令)

对于由Sql_cmd类表示的Sql命令,LEX对象严格来说是Sql_cmd的一部分。对于其余的SQL命令,它是链接到当前THD的独立对象。

LEX对象的生命周期如下:

LEX对象可以在执行mem_root(对于常规语句),Prepared_statement mem_root(对于预处理语句),SP mem_root(对于存储过程指令)上构造,或者在当前mem_root上创建以用于短期使用。
在使用之前,调用lex_start()初始化LEX对象。这将初始化对象的执行状态部分。它还调用LEX :: reset()以确保正确初始化所有成员。
使用LEX作为工作区来解析并解析该语句。
执行一个SQL命令:开始执行时(实际上是开始优化时)调用set_exec_started()。通常,调用is_exec_started()来区分SQL命令执行的准备阶段和优化/执行阶段。
执行完成后,调用clear_execution()。这将清除与SQL命令关联的所有执行状态,还包括调用LEX :: reset_exec_started()。

LEX对象会继承  Query_tables_list 的成员,其次 LEX 自身也定义了一些成员变量和函数:

 Query_expression *unit;  // 最外层查询表达式
  /// @todo: query_block can be replaced with unit->first-select()
  Query_block *query_block;            // 第一个查询块
  Query_block *all_query_blocks_list;  // 所有的查询块列表
 private:
  Query_block *m_current_query_block;  // 分析中的当前Query_block

 public:
  inline Query_block *current_query_block() const {
    return m_current_query_block;
  }

Query_expression 类 和 Query_block 类

类Query_expression表示查询表达式,类Query_block表示查询块。查询表达式包含一个或多个查询块(多个表示我们有UNION查询),这两个类的联系如下:

这两个类都有master、slave、next和prev 四个字段。对于Query_block类,master和slave指向Query_expression类型的对象,而对于Query_express类,它们指向Query_block。master是指向外部节点的指针。slave是指向第一个内部节点的指针。neighbors是同一级别上的两个Query_block或Query_expression对象。

这些结构与以下指针链接:

邻居列表(next/prev)(第一个元素的prev指向外部结构的slave指针)对于Query_block,这是一个查询块列表。对于Query_expression,这是子查询的列表。

指向外部节点(master)的指针,如果是Query_expression指针,指向外部Query_block。如果是Query_block指针,指向外部Query_expression。

指向内部对象(slave)的指针,如果是Query_expression:属于此查询表达式的第一个查询块。如果是Query_block, 属于此查询块(子查询)的第一个查询表达式

link_next/link_pre 用来链接所有Query_block对象(这将用于创建派生表之类的事情,在这里我们将遍历此列表并创建派生表。

若查询表达式包含多个查询块(UNION INTERSECT等),则它有一个名为fake_query_block的特殊查询块。它用于存储全局参数(如ORDERBY、LIMIT)和执行并集。

全局ORDER BY子句中使用的子查询将附加到此fake_query_block,这将允许它们正确解析包含UNION和外部select的字段。

例如以下 sql:

  select *
     from table1
     where table1.field IN (select * from table1_1_1 union
                            select * from table1_1_2)
     union
   select *
     from table2
     where table2.field=(select (select f1 from table2_1_1_1_1
                                   where table2_1_1_1_1.f2=table2_1_1.f3)
                           from table2_1_1
                           where table2_1_1.f1=table2.f2)
     union
   select * from table3;

我们将具有以下结构:

select1: (select * from table1 ...)
select2: (select * from table2 ...)
select3: (select * from table3)
select1.1.1: (select * from table1_1_1)
...

主要单位的关系如下:

所有query_block的列表如下(因为它将由解析器构建)

此处我们不再对这两个类做过多的描述,进一步了解可以参考以下博客

参考链接: mysql源码注释 

                mysql 8.0 Server 层最新架构详解 

                MySQL SELECT_LEX与subselect 执行 源码阅读笔记

源码调试

我们此次把断点打到 LEX::make_sql_cmd 函数,然后执行 select * from bank; 如图所示,已经命中了该断点:

我们执行 s 进去make_sql_cmd 函数进行调试,可以看到该函数定义如下:

我们继续跟进 make_cmd 函数。如下图,调用了 PT_query_expression::contextualize 进行上下文初始化,这个我们之前分析的 AST 树是一致的。根节点的 m_qe 成员指向 PT_query_expression。

我们继续用 s 跟进,看看 PT_query_expression::contextualize 函数做了什么,如下所示:首先对 with 语句做上下文初始化,因为此处 with 语句为空,所以该函数什么也不做。接下来执行了 Parse_tree_node::contextualize 将节点标记为上下文化。

接下来还执行了 m_body->contextualize(pc) 。我们前面分析过此处的 m_body 指的是 PT_query_specification。所以该函数为 PT_query_specification::contextualize。我们继续 s 跟进去查看

分别对PT_select_item_list 和 from_clauser做上下文初始化。  该函数还对 into 等字句做上下文初始化,因为我们的例子不涉及该函数,此处我们不做分析。我们继续跟进item_list 的上下文初始化。

如上图所示,我们对PT_select_item_list的初始化跟进分析,最终把 value 值赋给了 pc->select->fields。我们打印 value 是一个容器,容器存储的是 Item 对象,每个 Item 对象通过 for循环做对象初始化,(因为我们使用的 select *, 因此此处只有一个 Item 对象,就是Item_asterisk)。Item_asterisk对象的 itemize 函数中设置 pc->select->with_wild++, 设置selct 语句后边字段的个数,此处我们只有一个 * 号。 因此只有一个。

接下来是 from 子句的上下文初始化,

如上图所示,对 from 子句的初始化,通过 pc->select->add_to_list 函数加入 bank 表。

后续继续做了一系列初始化,如 where 条件,into 子句,窗口函数,当然本例为了简单,没有这些。最终返回一个 Sql_cmd_select 对象。

 做过初始化后的 AST 树如下所示:这里知识赋予了一些 value 值后的结果。 

 

最后我们来打印一下被初始化后的 AST 树的样子:

到此语法分析结束,进行语句执行函数,进行 sql 预处理和优化器等

参考:https://www.freesion.com/article/64711249190/

         https://blog.csdn.net/fs3296/article/details/117573357