一 背景和架構(gòu)
我們都知道,利用編寫(xiě)程序來(lái)動(dòng)態(tài)實(shí)現(xiàn)我們應(yīng)用所需要的邏輯,從而程序執(zhí)行時(shí)得到我們需要的結(jié)果。那么數(shù)據(jù)庫(kù)就是一種通過(guò)輸入SQL字符串來(lái)快速獲取數(shù)據(jù)的應(yīng)用。當(dāng)然,假設(shè)沒(méi)有數(shù)據(jù)庫(kù)這種系統(tǒng)應(yīng)用,用程序如何實(shí)現(xiàn)呢?我們可能會(huì)發(fā)現(xiàn),即使不管數(shù)據(jù)如何存儲(chǔ)、數(shù)據(jù)是否并發(fā)訪問(wèn),仍然需要不斷通過(guò)修改程序處理不同應(yīng)用對(duì)數(shù)據(jù)的不同請(qǐng)求。比如大數(shù)據(jù)領(lǐng)域,我們通常通過(guò)非關(guān)系型數(shù)據(jù)庫(kù)的API,實(shí)現(xiàn)對(duì)數(shù)據(jù)的獲取。然而這種方式雖然入門(mén)簡(jiǎn)單,但是維護(hù)極難,而且通用性不強(qiáng),即使不斷進(jìn)行軟件架構(gòu)設(shè)計(jì)或者抽象重構(gòu),仍然需要不斷地變換應(yīng)用,這也是為何非關(guān)系型數(shù)據(jù)庫(kù)回頭擁抱數(shù)據(jù)庫(kù)SQL優(yōu)化器的原因。
SQL優(yōu)化器本質(zhì)上是一種高度抽象化的數(shù)據(jù)接口的實(shí)現(xiàn),經(jīng)過(guò)該設(shè)計(jì),客戶可以使用更通用且易于理解的SQL語(yǔ)言,對(duì)數(shù)據(jù)進(jìn)行操作和處理,而不需要關(guān)注和抽象自己的數(shù)據(jù)接口,極大地解放了客戶的應(yīng)用程序。
本文就來(lái)通過(guò)圖形解說(shuō)的方式介紹下MySQL 8.0 SQL優(yōu)化器如何把一個(gè)簡(jiǎn)單的字符串(SQL),變成數(shù)據(jù)庫(kù)執(zhí)行器可以理解的執(zhí)行序列,最終將數(shù)據(jù)返還給客戶。強(qiáng)大的優(yōu)化器是不需要客戶關(guān)注SQL如何寫(xiě)的更好來(lái)更快獲得需要的數(shù)據(jù),因此優(yōu)化器對(duì)原始SQL一定會(huì)做一些等價(jià)的變化。在《MySQL 8.0 Server層最新架構(gòu)詳解》一文中我們重點(diǎn)介紹了MySQL最新版本關(guān)于Server層解析器、優(yōu)化器和執(zhí)行器的總體介紹,包括一些代碼結(jié)構(gòu)和變化的詳細(xì)展示,并且通過(guò)simple_joins函數(shù)拋磚引玉展示了MySQL優(yōu)化器在邏輯變換中如何簡(jiǎn)化嵌套Join的優(yōu)化。本文我們會(huì)一步一步帶你進(jìn)入神奇的優(yōu)化器細(xì)節(jié),詳細(xì)了解優(yōu)化器優(yōu)化部分的每個(gè)步驟如何改變著一個(gè)SQL最終的執(zhí)行。
本文基于最新MySQL8.0.25版本,因?yàn)閮?yōu)化器轉(zhuǎn)換部分篇幅比較長(zhǎng),我們分成兩篇文章來(lái)介紹,第一部分介紹基于基本結(jié)構(gòu)的Setup和Resolve的解析轉(zhuǎn)換過(guò)程,第二部分介紹更為復(fù)雜的子查詢、分區(qū)表和連接的復(fù)雜轉(zhuǎn)換過(guò)程,大綱如下:
Setup and Resolve
二 詳細(xì)轉(zhuǎn)換過(guò)程
轉(zhuǎn)換的整個(gè)框架是由Query_expression到Query_block調(diào)用prepare函數(shù)(sql/sql_resolver.cc)并且根據(jù)不同轉(zhuǎn)換規(guī)則的要求自頂向下或者自底向上的過(guò)程。
圖片
1 傳遞null到j(luò)oin的內(nèi)表列表(propagate_nullability)
prepare開(kāi)始先要處理nullable table,它指的是table可能包含全為null的row,根據(jù)JOIN關(guān)系(top_join_list)null row可以被傳播。如果能確定一個(gè)table為nullable會(huì)使得一些優(yōu)化退化,比如access method不能為EQ_REF、outer join不能優(yōu)化為inner join等。
2 解析設(shè)置查詢塊的leave_tables(setup_tables)
SELECT t1.c1FROM t1, (SELECt t2.c1 FROM t2, (SELECt t3.c1 FROM t3 UNIOn SELECt t4.c1 FROM t4) AS t3a) AS t2a;
未在setup_table調(diào)用之前,每個(gè)Query_block的leaf_tables是為0的。
該函數(shù)的作用就是構(gòu)建leaf_tables,包括base tables和derived tables列表,用于后續(xù)的優(yōu)化。setup_tables并不會(huì)遞歸調(diào)用,而是只解決本層的tables,并統(tǒng)計(jì)出本層derived table的個(gè)數(shù)。但是隨后會(huì)調(diào)用resolve_placeholder_tables()->resolve_derived()->derived(Query_expression)::prepare->Query_block::prepare來(lái)專門(mén)遞歸處理derived table對(duì)應(yīng)的Query_expression。
接下來(lái)我們根據(jù)prepare的調(diào)用順序,繼續(xù)看下針對(duì)于derived table處理的函數(shù)resolve_placeholder_tables。
3 解析查詢塊Derived Table、View、Table函數(shù) (resolve_placeholder_tables)
這個(gè)函數(shù)用于對(duì)derived table、view和table function的處理,如果該table已經(jīng)merged過(guò)了,或者是由于使用transform_grouped_to_derived()被調(diào)用到,已經(jīng)決定使用materialized table方式,則直接忽略。
前面已經(jīng)介紹過(guò)resolve_derived()的作用,我們重點(diǎn)介紹merge_derived()函數(shù),merge_derived是改變Query_expression/Query_block框架結(jié)構(gòu),將derived table或者view合并到到query block中。
merge_derived 處理和合并Derived table
1)merge_derived transformation的先決條件
2)merge_derived transformation的轉(zhuǎn)換過(guò)程
過(guò)程簡(jiǎn)化為:
merge_derived 圖解過(guò)程
看起來(lái)官方的derived merge還是不夠完美,無(wú)法自底向上的遞歸merge
包含的opt trace:
trace_derived.add_utf8_table(derived_table) .add("select#", derived_query_block->select_number) .add("merged", true);trace_derived.add_alnum("transformations_to_derived_table", "removed_ordering");
該優(yōu)化可以通過(guò)set optimizer_switch="derived_merge=on/off"來(lái)控制。
setup_materialized_derived 設(shè)置物化Derived Table
對(duì)于剩下不能采用 merge 算法的 derived table ,會(huì)轉(zhuǎn)為materialize 物化方式去處理。但此時(shí)只是做一些變量設(shè)置等預(yù)處理,實(shí)際的物化執(zhí)行是在executor階段執(zhí)行。
trace_derived.add_utf8_table(this) .add("select#", derived->first_query_block()->select_number) .add("materialized", true);
setup_table_function 處理表函數(shù)
如果 query block 中有 table function,整個(gè)過(guò)程會(huì)處理兩遍。第一遍會(huì)跳過(guò) table function 的 table ,第二遍才專門(mén)再對(duì)table function 的 table 執(zhí)行一遍上述邏輯。這里的考慮應(yīng)該是先 resolve 了外部環(huán)境(相對(duì)于table function),因?yàn)橛锌赡芎瘮?shù)參數(shù)會(huì)有依賴外部的 derived table。
trace_derived.add_utf8_table(this) .add_utf8("function_name", func_name, func_name_len) .add("materialized", true);
4 將SELECT *的通配符展開(kāi)成具體的fields(setup_wild)
5 建立Query_block級(jí)別的base_ref_items(setup_base_ref_items)
base_ref_items記錄了所有Item的位置,方便查詢塊的其他Item可以進(jìn)行引用,或者通過(guò)Item_ref及其Item_ref子類進(jìn)行直接引用,例如子查詢的引用(Item_view_ref)、聚合函數(shù)引用(Item_aggregate_ref)、外查詢列的引用(Item_outer_ref)、subquery 子查詢產(chǎn)生NULL value的引用輔助(Item_ref_null_helper)。
舉例說(shuō)明比較復(fù)雜的Item_outer_ref:
6 對(duì)select_fields進(jìn)行fix_fields()和列權(quán)限檢查(setup_fields)
下圖是比較復(fù)雜的帶子查詢的fixed field過(guò)程。有些field和表關(guān)聯(lián),有的要添加相應(yīng)的Item_xxx_ref引用。
7 解析和fixed_fields WHERe條件和Join條件(setup_conds)
setup_join_cond如果有nested_join會(huì)遞歸調(diào)用setup_join_cond進(jìn)行解析和設(shè)置。這里也順帶介紹下simplify_const_condition函數(shù)的作用,如果發(fā)現(xiàn)可以刪除的const Item,則會(huì)用Item_func_true/Item_func_false來(lái)替代整個(gè)的條件,如圖。
8 解析和設(shè)置ROLLUP語(yǔ)句(resolve_rollup)
在數(shù)據(jù)庫(kù)查詢語(yǔ)句中,在 GROUP BY 表達(dá)式之后加上 WITH ROLLUP 語(yǔ)句,可以使得通過(guò)單個(gè)查詢語(yǔ)句來(lái)實(shí)現(xiàn)對(duì)數(shù)據(jù)進(jìn)行不同層級(jí)上的分析與統(tǒng)計(jì)。
SELECT YEAR, country, product, SUM(profit) AS profitFROM salesGROUP BY YEAR, country, product WITH ROLLUP;+------+---------+------------+--------+| year | country | product | profit |+------+---------+------------+--------+| 2000 | Finland | Computer | 1500 || 2000 | Finland | Phone | 100 || 2000 | Finland | NULL | 1600 || 2000 | India | Calculator | 150 || 2000 | India | Computer | 1200 || 2000 | India | NULL | 1350 || 2000 | USA | Calculator | 75 || 2000 | USA | Computer | 1500 || 2000 | USA | NULL | 1575 || 2000 | NULL | NULL | 4525 || 2001 | Finland | Phone | 10 || 2001 | Finland | NULL | 10 || 2001 | USA | Calculator | 50 || 2001 | USA | Computer | 2700 || 2001 | USA | TV | 250 || 2001 | USA | NULL | 3000 || 2001 | NULL | NULL | 3010 || NULL | NULL | NULL | 7535 |+------+---------+------------+--------+相當(dāng)于做了下面的查詢:SELECt *FROM (SELECt YEAR, country, product, SUM(profit) AS profit FROM sales GROUP BY YEAR, country, product UNIOn ALL SELECt YEAR, country, NULL, SUM(profit) AS profit FROM sales GROUP BY YEAR, country UNIOn ALL SELECt YEAR, NULL, NULL, SUM(profit) AS profit FROM sales GROUP BY YEAR UNIOn ALL SELECt NULL, NULL, NULL, SUM(profit) AS profit FROM sales) AS sum_tableORDER BY YEAR, country, product;+------+---------+------------+--------+| YEAR | country | product | profit |+------+---------+------------+--------+| NULL | NULL | NULL | 7535 || 2000 | NULL | NULL | 4525 || 2000 | Finland | NULL | 1600 || 2000 | Finland | Computer | 1500 || 2000 | Finland | Phone | 100 || 2000 | India | NULL | 1350 || 2000 | India | Calculator | 150 || 2000 | India | Computer | 1200 || 2000 | USA | NULL | 1575 || 2000 | USA | Calculator | 75 || 2000 | USA | Computer | 1500 || 2001 | NULL | NULL | 3010 || 2001 | Finland | NULL | 10 || 2001 | Finland | Phone | 10 || 2001 | USA | NULL | 3000 || 2001 | USA | Calculator | 50 || 2001 | USA | Computer | 2700 || 2001 | USA | TV | 250 |+------+---------+------------+--------+
排序由于有NULL的問(wèn)題,所以分級(jí)匯總的效果非常難弄,而且group 列不同改變,SQL復(fù)雜度來(lái)回變化,而ROLLUP很簡(jiǎn)單就可以實(shí)現(xiàn)效果,下面看下rollup在解析過(guò)程做了什么樣的轉(zhuǎn)換達(dá)到了意想不到的效果。
9 解析和設(shè)置GROUP BY/ORDER BY語(yǔ)句(setup_group/setup_order)
其中一個(gè)函數(shù)find_order_in_list(): 嘗試在select fields里去尋找可以映射的列,否則就得在最后投影的all fields里加上當(dāng)前列,同時(shí)也做fix_fields。
remove_redundant_subquery_clause : 對(duì)于Table Subquery的表達(dá)式,通常是IN/ANY/ALL/EXISTS/etc,如果沒(méi)有聚合函數(shù)和Having子句,通常可以考慮刪除不必要的ORDER/DISTINCT/GROUP BY。該函數(shù)支持三種REMOVE_ORDER | REMOVE_DISTINCT | REMOVE_GROUP,如果是SINGLEROW_SUBS的子查詢,只考慮刪除REMOVE_ORDER。
select c1 from t1 where t1.c2 in (select distinct c1 from t2 group by c1, c2 order by c1);轉(zhuǎn)化為 =>select c1 from t1 where t1.c2 in (select c1 from t2);
is_grouped() && hidden_group_field_count == 0 && olap == UNSPECIFIED_OLAP_TYPE
例如場(chǎng)景:
SELECT DISTINCT c1, max(c2) from t1 group by c1;
10 解析和設(shè)置Window函數(shù)(Window::setup_windows1)
SELECt id, release_year, rating, avg(rating) over(PARTITION BY release_year) AS year_avgFROM tw;+------+--------------+--------+-------------------+| id | release_year | rating | year_avg |+------+--------------+--------+-------------------+| 1 | 2015 | 8 | 8.5 || 3 | 2015 | 9 | 8.5 || 2 | 2015 | 8.5 | 8.5 || 4 | 2016 | 8.2 | 8.3 || 5 | 2016 | 8.4 | 8.3 || 6 | 2017 | 7 | 7 |+------+--------------+--------+-------------------+
執(zhí)行的過(guò)程和結(jié)果類似于下圖:
我們看下它在開(kāi)始Query_block::prepare解析過(guò)程做了哪些事情:
select_lex->m_windows 不為空,就調(diào)用 Window::setup_windows1
三 綜述
本文重點(diǎn)介紹了下優(yōu)化器的基于規(guī)則的其中一部分優(yōu)化,更多的偏重于SQL中的基本操作符,如表、列、函數(shù)、聚合、分組、排序等元素的解析和設(shè)置以及一些顯而易見(jiàn)的結(jié)構(gòu)變化。下一篇文章我們將繼續(xù)介紹子查詢、分區(qū)表和JOIN操作的轉(zhuǎn)換部分,敬請(qǐng)期待。
四 參考資料
五 關(guān)于我們
PolarDB 是阿里巴巴自主研發(fā)的云原生分布式關(guān)系型數(shù)據(jù)庫(kù),于2020年進(jìn)入Gartner全球數(shù)據(jù)庫(kù)Leader象限,并獲得了2020年中國(guó)電子學(xué)會(huì)頒發(fā)的科技進(jìn)步一等獎(jiǎng)。PolarDB 基于云原生分布式數(shù)據(jù)庫(kù)架構(gòu),提供大規(guī)模在線事務(wù)處理能力,兼具對(duì)復(fù)雜查詢的并行處理能力,在云原生分布式數(shù)據(jù)庫(kù)領(lǐng)域整體達(dá)到了國(guó)際領(lǐng)先水平,并且得到了廣泛的市場(chǎng)認(rèn)可。在阿里巴巴集團(tuán)內(nèi)部的最佳實(shí)踐中,PolarDB還全面支撐了2020年天貓雙十一,并刷新了數(shù)據(jù)庫(kù)處理峰值記錄,高達(dá)1.4億TPS。歡迎有志之士加入我們,簡(jiǎn)歷請(qǐng)投遞到daoke.wangc@alibaba-inc,期待與您共同打造世界一流的下一代云原生分布式關(guān)系型數(shù)據(jù)庫(kù)。
作者 | 道客
原文鏈接:click.aliyun/m/1000295120/
本文為阿里云原創(chuàng)內(nèi)容,未經(jīng)允許不得轉(zhuǎn)載。