前言
從傳統規則來看,內聯視圖通常不允許引用在同一FROM子句中前面定義的表的列。但從OceanBase?4.2.2版本開始,這一限制得到了突破,允許內聯視圖作為Lateral Derived Table來定義,從而允許此類引用。Lateral Derived Table的語法與普通內聯視圖的語法相似,只是需要在內聯視圖之前之前添加關鍵字LATERAL。LATERAL關鍵字必須緊跟在需要作為Lateral Derived Table的每一個子查詢之前。
LATERAL關鍵字及其使用實例
允許在MySQL模式和Oracle模式下使用Lateral Derived Table,同時需要滿足如下要求:
- LATERAL關鍵字只能出現在FROM子句中,可以是用逗號分隔的表列表或者是JOIN(包含JOIN、INNER JOIN、CROSS JOIN、LEFT [OUTER] JOIN 或 RIGHT [OUTER] JOIN)中的一種。
- 如果LATERAL關鍵字在JOIN子句的右操作數中,并且包含對左操作數的引用,那么JOIN操作必須是INNER JOIN、CROSS JOIN或LEFT [OUTER] JOIN。如果表在左操作數中,并且包含對右操作數的引用,則JOIN操作必須是RIGHT [OUTER] JOIN。
- 如果Lateral Derived Table引用聚合函數,則該函數的聚合查詢不能是包含當前Lateral Derived Table所屬的查詢。
-- 滿足要求1,2,3
select * from t1, lateral (select * from t2 where t1.c1 = t2.c1);
select * from t1 cross join lateral (select * from t2 where t1.c1 = t2.c1) on 1=1;
select * from t1 left join lateral (select * from t2 where t1.c1 = t2.c1) on 1=1;
select * from lateral (select * from t2 where t1.c1 = t2.c1) right join t1 on 1=1;-- 不滿足要求3
select sum(t1.c1) as s from t1, lateral (select * from t2 where s = t2.c1);
ERROR 1054 (42S22): Unknown column 's' in 'where clause'
典型使用場景
場景1
Lateral關鍵字可以解決一些select list中的子查詢需要返回多列的場景,對于原先的查詢需要寫兩次子查詢,但使用Lateral就可以很好地解決這個問題。
-- Q1
select
(select avg(score) from score s where s.course_id = c.course_id) avg_score,
(select max(score) from score s where s.course_id = c.course_id) max_score
from course c where course_name = 'math';
Q1 會產生 2 次 score 表的掃描任務,使用 Lateral 改寫為 Q2 后,score 表可減少 1 次掃描。
-- Q2
select v1.avg_score, v1.max_scorefrom course c, lateral (select avg(score) avg_score, max(score) max_score from score s where s.course_id = c.course_id) v1where course_name = 'math';
場景2
-- 當前存在兩張表,一張是課程表,還有一張是成績表
create table Course (course_id int primary key,course_name varchar(20),teacher_id varchar(20)
);create table Score (student_id int,course_id int,score int,key i_course (course_id)
);
現在需要查出數學這門課的平均分和最高分,并且統計出超過平均分人數,基于現有的語法我們可以先使用子查詢分別查出平均分和最高分,然后在另外的子查詢中篩選出超過平均分的人數。
-- Q3
select
(select avg(score) from score s where s.course_id = c.course_id) avg_score,
(select max(score) from score s where s.course_id = c.course_id) max_score,
(select count(1) from score s where s.course_id = c.course_id and s.score > (select avg(score) from score s where s.course_id = c.course_id)) gt_avg_count
from course c where course_name = 'math';
===========================================================================
|ID|OPERATOR |NAME |EST.ROWS|EST.TIME(us)|
---------------------------------------------------------------------------
|0 |SUBPLAN FILTER | |1 |95 |
|1 |├─TABLE FULL SCAN |c |1 |4 |
|2 |├─SCALAR GROUP BY | |1 |23 |
|3 |│ └─DISTRIBUTED TABLE RANGE SCAN |s(i_course)|1 |23 |
|4 |├─SCALAR GROUP BY | |1 |23 |
|5 |│ └─DISTRIBUTED TABLE RANGE SCAN |s(i_course)|1 |23 |
|6 |└─SCALAR GROUP BY | |1 |46 |
|7 | └─SUBPLAN FILTER | |1 |46 |
|8 | ├─DISTRIBUTED TABLE RANGE SCAN |s(i_course)|1 |23 |
|9 | └─SCALAR GROUP BY | |1 |23 |
|10| └─DISTRIBUTED TABLE RANGE SCAN|s(i_course)|1 |23 |
===========================================================================
從上面的計劃可以看出,查詢Q3效率比較差,取平均分的子查詢調用了兩次,且平均分和最高分的子查詢可以合并到一起計算,現在分別使用兩個子查詢需要對score表掃描兩次相同的數據集。
對于查詢Q3,可以使用Lateral子句改寫一下,將查詢最大值和最小值的兩個子查詢合并,然后查詢超過平均分人數的子查詢引用外面已經計算好的平均分,改寫后的查詢語句Q4 如下。
-- Q3
select v1.avg_score, v1.max_score, v2.gv_avg_countfrom course c, lateral (select avg(score) avg_score, max(score) max_score from score s where s.course_id = c.course_id) v1,lateral (select count(1) gv_avg_count from score s where s.course_id = c.course_id and s.score > v1.avg_score) v2where course_name = 'math';
===========================================================================
|ID|OPERATOR |NAME |EST.ROWS|EST.TIME(us)|
---------------------------------------------------------------------------
|0 |NESTED-LOOP JOIN | |1 |51 |
|1 |├─NESTED-LOOP JOIN | |1 |28 |
|2 |│ ├─TABLE FULL SCAN |c |1 |4 |
|3 |│ └─SUBPLAN SCAN |v1 |1 |23 |
|4 |│ └─SCALAR GROUP BY | |1 |23 |
|5 |│ └─DISTRIBUTED TABLE RANGE SCAN|s(i_course)|1 |23 |
|6 |└─SUBPLAN SCAN |v2 |1 |23 |
|7 | └─SCALAR GROUP BY | |1 |23 |
|8 | └─DISTRIBUTED TABLE RANGE SCAN |s(i_course)|1 |23 |
===========================================================================
從改寫之后的計劃可以看出,效率明顯是高于Q3,現在對于score表只需要掃描兩次就能得到結果。
優化器改寫優化
查詢Q2改寫為LATERAL子句后,生成的執行計劃如下:
=========================================================================
|ID|OPERATOR |NAME |EST.ROWS|EST.TIME(us)|
-------------------------------------------------------------------------
|0 |NESTED-LOOP JOIN | |1 |28 |
|1 |├─TABLE FULL SCAN |c |1 |4 |
|2 |└─SUBPLAN SCAN |v1 |1 |23 |
|3 | └─SCALAR GROUP BY | |1 |23 |
|4 | └─DISTRIBUTED TABLE RANGE SCAN|s(i_course)|1 |23 |
=========================================================================
Outputs & filters:
-------------------------------------0 - output([v1.avg_score], [v1.max_score]), filter(nil), rowset=16conds(nil), nl_params_([c.course_id(:0)]), use_batch=true1 - output([c.course_id]), filter([c.course_name = 'math']), rowset=16access([c.course_id], [c.course_name]), partitions(p0)is_index_back=false, is_global_index=false, filter_before_indexback[false],range_key([c.course_id]), range(MIN ; MAX)always true2 - output([v1.avg_score], [v1.max_score]), filter(nil), rowset=16access([v1.avg_score], [v1.max_score])3 - output([T_FUN_SUM(s.score) / cast(T_FUN_COUNT(s.score), DECIMAL(20, 0))], [T_FUN_MAX(s.score)]), filter(nil), rowset=16group(nil), agg_func([T_FUN_MAX(s.score)], [T_FUN_SUM(s.score)], [T_FUN_COUNT(s.score)])4 - output([s.score]), filter(nil), rowset=16access([GROUP_ID], [s.__pk_increment], [s.score]), partitions(p0)is_index_back=true, is_global_index=false,range_key([s.course_id], [s.__pk_increment]), range(MIN,MIN ; MAX,MAX)always true,range_cond([s.course_id = :0])
從執行計劃可以看出,需要通過c表驅動v1執行,只能走Nested Loop Join,在c表數據量很小的情況下執行的效率很高。但當c表數據量很大,score表中的數據很少時,執行的效率就會很差。顯然Lateral關鍵字限制了Join Order的枚舉順序,因此需要優化器通過預設的改寫規則去除LATERAL關鍵字,提升計劃的枚舉空間。
-- Q5
select v1.avg_score, v1.max_scorefrom course c, (select course_id, avg(score) avg_score, max(score) max_score from score s group by course_id ) v1where course_name = 'math' and v1.course_id = c.course_id;
=======================================================
|ID|OPERATOR |NAME|EST.ROWS|EST.TIME(us)|
-------------------------------------------------------
|0 |MERGE JOIN | |1 |9 |
|1 |├─TABLE FULL SCAN |c |1 |4 |
|2 |└─SORT | |1 |5 |
|3 | └─SUBPLAN SCAN |v1 |1 |5 |
|4 | └─HASH GROUP BY | |1 |5 |
|5 | └─TABLE FULL SCAN|s |1 |4 |
=======================================================
Outputs & filters:
-------------------------------------0 - output([v1.avg_score], [v1.max_score]), filter(nil), rowset=16equal_conds([v1.course_id = c.course_id]), other_conds(nil)merge_directions([ASC])1 - output([c.course_id]), filter([c.course_name = 'math']), rowset=16access([c.course_id], [c.course_name]), partitions(p0)is_index_back=false, is_global_index=false, filter_before_indexback[false],range_key([c.course_id]), range(MIN ; MAX)always true2 - output([v1.avg_score], [v1.max_score], [v1.course_id]), filter(nil), rowset=16sort_keys([v1.course_id, ASC])3 - output([v1.course_id], [v1.avg_score], [v1.max_score]), filter(nil), rowset=16access([v1.course_id], [v1.avg_score], [v1.max_score])4 - output([s.course_id], [T_FUN_SUM(s.score) / cast(T_FUN_COUNT(s.score), DECIMAL(20, 0))], [T_FUN_MAX(s.score)]), filter(nil), rowset=16group([s.course_id]), agg_func([T_FUN_MAX(s.score)], [T_FUN_SUM(s.score)], [T_FUN_COUNT(s.score)])5 - output([s.course_id], [s.score]), filter(nil), rowset=16access([s.course_id], [s.score]), partitions(p0)is_index_back=false, is_global_index=false,range_key([s.__pk_increment]), range(MIN ; MAX)always true
優化器改寫后的SQL去除掉了Lateral關鍵字,增加了c表和v1的計劃枚舉空間,在score表數據量很小時,優化器還是會根據代價生成下面的計劃,通過v1來驅動c表執行,減少計劃執行時間。
=======================================================
|ID|OPERATOR |NAME|EST.ROWS|EST.TIME(us)|
-------------------------------------------------------
|0 |NESTED-LOOP JOIN | |1 |23 |
|1 |├─SUBPLAN SCAN |v1 |1 |5 |
|2 |│ └─HASH GROUP BY | |1 |5 |
|3 |│ └─TABLE FULL SCAN |s |1 |4 |
|4 |└─DISTRIBUTED TABLE GET|c |1 |18 |
=======================================================
Outputs & filters:
-------------------------------------0 - output([v1.avg_score], [v1.max_score]), filter(nil), rowset=16conds(nil), nl_params_([v1.course_id(:0)]), use_batch=true1 - output([v1.course_id], [v1.avg_score], [v1.max_score]), filter(nil), rowset=16access([v1.course_id], [v1.avg_score], [v1.max_score])2 - output([s.course_id], [T_FUN_SUM(s.score) / cast(T_FUN_COUNT(s.score), DECIMAL(20, 0))], [T_FUN_MAX(s.score)]), filter(nil), rowset=16group([s.course_id]), agg_func([T_FUN_MAX(s.score)], [T_FUN_SUM(s.score)], [T_FUN_COUNT(s.score)])3 - output([s.course_id], [s.score]), filter(nil), rowset=16access([s.course_id], [s.score]), partitions(p0)is_index_back=false, is_global_index=false,range_key([s.__pk_increment]), range(MIN ; MAX)always true4 - output(nil), filter([c.course_name = 'math']), rowset=16access([GROUP_ID], [c.course_name]), partitions(p0)is_index_back=false, is_global_index=false, filter_before_indexback[false],range_key([c.course_id]), range(MIN ; MAX),range_cond([:0 = c.course_id])
通過改寫可以提高SQL在不同場景下的適應性,減少差計劃產生的可能。但是如果業務不需要做這個改寫,可以通過NO_DECORRELATE這個hint來禁用對Lateral Derived Table的改寫。
-- Q4'
select /*+ NO_DECORRELATE */ v1.avg_score, v1.max_scorefrom course c, lateral (select avg(score) avg_score, max(score) max_score from score s where s.course_id = c.course_id) v1where course_name = 'math';
總結
Lateral語法打開了之前同一From子句不能引用前面表的列的限制,在很多情況下都可以?來加速SQL執行,或者可以使SQL更容易理解。