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也是沒問題的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM