MySQL Item 源码阅读笔记
MySQL Item 源码阅读笔记
Based on MySQL8.0 community version
Outline
- Item的内容与作用
- Item的构建
- 几种典型Item的介绍
- Item表达式求值与相关优化的实现
- Item与下推优化
1. Item的内容与作用
一个前人画的Item继承关系图,应该是基于MySQL 5.x:http://www.orczhou.com/wp-content/uploads/2012/11/classItem__inherit__graph.png
Item(继承自Parse_tree_node)是用于表示条件表达式查询的结点(包括sub select),在其他AP引擎中条件表达式一般会表达成多叉结点树结构,Item组织关系逻辑上也是棵树。
一般条件表达式结点的分类是:
-
常量节点/值节点(对应Item_base_constant):存储常量值
-
字段节点/列节点(对应Item_field):存储列字段的相关元信息
-
函数计算节点(对应Item_func):分为系统函数和UDF。系统函数指
+-*/ =><
等系统提供的基本函数型操作,也包含一些常用的函数,比如一些数学函数、加密函数等。有的其他AP引擎实现会将大部分的System func基于UDF实现。- 逻辑计算节点(对应Item_cond):主要是and、or、not等。这类函数可以看作是输入值为1个(not)或2个bool参数,返回值为bool的特殊函数。因此实现时也会基于函数计算节点去实现,但在表达式优化和计算时会另外看待。MySQL not实现在Item_func_not中。
-
聚合函数计算(对应Item_sum):分为系统聚合函数和UDF(有的也叫UDAF)。系统聚合函数包括sum、count、avg、max、min等。
与大部分表达式节点树不同的是,Item对象除了节点表示之外还承载了计算的功能。以下为Item的主要作用:
- 表达式节点表示。
- Item_base_constant
- Item_field
- Item_func
- 计算。每个Item对象都有
val_xxx
方法,尤其是val_int和val_str这两个方法MySQL内置Item类型都支持调用。以val_int举例,调用其可以得到以该Item为根节点的子树的求值。 - 遍历(调用入口为walk方法)。Item里定义了很多只属于其子类的Item_processor方法,具体的walk实现也是在相应子类中,除了Item_subselect,其他的walk实现都差不多。
- Transform&Compile(对应transform和compile方法):Transform表示对Item tree的转换,可能会添加0或多个新的Item节点;Compile则是会在当前节点transform之前做一次该节点子树的analyze,。
2. Item的构建
MySQL会通过yacc解析将条件表达式解析成一颗Item树(暂称为解析树)。解析树里会有一部分是PTI_开头的Item,PTI_Item都是继承自Parse_tree_item(也是Item的子类),是一种解析过程中过渡的Item(注释里认为这是一种placeholder)。在contextualize阶段时,会对这些PTI_item进行itemize,将它们从解析树节点转化成真正意义的表达式树节点。
需注意:
- 部分非PTI_Item (比如非date的常量类的等比较简单的节点)会在yacc解析时直接构造。PTI_Item可以认为是一种过渡,只是因为实现方式问题而存在,并非是HighLevel意义上一定要存在的概念。
- 此时解析出来的表达式树未必是最终的完整版,后面经过transform/compile等操作有可能会改变树的结构。
- 不同的Item的构造时机不一样,需case by case看,有的是在yacc解析时直接构造,有的是在itemize的时候构造。
常量节点
- 非时间类型的常量,会在yacc解析时直接构造相应的Item
- 时间类型的常量会先解析成PTI_temporal_literal,
PTI_temporal_literal::itemize
中会调用create_temporal_literal
来转换成对应的时间类型的Item。
TODO: 字段节点
- Select 函数内的field,i.e.
SELECT sum(l_extendedprice)
- Where 的field, i.e.
WHERE l_returnflag='A'
- Where 函数内的field, i.e.
WHERE abs(l_extendedprice) > 2
// TODO: refix_fields是干啥的?
3.几种典型Item的介绍
常量节点:Item_num
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;
}
};
常量节点:Item_string
存储字符串常量值,类型默认为VARCHAR。varchar变量关注str_value、collation、max_length。
- str_value存储字符串值
- collation存储字符集编码
- max_length存储的是根据编码实际encode后的字符串最大长度 (VARCHAR是变长的)
其中val_int的实现是my_strtoll10
,可以理解为是一个string到longlong的hash实现。
常量节点:Item_date_literal
时间类的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举例:
- struct MYSQL_TIME,直观的结构体表示
- val_int() ,MYSQL_TIME_cache::time_packed ,将年月日时分秒表示成整型形式,比如
2019-01-01
表示成整型20190101
。(私以为这个还不如时间戳统一) - string representation "2019-01-01"
- 存储时encode成3字节的存储格式的int表示
DATE/DATETIME/TIME的实现和上述相似。
Cond节点:Item_cond_and
Item_cond_and继承自Item_cond,本身没有什么新的方法或属性。唯一不同的是它的children是存在一个List<Item> list
成员变量里,而并非使用Item的arguments来存储。
Item_cond_or类似不再赘述。
字段节点:Item_field
字段节点最主要的成员变量如下:
/**
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;
- 在一些处理逻辑中,table_ref表示该Field所属的table
- field存储实际的字段值,每次read record后会将record store到相应的field里以便表达式计算。table scan里这一步是在handler::position()方法里由handler自己实现的,从
uchar* record
提取字段设置到table里。Item_field里的field和table的对应field 指向同一个Field对象。 - orig_field、result_field和item_equal未知
聚合节点:Item_sum
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 接口
可以把一个agg看成一组操作的组合:setup + N * add + val_xxx ,即初始化、流式操作或计算数据、合并计算。调用这组接口的是Aggregator类,Aggregator有两个子类实现 simple和distinct,simple什么都不做直接传递调用;distinct会借助去重树或临时表去做distinct操作。
Item_sum另外一类重要的变量和函数是关于window的,这个另外再提。
子查询节点:Item_subselect
待看完子查询相关再写
4.Item表达式求值
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
。
5.Item与谓语下推优化
谓语下推核心是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也是没问题的。