|
10.2 只读事务7 n$ F: T( k( ^
只读事务(SELECT语句),经过词法分析、语法分析,预处理后,转化为逻辑
( _. y- u' x" v) k) Z/ d查询计划和物理查询计划。以SQL语句select c1,c2 from t1 where id=1 group by c15 P4 I3 T! }( M" B F
order by c2为例,MergeServer收到该语句后将调用ObSql类的静态方法
2 L2 d% V4 W- hdirect_execute,执行步骤如下:
$ m+ a8 }4 G- f! ^ c* ~, s$ U- X5 [1)调用flex、bison解析SQL语句生成一个语法树。
$ z: @+ W% s+ @# p" e2)解析语法树,生成逻辑执行计划ObSelectStmt。ObSelectStmt结构中记录了1 S# S- T+ ~( |0 m1 j3 O' l* \2 F: q! U
SQL语句扫描的表格名(t1),投影列(c1,c2),过滤条件(id=1),分组列
w' v4 @' k, n3 s) h$ j(c1)以及排序列(c2)。& U7 l( ~) t8 e5 t
3)根据逻辑执行计划生成物理执行计划。ObSelectStmt只是表达了一种意图,: s/ |" x( h7 b( d4 q
但并不知道实际如何执行,ObTransformer类的generate_physical_plan将ObSelectStmt转. C: C; f1 R5 b
化为物理执行计划。
4 K7 d: E% b2 w+ l0 p, e0 t逻辑查询计划的改进以及物理查询计划的选择,即查询优化器,是关系数据库; \7 y/ B0 o& I5 R5 P; _# N
最难的部分,OceanBase目前在这一部分的工作不多。因此,本节不会涉及太多关于
: a Q( F6 @" g7 b2 r q/ \5 f如何生成物理查询计划的内容,下面仅以两个例子说明OceanBase的物理查询计划。% l6 K+ S' @, W+ m3 _
例10-1 假设有一个单表SQL语句如图10-2所示。3 m1 S8 O/ ^# z* B
图 10-2 单表物理查询计划示例1 C- v; I9 H7 `
单表SQL语句执行过程如下:
% a0 ~( Q: m, c6 A1)调用TableScan操作符,读取子表t1中的数据,该操作符还将执行投影
! I+ w/ `! j9 f) b$ S! o(Project)和过滤(Filter),返回的结果只包含c3=10的数据行,且每行只包含c1、
2 ~0 A6 p. [$ `- [0 Sc2、c3三列。
* p) O+ k/ {! T3 u V) J0 M! q e' J2)调用HashGroupBy操作符(假设采用基于哈希的分组算法),按照c1对数据
" C k5 Q$ @1 \5 X分组,同时计算每个分组内c2列的总和。
$ c! H$ w; G6 E6 b3)调用Filter操作符,过滤分组后生成的结果,只返回上一层sum(c2)>=10的
- r$ u+ w6 L7 a' `, \行。3 N! Z2 M/ r/ f) C# F) C& t8 F
4)调用Sort操作符将结果按照c1排序。
9 t7 a3 V" X$ D& v* d5)调用Project操作符,只返回c1和sum(c2)这两列数据。: t3 `3 L- w, X: s! V1 i0 w( s
6)调用Limit操作符执行分页操作,只返回前20条数据。
* @2 z; H1 ]+ i3 n5 R/ f9 K4 y例10-2 假设有一个需要联表的SQL语句如图10-3所示。
4 T8 [! S) h) m" M图 10-3 多表物理查询计划示例( K" m8 d' x* A3 G' S3 s, k$ V3 ^; d
多表SQL语句执行过程如下:& M9 k x C+ L$ p; g; M: x7 @
1)调用TableScan分别读取t1和t2的数据。对于t1,使用条件c3=10对结果进行过3 _% k0 C9 d- |/ J
滤,t1和t2都只需要返回c1,c2,c3这三列数据。
/ h4 u) ^# j- B$ I* s* V' ` K! H2)假设采用基于排序的表连接算法,t1和t2分别按照t1.c2和t2.c2排序后,调用8 T9 s3 `/ [ f5 i9 Y
Merge Join运算符,以t1.c2=t2.c2为条件执行等值连接。
% w6 l5 P( { Z3)调用HashGroupBy运算符(假设采用基于哈希的分组算法),按照t1.c1对数/ c5 i0 H( o; G4 h
据分组,同时计算每个分组内t2.c3列的总和。
1 A, t; ?2 P9 u4 n8 d7 n) S4)调用Filter运算符,过滤分组后的生成的结果,只返回上一层sum(t2.c3)>) V2 _- ^" z3 E1 e
=10的行。
O; Z/ ]2 l6 s$ X* A5)调用Sort操作符将结果按照t1.c1排序。
& P) R% Z# p# V6)调用Project操作符,只返回t1.c1和sum(t2.c3)这两列数据。; |* E" q3 P7 P2 q/ P+ v
7)调用Limit操作符执行分页操作,只返回前20条数据。/ t; M( M1 C6 t2 h" G
10.2.1 物理操作符接口
" B3 D$ [0 y' w9.4.2节介绍一期分布式存储引擎中的迭代器接口为ObIterator,通过它,可以将2 ^$ ]' j/ a8 M6 c& u$ F
读到的数据以cell为单位逐个迭代出来。然而,数据库操作总是以行为单位的,因1 B" b) \0 x F; R! Y1 Z
此,二期实现数据库功能层时考虑将基于cell的迭代器修改为基于行的迭代器。; k; v9 f8 I; _% P
行迭代器接口如下:
6 ?( k- `1 ~( c" ]0 u* h' J4 m$ w" r//ObRow表示一行数据内容
1 C0 X5 M2 ~8 j5 q3 k1 Gclass ObRow
( p" t/ G5 M# {- B% D: i( f{
# [ S% R. [- z" [$ `2 `6 _9 Apublic:) A- w! n# B7 d" p0 ~5 Y
//根据表ID以及列ID获得指定cell
- _- _" q2 N! {1 \//@param[in]table_id表格ID$ u( m D: l$ u" e, L
//@param[in]column_id列ID
; O& ?3 g) E8 B) U o//@param[out]cell读到的cell
r# Z+ p5 G" N$ a, d0 Hint get_cell(const uint64_t table_id,const uint64_t column_id,ObObj*&cell);
+ X9 M* F- B) @. e* D. D f I//获取第cell_idx个cell6 Q4 _$ [- w8 {
int raw_get_cell(const int64_t cell_idx,const ObObj*&cell,uint64_t&table_id,. D6 w$ j9 K! o8 L+ ]0 F
uint64_t&column_id);
7 y( C! W7 Q/ j. E, z1 ]9 g' S1 N//获取本行的列数9 f, r# W& b9 K: l
int64_t get_column_num()const;$ z: T |" n5 f) [3 O/ c0 F$ d
};7 D7 B9 r4 |% F$ b
每一行数据(ObRow)包括多个列,每个列的内容包括所在的表
$ Y1 J8 C4 Y+ I z9 b7 X MID(table_id),列ID(column_id)以及列内容(cell)。ObRow提供两种访问方+ b& q) {* D9 ~+ J3 c
式:根据table_id和column_id随机访问某个列,以及根据列下标(cell_idx)获取某个6 Z; J5 o$ x+ U8 G/ J/ E
指定列。
$ o; {+ [5 x5 U物理运算符接口如下:% @+ t0 p) ?8 Z5 ~
//物理运算符接口
1 {0 M6 W0 I0 k6 X. c9 T+ Eclass ObPhyOperator
) r, r7 ` W; M, t6 p; x' `0 a4 o{
& {& g5 m7 D4 s2 Q+ `public:8 q6 j i, [; t2 g
//添加子运算符,所有非叶子节点物理运算符都需要调用该接口5 w+ P7 P! Q4 X) T |1 a, S
virtual int set_child(int32_t child_idx,ObPhyOperator&child_operator);
5 E( I" n, W4 i! j' B$ b0 d//打开物理运算符。申请资源,打开子运算符等 {9 s: d6 i% `; b% w0 {
virtual int open()=0;
5 s( E9 K; p3 ~9 r" W' I$ A- m//关闭物理运算符。释放资源,关闭子运算符等% a, c$ L8 N! Z
virtual int close()=0;
4 @% v7 L- B5 c1 d7 ?//获得下一行数据内容
+ Q- E& |* f" E+ x1 E ]$ O//@param[out]row下一行数据内容的引用
% O3 ]% n' t! s K! p//@return返回码,包括成功、迭代过程中出现错误以及迭代完成# F$ p; y: k( _. V+ ^
virtual int get_next_row(const ObRow*&row)=0;# ^3 R* b+ ~" H6 T
};
/ n9 y$ G# p: S$ K) k8 NObPhyOperator每次获取一行数据,使用方法如下:. P8 Q7 w8 S* Q
ObPhyOperator root_operator=root_operator_;//根运算符( C& [* g5 `6 u6 t% P, L [
root_operator->open();
- X% ]3 H7 ~4 e8 f; ]' lObRow*row=NULL;
& H" W: `. v3 V% g" e$ N- _while(OB_SUCCESS==root_operator->get_next_row(row))& H) V6 ~, [0 V, u
{
& \5 F& Z2 {+ y$ oOutput(row);//输出本行
/ V9 ]5 J# c. m* o: A& F# _}
5 Q" R [) `0 j6 A9 ~root_operator->close();6 I' f! v- }5 E- H+ x
为什么ObPhyOperator类中有一个set_child接口呢?这是因为所有的物理运算符构
& C. g( Q4 ` {6 K' t7 |- }) {成一个树,每个物理运算的输出结果都可以认为是一个临时的二维表,树中孩子节
1 G- P5 o: Y& K- r6 S/ Y1 A' N/ b点的输出总是作为它的父亲节点的输入。例10-1中,叶子节点为一个TableScan类型' i- b! K$ z3 \1 b7 j5 m; u
的物理运算符(称为table_scan_op),它的父亲节点为一个HashGroupBy类型的物理- P2 G. N3 E! M0 V$ \- y6 ?: m* P
运算符(称为hash_group_by_op),接下来依次为Filter类型物理运算符filter_op,Sort
) S H$ s& {% Z! u: j类型物理运算符sort_op,Project类型物理运算符project_op,Limit类型物理运算符. d0 M2 m* W* |' z# R
limit_op。其中,limit_op为根运算符。那么,生成物理运算符时将执行如下语句:
- X- @; N- c4 I9 k" nlimit_op->set_child(0,project_op);
" ]4 d5 [3 p& G# h3 Hproject_op->set_child(0,sort_op);
" W/ W) D. {8 j- z7 P9 o+ usort_op->set_child(0,filter_op);
! L j9 E- ?0 e6 y/ Efilter_op->set_child(0,hash_group_by_op);
$ a2 F+ X3 q5 d3 x- W" } A2 yhash_group_by_op->set_child(0,table_scan_op);
7 J/ ~: K9 E' Z7 rroot_op=limit_op;
& G1 t! m8 K) L- f& r; l* nSQL最终执行时,只需要迭代root_op(即limit_op)就能够把需要的数据依次迭
$ d# d6 {1 Y* |; _ q1 W7 D$ P代出来。limit_op发现前一批数据迭代完成则驱动下层的project_op获取下一批数据,
: A9 W; N/ v& R }) c _project_op发现前一批数据迭代完成则驱动下层的sort_op获取下一批数据。以此类
0 h; O$ m% v! t; q推,直到最底层的table_scan_op不断地从原始表t1中读取数据。 i6 S! m& K1 e
10.2.2 单表操作
( F0 B# w3 o( [/ t6 r4 T# ?; F单表相关的物理运算符包括:
5 O6 k' a g( B( e6 K●TableScan:扫描某个表格,MergeServer将扫描请求发给请求的各个子表所在
) p, q4 Y# ]5 x/ _的ChunkServer,并将ChunkServer返回的结果按照子表范围拼接起来作为输出。如果
/ U5 ?, D$ O# R请求涉及多个子表,TabletScan可由多台ChunkServer并发执行。! P8 W2 |* P' q% Z5 O0 d+ y5 S
●Filter:针对每行数据,判断是否满足过滤条件。
: c/ [' m; b. W6 r; Z- ]2 B4 t$ v●Projection:对输入的每一行,根据定义的输出表达式,计算输出结果行。
3 @ J2 ^1 ]) H$ o●GroupBy:把输入数据按照指定列进行聚集,对聚集后的每组数据可以执行计
' a0 L7 M, I# E4 X数(count)、求和(sum)、计算最小值(min)、计算最大值(max)、计算平均值
0 J" L/ W: }# d0 X(avg)等聚集操作。
7 M/ k0 Q7 H3 R' ? i% m6 ~0 g●Sort:对输入数据进行整体排序,如果内存不够,需要使用外排序。5 r- a* \3 C" P4 A1 K5 k3 k
●Limit(offset,count):返回行号在[offset,offset+count)范围内的行。
8 c x& F: F; E% ~9 R●Distinct:消除某些列相同的重复行。! v% S7 F1 e/ `" D: }0 u
GroupBy、Distinct物理操作符可以通过基于排序的算法实现,也可以通过基于哈! d' ]) z0 J9 n5 M
希的算法实现,分别对应HashGroupBy和MergeGroupBy,以及HashDistinct和) |% X1 E+ o% [! }( S# N
MergeDistinct。下面分别讨论排序算法和哈希算法。' C7 Q$ h' b/ K$ |0 y
1.排序算法( l4 C/ P, D9 q Z0 D* U: {" g9 B
MergeGroupBy、MergeDistinct以及Sort都需要使用排序算法。通用的<key,value
* c: Y% S5 P9 P) B>排序器可以分为两个阶段:
- f9 o4 |& n# O5 m* d●数据收集:在数据收集阶段,调用者将<key,value>对依次加入到排序器。如
) S8 F9 ]( n& T) r% r4 y, a! r果数据总量超过排序器的内存上限,需要首先将内存中的数据排好序,并存储到外
5 x. M( T+ O* z- V b1 I2 y2 o$ B部磁盘中。8 [2 f6 K$ ]1 O' H
●迭代输出:迭代第一行数据时,内存中可能有一部分未排序的数据,磁盘中也
2 ], Q; K, O1 A; S5 i可能有几路已经排好序的数据。因此,首先将内存中的数据排好序。如果数据总量, R# p8 W* D: U z, H6 o
不超过排序器内存上限,那么将内存中已经排好序的数据按行迭代输出(内排% @& B. ^* w7 h [! M$ ?
序);否则,对内存和磁盘中的部分有序数据执行多路归并,一边归并一边将结果
" U: X/ Z1 c$ K5 e% J, [迭代输出。
/ \7 R1 F0 o1 Y! q, [2 ]2.哈希算法
* p+ i- P+ a, U7 s% X" M& x* F( cHashGroupBy以及HashDistinct都需要使用哈希算法。假设需要对<key,value>对
) D& l7 o* h( I1 z& p. i按照key分组,那么首先使用key计算哈希值K,并将这个<key,value>对写入到第K个. X# Z) n9 Y0 o- \6 S
桶中。不同的key可能对应相同的哈希桶,因此,还需要对每个哈希桶内的<+ X% d( f: U9 t2 {
key,value>对排序,这样才能使得key相同的元组能够连续迭代出来。哈希算法的难& [/ o- H6 ?( U$ A$ k6 v
点在于数据总量超过内存上限的处理,由于篇幅有限,请自行思考。
. j) K$ p4 y/ V10.2.3 多表操作' T) ~" o- x1 C3 I k7 F( w9 a
多表相关的物理操作符主要是Join。最为常见的Join类型包括两种:内连接$ g z/ m. @2 ]) `
(Inner Join)和左外连接(Left Outer Join),而且基本都是等值连接。如果需要连接$ ~/ _# k! A. b: \) `9 }' g$ W" ?
多张表,可以先连接前两张表,再将前两张表连接生成的结果(相当于一张临时
8 f6 S9 i1 Q2 T: r3 {表)与第三张表格连接,以此类推。( L2 J l. z8 v$ Y2 a2 d7 p0 G
两张表实现等值连接方式主要分为两类:基于排序的算法(MergeJoin)以及基1 o/ z- s0 Y5 Z
于哈希的算法(HashJoin)。对于MergeJoin,首先使用Sort运算符分别对输入表格预& D% d) v2 _1 ?4 W# U. A d
处理,使得两张输入表都在连接列上排好序,接着按顺序迭代两张输入表,合并连
( ?% r. a0 B0 H5 r7 l( M7 Q接列相同的行并输出;对于HashJoin,首先根据连接列计算哈希值K,并分别将两张1 E9 d4 U- H& K% t/ F
输入表格的数据写入到第K个桶中。接着,对每个哈希桶按照连接列排序。最后,依
4 k4 q% ?8 v* E+ s+ R( [次对每个哈希桶合并连接列相同的行并输出。
6 X8 N0 ?9 F$ L子查询分为两种:关联子查询和非关联子查询,其中比较常用的是使用IN子句3 D$ u6 v, J8 l
的非关联子查询。举例如下:
! W- ]4 m4 y5 W3 j7 T( g* w例10-3 假设有两张表格:item(商品表,包括商品号item_id,商品名2 W8 y9 Y. U7 t6 q" k7 ?: _4 ^
item_name,分类号category_id,),category(类别表,包括分类号category_id,分$ H9 b7 O' W7 ~5 [6 x8 |) D
类名category_name)。如果需要查询分类号出现在category表中商品,可以采用图10-
, [/ t( z# f7 [ g' H& _* X; J4左边的IN子查询,而这个子查询将被自动转化为图10-4右边的等值连接。如果
2 q: h* { s6 G8 Ocategory表中的category_id列有重复,表连接之前还需要使用distinct运算符来删除重
U# [. L% Q i/ @* v, E复的记录。0 P2 ~$ ^( v% o9 q) w4 j
图 10-4 IN子查询转化为等值连接3 d: f( s% Y% R" C9 }( P
例10-4 例10-3中,如果category表只包含category_id为1~10的记录,那么,可; s; r- T, u/ b' \3 B
以将IN子查询写成图10-5中的常量表达式。5 @1 H/ X+ b9 y6 A$ p( X7 ^
图 10-5 IN子查询转化为常量表达式
. ]4 M) h$ [! m) t% m转化为常量表达式后,MergeServer执行SQL计算时,可以将IN后面的常量列表. o, o4 Z6 ]$ V9 F- w
发送给ChunkServer,ChunkServer只返回category_id在常量列表中的商品记录,而不是9 s& t h5 _, b8 S* K- t
将所有的记录返回给MergeServer过滤,从而减少二者之间传输的数据量。
5 j Z8 P: [7 `# H- c( HOceanBase多表操作做得还很粗糙,例如不支持嵌套连接(Nested Loop Join),
# h: w; Y. Q8 Q9 S* l3 k3 [不支持非等值连接,不支持查询优化等,后续将在合适的时间对这一部分代码进行 h$ o5 @& i7 A) y
重构。8 ]1 _6 r, b3 W8 O$ ]* ^7 Y
10.2.4 SQL执行本地化
* J, {, i& O$ I; _/ t3 HMergeServer包含SQL执行模块MS-SQL,ChunkServer也包含SQL执行模块CS-
& E: N# C. t9 @7 ?7 U0 MSQL,那么,如何区分二者的功能呢?多表操作由MergeServer执行,对于单表操4 l7 }8 u V% V: ^$ R% k
作,OceanBase设计的基本原则是尽量支持SQL计算本地化,保持数据节点与计算节
) I* T( _9 p+ h6 n7 t; I; [点一致,也就是说,只要ChunkServer能够实现的操作,原则上都应该由它来完成。$ M# B' L0 x6 j1 K, G
●TableScan:每个ChunkServer扫描各自子表范围内的数据,由MergeServer合并
) ], _$ |! Y. F2 {( Q: M, ]; lChunkServer返回的部分结果。' P7 I& y9 M# i
●Filter:对基本表的过滤集成在TableScan操作符中,由ChunkServer完成。对分( M* n/ |" j# D& F( {
组后的结果执行过滤(Having)集成在GroupBy操作符中,一般情况下由MergeServer
- i( ]/ j" T7 F& I完成;但是,如果能够确定每个分组的所有数据行只属于同一个子表,比如SQL请求
; I v) m& h8 z) B! Z, N只涉及一个tablet,那么,分组以及分组后的过滤操作符可以由ChunkServer完成。
1 ~- P6 b& p- b●Projection:对基本表的投影集成在TableScan操作符中,由ChunkServer完成,
+ L3 {' z% d: }; e$ K% z对最终结果的投影由MergeServer完成。; {0 H) b7 [3 ]
●GroupBy:如果SQL读取的数据只在一个子表上,那么由该子表所在的3 y- f* M- }- b' Y: a
ChunkServer完成分组操作;否则,每台ChunkServer各自完成部分数据的分组操作,! Z* v6 b1 e- J
执行聚合运算后得到部分结果,再由MergeServer合并所有ChunkServer返回的部分结
. ?: `. h- R+ f5 B+ A5 Q8 w( r: [/ m果,对于属于同一个分组的数据再次执行聚合运算。某些聚合运算需要做特殊处
; @% L8 ]- y2 K+ u5 w理,比如avg,需要转化为sum和count操作发送给ChunkServer,MergeServer合并
% p7 z) b6 u# fChunkServer返回的部分结果后计算出最终的sum和count值,并通过sum/count得到avg8 c4 P9 T9 V0 R2 e7 u% n
的最终结果。
* n% L6 z4 t% z●Sort:如果SQL读取的数据只在一个子表上,那么由该子表所在的ChunkServer9 z# g4 s6 Q" Z d+ g! s
完成排序操作;否则,每台ChunkServer各自完成部分数据的排序,并将排好序的部
! X) H0 b8 h& O7 B分数据返回MergeServer,再由MergeServer执行多路归并。4 E3 X1 z# q0 O0 _
●Limit:Limit操作一般由MergeServer完成,但是,如果请求的数据只在一个子9 Q, S5 g9 W8 y9 N" z: N
表上,可以由ChunkServer完成,这往往会大大减少MergeServer与ChunkServer之间传3 F0 F* t' [) c$ P* c, G4 h
输的数据量。; H4 w3 \0 e2 {
●Distinct:Distinct与GroupBy类似。ChunkServer先完成部分数据的去重,再由
; K- ?# B2 L& p. XMergeServer进行整体去重。
4 o! F3 m+ @# x6 s* _+ S7 ]0 _2 b例10-5 图10-2中的SQL语句为"select c1,sum(c2)from t1 where c3=10 group
$ T1 ?" ?9 T7 cby c1 having sum(c2)>=10 order by c1 limit 0,20"。执行步骤如下:
8 E' G1 x6 c% K* H2 Z/ Y" W4 r1)ChunkServer调用TableScan操作符,读取子表t1中的数据,该操作符还将执行
{& A4 o# \1 c0 ^* Y6 `投影(Project)和过滤(Filter),返回的结果只包含c3=10的数据行,且每行只包含" C1 ]7 c; N4 V0 b. q
c1、c2、c3三列。0 e, A( x5 ^. {0 }; Z f
2)ChunkServer调用HashGroupBy操作符(假设采用基于哈希的分组算法),按$ D- m! F4 v, d8 G! D6 t1 W
照c1对数据分组,同时计算每个分组内c2列的总和sum(c2)。
6 z- R* v; X$ m. F) ^- l3)每个ChunkServer将分组后的部分结果返回MergeServer,MergeServer将来自不
& C7 \# O5 s# I同ChunkServer的c1列相同的行合并在一起,再次执行sum运算。2 X% a( z0 l* H0 g; ^
4)MergeServer调用Filter操作符,过滤第3)步生成的最终结果,只返回. p; H6 b( K7 a
sum(c2)>=10的行。' r1 V0 a7 G) J& e, N6 z
5)MergeServer调用Sort操作符将结果按照c1排序。4 I, Y, b, f/ t: ]+ `; f7 q
6)MergeServer调用Project操作符,只返回c1和sum(c2)这两列数据。; B$ w) \$ R" x7 K% w( w3 m5 P
7)MergeServer调用Limit操作符执行分页操作,只返回前20条数据。
+ Q7 v2 U7 r) V* F. q当然,如果能够确定请求的数据全部属于同一个子表,那么,所有的物理运算" e" `: h5 j3 d5 h
符都可以由ChunkServer执行,MergeServer只需要将ChunkServer计算得到的结果转发$ o9 c7 m) f, S& T9 D0 B
给客户端。" a5 ^& p3 F2 [, N
/ \9 G1 d& c3 J* B% N3 p3 |' \+ k. C7 t1 B' B' L4 I- a# _+ a
|
|