MySQL Item 源码阅读笔记

MySQL Item 源码阅读笔记

Based on MySQL8.0 community version

Outline

  1. Item的内容与作用
  2. Item的构建
  3. 几种典型Item的介绍
  4. Item表达式求值与相关优化的实现
  5. 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,将它们从解析树节点转化成真正意义的表达式树节点。

需注意:

  1. 部分非PTI_Item (比如非date的常量类的等比较简单的节点)会在yacc解析时直接构造。PTI_Item可以认为是一种过渡,只是因为实现方式问题而存在,并非是HighLevel意义上一定要存在的概念。
  2. 此时解析出来的表达式树未必是最终的完整版,后面经过transform/compile等操作有可能会改变树的结构。
  3. 不同的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也是没问题的。

posted @ 2021-06-13 11:12  Lhfcws  阅读(1116)  评论(0编辑  收藏  举报