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