语法分析是编译过程的一个逻辑截断。语法分析的任务是在词法分析的基础上将单词序列组合成各类语法短语,如“程序”,“语句”,“表达式”等等.语法分析程序判断源程序在结构上是否正确.源程序的结构由上下文无关文法描述.语法分析程序可以用YACC(BISON)等工具自动生成。
词法分析和语法解析有两个较成熟的开源工具Flex和Bison分别用来解决这两个问题。MySQL出于于性能和灵活考虑,选择了自己完成词法解析部分,语法规则部分使用Bison。词法解析和Bison沟通的核心函数是由词法解析器提供的函数接口yylex(),在Bison中,必要的时候调用yylex()获得词法解析的数据,完成自己的语法解析。Bison的入口时yyparse(),在MySQL中是,MYSQLParse。 如下 mysql 中的宏定义可以看出:
/* Substitute the variable and function names. */
#define yyparse MYSQLparse
#define yylex MYSQLlex
上篇博客中讲了词法分析的过程,词法分析将整个SQL语句打碎成一个个单词(Token)。MySQL 源码解读之-词法分析,而语法规则模块则根据MySQL定义的语法规则生成对应的数据结构,并存储在对象THD->LEX结构当中。
语法解析调用堆栈
yyparse是Bison生成的语法分析器入口,yyparse会不断地调用yylex获取token流去解析,和语法规则去做匹配,直到token流结束或者发现语法错误
基础数据结构
Bison在做语法解析后,会将解析结果(解析树/AST)存储在THD::LEX中,通过存储WHERE的数据结构来查看语法解析的结果。
语法树节点相关的声明和定义文件:
parse_tree_column_attrs.h parse_tree_hints.cc parse_tree_node_base.h parse_tree_handler.cc
parse_tree_hints.h parse_tree_nodes.cc parse_tree_handler.h parse_tree_items.cc
parse_tree_nodes.h parse_tree_helpers.cc parse_tree_items.h parse_tree_partitions.cc
parse_tree_helpers.h parse_tree_node_base.cc parse_tree_partitions.h
语法树根节点的基类: Parse_tree_root
class Parse_tree_root {
Parse_tree_root(const Parse_tree_root &) = delete;
void operator=(const Parse_tree_root &) = delete;
protected:
virtual ~Parse_tree_root() = default;
Parse_tree_root() = default;
public:
virtual Sql_cmd *make_cmd(THD *thd) = 0; // 纯虚函数,该类不可实例化,派生类需要重写该函数
};
其他语法树节点例如 PT_select_stmt PT_insert PT_delete 均继承 Parse_tree_root 类,Parse_tree_root的子类都代表一类 sql,换句话说,所有Parse_tree_root的子类只能作为树的根节点。下边我们以 PT_select_stmt 为例:
class PT_select_stmt : public Parse_tree_root {
typedef Parse_tree_root super;
public:
/**
@param qe The query expression. // 参数qe, 查询表达式
@param sql_command The type of SQL command. // 参数sql_command SQL命令的类型
*/
PT_select_stmt(enum_sql_command sql_command, PT_query_expression_body *qe)
: m_sql_command(sql_command),
m_qe(qe),
m_into(nullptr),
m_has_trailing_locking_clauses{false} {}
/**
Creates a SELECT command. Only SELECT commands can have into. // 创建一个查询命令,只有查询可以存在 into ,即 select ... into
@param qe The query expression. // 查询表达式
@param into The own INTO destination. // into 的目的表 (SELECT * INTO Persons_backup FROM Persons)
@param has_trailing_locking_clauses True if there are locking clauses (like
`FOR UPDATE`) at the end of the
statement. // 如果语句末尾有锁定子句(如“FOR UPDATE”),则为True
*/
explicit PT_select_stmt(PT_query_expression_body *qe,
PT_into_destination *into = nullptr,
bool has_trailing_locking_clauses = false)
: m_sql_command{SQLCOM_SELECT},
m_qe{qe},
m_into{into},
m_has_trailing_locking_clauses{has_trailing_locking_clauses} {}
PT_select_stmt(PT_query_expression *qe) : PT_select_stmt(qe, nullptr) {}
Sql_cmd *make_cmd(THD *thd) override;
private:
enum_sql_command m_sql_command;
PT_query_expression_body *m_qe;
PT_into_destination *m_into;
const bool m_has_trailing_locking_clauses;
};
语法树node节点的基类: Parse_tree_node
Parse tree上所有的node都定义为Parse_tree_node的子类。Parse_tree_node是一个类模板结构体,定义如下:
typedef Parse_tree_node_tmpl<Parse_context> Parse_tree_node;
template<typename Context>
class Parse_tree_node_tmpl
{
...
private:
/*
False right after the node allocation. The contextualize/contextualize_
function turns it into true.
*/
#ifndef DBUG_OFF
bool contextualized;
#endif//DBUG_OFF
/*
这个变量是由于当前仍旧有未完成的相关worklog,parser的refactor还没有彻底完成。当前的parser中还有一部分上下文依赖的关系没有独立出来。
等到整个parse refactor完成之后该变量就会被移除。
*/
bool transitional;
public:
/*
Memory allocation operator are overloaded to use mandatory MEM_ROOT
parameter for cheap thread-local allocation.
Note: We don't process memory allocation errors in refactored semantic
actions: we defer OOM error processing like other error parse errors and
process them all at the contextualization stage of the resulting parse
tree.
*/
static void *operator new(size_t size, MEM_ROOT *mem_root) throw ()
{ return alloc_root(mem_root, size); }
static void operator delete(void *ptr,size_t size) { TRASH(ptr, size); }
static void operator delete(void *ptr, MEM_ROOT *mem_root) {}
protected:
Parse_tree_node()
{
#ifndef DBUG_OFF
contextualized= false;
transitional= false;
#endif//DBUG_OFF
}
public:
...
/*
True if contextualize/contextualized function has done:
*/
#ifndef DBUG_OFF
bool is_contextualized() const { return contextualized; }
#endif//DBUG_OFF
/*
这个函数是需要被所有子类继承的,所有子类需要定义属于自己的上下文环境。通过调用子类的重载函数,进而初始化每个Parse tree node。
*/
virtual bool contextualize(THD *thd);
/**
my_parse_error() function replacement for deferred reporting of parse
errors
@param thd current THD
@param pos location of the error in lexical scanner buffers
*/
void error(THD *thd) const;
};
item 类:
Item是一个类,每一个Item实例都代表一个SQL语句里的对象,它有取值和数据类型指针。下面列出的的SQL相关的对象都是一个Item对象,或者继承至Item:
1、一段字符 2、数据表的某列 3、一个局部或全局变量 4、一个存储过程的变量 5、一个用户参数 6、个函数/存储过程(这包括运算符+、||、=、like等)
例如下面的SQL语句:
SELECT UPPER(column1) FROM t WHERE column2 = @x;
MySQL需要一系列的Item来描述上面的SQL:一个描述column1对象,描述UPPER函数的对象,还有描述WHERE语句的几个相关的Item对象。Item对象可以理解做一个特殊的数据对象。
item相关的声明和定义文件:
item.cc item_geofunc_internal.h item_row.cc item.h item_geofunc_relchecks.cc
item_row.h item_buff.cc item_geofunc_relchecks_bgwrap.cc item_strfunc.cc
item_cmpfunc.cc item_geofunc_relchecks_bgwrap.h item_strfunc.h item_cmpfunc.h
item_geofunc_setops.cc item_subselect.cc item_create.cc item_inetfunc.cc
item_subselect.h item_create.h item_inetfunc.h item_sum.cc item_func.cc
item_json_func.cc item_sum.h item_func.h item_json_func.h item_timefunc.cc
item_geofunc.cc item_pfs_func.cc item_timefunc.h item_geofunc.h item_pfs_func.h
item_xmlfunc.cc item_regexp_func.h item_geofunc_buffer.cc item_regexp_func.cc
item_xmlfunc.h item_geofunc_internal.cc
Item(继承自Parse_tree_node)使得对象和词法语法解析关联起来。用于表示条件表达式查询的结点(包括sub select),Item组织关系逻辑上也是棵树。
一般条件表达式结点的分类是:
与大部分表达式节点树不同的是,Item对象除了节点表示之外还承载了计算的功能。以下为Item的主要作用:
Item的构建
MySQL会通过yacc解析将条件表达式解析成一颗Item树(暂称为解析树)。解析树里会有一部分是PTI_开头的Item,PTI_Item都是继承自Parse_tree_item(也是Item的子类),是一种解析过程中过渡的Item(注释里认为这是一种placeholder)。在contextualize阶段时,会对这些PTI_item进行itemize,将它们从解析树节点转化成真正意义的表达式树节点。
需注意:
// TODO: refix_fields是干啥的?
集中典型的Item 介绍
Item_num是表示数值型的常量,类里存储的就是对应数值常量值value,int/bigint统一存成longlong,float/double统一存成double,decimal类型自己有一个Item_decimal实现。
数值型的实现简单可表示成如下:
class Item_xx : public Item_num { // xx for int/uint/float/decimal...
NUM_TYPE value;
int val_int() {
// return int rep of value;
}
double val_real() {
// return double rep of value;
}
};
存储字符串常量值,类型默认为VARCHAR。varchar变量关注str_value、collation、max_length。常量节点:Item_string
其中val_int的实现是my_strtoll10,可以理解为是一个string到longlong的hash实现。
时间类的Item实现都在item_timefunc.h/cc,时间相关的函数在MySQL里一般都包含temporal的命名。
Item_date_literal继承自Item_date_func,是因为MySQL的SQL中表示DATE常量是用DATE '2019-01-01'这种函数形式实现的。内部存储是一个MYSQL_TIME_cache对象,里面的MYSQL_TIME会以struct形式存储年月日时分秒的信息,同时还支持微秒us (microsecond)。需注意内部时间有多种表示,以DATE举例:
DATE/DATETIME/TIME的实现和上述相似。
Item_cond_and继承自Item_cond,本身没有什么新的方法或属性。唯一不同的是它的children是存在一个List<Item> list成员变量里,而并非使用Item的arguments来存储。
Item_cond_or类似不再赘述。
字段节点最主要的成员变量如下:
/**
Table containing this resolved field. This is required e.g for calculation
of table map. Notice that for the following types of "tables",
no TABLE_LIST object is assigned and hence table_ref is NULL:
- Temporary tables assigned by join optimizer for sorting and aggregation.
- Stored procedure dummy tables.
For fields referencing such tables, table number is always 0, and other
uses of table_ref is not needed.
*/
TABLE_LIST *table_ref;
/// Source field
Field *field;
/**
Item's original field. Used to compare fields in Item_field::eq() in order
to get proper result when field is transformed by tmp table.
*/
Field *orig_field;
/// Result field
Field *result_field;
Item_equal *item_equal;
Item_sum不代表sum函数(sum函数实现是Item_sum_sum),Item_sum是所有agg函数的父类(叫Item_agg可能更合适)。Item_sum都会有一组接口:
virtual void clear() = 0;
virtual bool add() = 0;
virtual bool setup(THD *) { return false; }
// 以及 val_xxx 接口
待看完子查询相关再写
Item的求值的核心方法就是val_xxx函数,统一的接口可以从val_int看进去,因为所有Item都会有个val_int的实现(内部可能会调用它实际的val_xxx类型的实现,然后转为int表示或hash值)。常量节点求值逻辑上面有部分介绍,函数节点就是函数的计算逻辑。
表达式计算调用在evaluate_join_record中,仅需要短短一句condition->val_int()来判断是否被筛选掉。
// static enum_nested_loop_state evaluate_join_record(JOIN *join, QEP_TAB *const qep_tab);
Item *condition = qep_tab->condition();
bool found = true;
if (condition) {
found = condition->val_int();
if (join->thd->killed) {
join->thd->send_kill_message();
DBUG_RETURN(NESTED_LOOP_KILLED);
}
/* check for errors evaluating the condition */
if (join->thd->is_error()) DBUG_RETURN(NESTED_LOOP_ERROR);
}
常量表达式会将节点const_for_execution设为true。但是除了eval_const_cond用于判断部分bool值表达式的常量计算外,比如 col > 1+2这种并未优化成 col>3。
谓语下推核心是handler的cond_push函数(默认未实现)或idx_cond_push函数。
5.x版的cond_push会在两个地方被调用,一个是优化器里,一个是records.cc里(for execution)。这里SELECT会触发两次的cond_push,该问题已在社区被汇报成issue。
8.0版的优化器里的cond_push被保留,records.cc里的去掉,相应的移到了sql_update.cc/sql_delete.cc里,避免了SELECT触发两次cond_push的bug。(RDS这边的封了个PushDownCondition,仍未解这个问题)。
// JOIN::optimize()
if (thd->optimizer_switch_flag(
OPTIMIZER_SWITCH_ENGINE_CONDITION_PUSHDOWN) &&
first_inner == NO_PLAN_IDX) {
Item *push_cond = make_cond_for_table(
thd, tmp, tab->table_ref->map(), tab->table_ref->map(), 0);
if (push_cond) {
/* Push condition to handler */
if (!tab->table()->file->cond_push(push_cond))
tab->table()->file->pushed_cond = push_cond;
}
}
make_cond_for_table已经保证抽取出来的push_cond是针对单表的condition了,handler相应实现拿到Item可以遍历或转化成自己想要的结构处理,这部分不在此赘述。
有个未确认的问题。实际的下推接口是一对接口 cond_push & cond_pop,而idx_cond_push不存在pop接口。按照ndb的实现,cond_push的是一个栈push操作,不知道为啥condition会构成一个栈结构存在。事实发现似乎不理会cond_pop,就当每个查询每个表只会调用一次cond_push也是没问题的。