|
10.2 只读事务
/ c; J) h+ C& u" e% T只读事务(SELECT语句),经过词法分析、语法分析,预处理后,转化为逻辑+ }: N4 W# d" r: w/ y/ M( \, |
查询计划和物理查询计划。以SQL语句select c1,c2 from t1 where id=1 group by c1
3 m2 L5 B+ X( b8 {2 n; v l) s1 Norder by c2为例,MergeServer收到该语句后将调用ObSql类的静态方法
9 t* i7 ^7 s' r' q$ q7 M+ }) Fdirect_execute,执行步骤如下:
* `4 o |7 p$ M8 L% Q1)调用flex、bison解析SQL语句生成一个语法树。; h& e7 F) x3 x
2)解析语法树,生成逻辑执行计划ObSelectStmt。ObSelectStmt结构中记录了" i6 Q9 ?) \8 l2 P% U t t
SQL语句扫描的表格名(t1),投影列(c1,c2),过滤条件(id=1),分组列
% h5 c2 z, [) K3 ~(c1)以及排序列(c2)。
$ L, }4 [+ H! C% t: T3)根据逻辑执行计划生成物理执行计划。ObSelectStmt只是表达了一种意图,# {! X1 s% q: E7 b. g
但并不知道实际如何执行,ObTransformer类的generate_physical_plan将ObSelectStmt转" z7 f: \+ h/ H' P6 @( A
化为物理执行计划。
8 E: X9 A# D+ b逻辑查询计划的改进以及物理查询计划的选择,即查询优化器,是关系数据库
5 }) ]. s2 A6 a9 W4 j! V' @9 h6 B最难的部分,OceanBase目前在这一部分的工作不多。因此,本节不会涉及太多关于2 h5 r/ M# R6 B6 K3 H
如何生成物理查询计划的内容,下面仅以两个例子说明OceanBase的物理查询计划。$ }- l' Q& R6 `, j" u
例10-1 假设有一个单表SQL语句如图10-2所示。
8 L: X3 _% G; X8 s. `6 P% G图 10-2 单表物理查询计划示例
! S, P8 q8 F% T# X5 s单表SQL语句执行过程如下:
# t( ~1 M4 g$ E/ } G/ b1)调用TableScan操作符,读取子表t1中的数据,该操作符还将执行投影
# s9 [/ Q1 z5 p) F5 M(Project)和过滤(Filter),返回的结果只包含c3=10的数据行,且每行只包含c1、
% J6 Z" S! j( w( T( Mc2、c3三列。
* F( N2 |3 v$ ]# G3 T2)调用HashGroupBy操作符(假设采用基于哈希的分组算法),按照c1对数据$ E1 Q9 \" Q$ Z" h1 \
分组,同时计算每个分组内c2列的总和。
; b1 C' m; g- w' Z. N3)调用Filter操作符,过滤分组后生成的结果,只返回上一层sum(c2)>=10的; _- l6 k5 l6 Z& b
行。
/ w! p# c6 O4 u5 j4)调用Sort操作符将结果按照c1排序。
- x3 U2 w6 K+ @# C$ H' @5)调用Project操作符,只返回c1和sum(c2)这两列数据。4 y' u1 X7 ?: J2 ?
6)调用Limit操作符执行分页操作,只返回前20条数据。/ Y: |9 @& h! a& H) S
例10-2 假设有一个需要联表的SQL语句如图10-3所示。
+ _4 d, u6 U! |, [4 w* L图 10-3 多表物理查询计划示例2 n) A" N! G' i
多表SQL语句执行过程如下:! ]: N9 E8 j/ @/ J, Z
1)调用TableScan分别读取t1和t2的数据。对于t1,使用条件c3=10对结果进行过
$ Y1 y6 J& B6 j j0 q6 j9 |7 `0 D1 r4 k滤,t1和t2都只需要返回c1,c2,c3这三列数据。: O6 w! ~9 @; u1 Q0 q
2)假设采用基于排序的表连接算法,t1和t2分别按照t1.c2和t2.c2排序后,调用+ k7 Q/ e* p2 l# D1 P) M
Merge Join运算符,以t1.c2=t2.c2为条件执行等值连接。
+ R9 y1 n$ v8 e K6 i( l! B4 I3)调用HashGroupBy运算符(假设采用基于哈希的分组算法),按照t1.c1对数
; \8 ]) m9 C2 u* z# r1 p据分组,同时计算每个分组内t2.c3列的总和。
+ f( x- B5 l; a% @3 U. h; U4)调用Filter运算符,过滤分组后的生成的结果,只返回上一层sum(t2.c3)>- o/ P% _: f% j& ^
=10的行。
6 J1 ]+ h/ T2 b3 u U5)调用Sort操作符将结果按照t1.c1排序。
* [ X; q4 D8 C9 h6)调用Project操作符,只返回t1.c1和sum(t2.c3)这两列数据。
. x% m z$ b6 d0 [* U7)调用Limit操作符执行分页操作,只返回前20条数据。
/ m7 d& F. t, ^, B' v10.2.1 物理操作符接口
6 T* {7 o n, h" v% Q( z9.4.2节介绍一期分布式存储引擎中的迭代器接口为ObIterator,通过它,可以将
% A. @# |, Y9 b% W7 N+ G读到的数据以cell为单位逐个迭代出来。然而,数据库操作总是以行为单位的,因/ S! j5 ~- p0 ]( B' c
此,二期实现数据库功能层时考虑将基于cell的迭代器修改为基于行的迭代器。
2 [8 m+ e5 x0 @* b5 \8 \, j行迭代器接口如下:* ~+ X8 m/ O( n: I, O
//ObRow表示一行数据内容
( z3 X4 Q% Z. F: w) T6 Q" Fclass ObRow" L2 t: ]7 C! t/ H6 D% @
{) @1 W6 F% U: B) Z/ S
public:* P8 x$ F! K( z% X ~ V7 }, j
//根据表ID以及列ID获得指定cell3 j$ W# t% W- s' G4 r
//@param[in]table_id表格ID
9 S' ?( S, F2 a7 o$ N9 o* q% a//@param[in]column_id列ID
4 G, F/ g) f* J9 a' @//@param[out]cell读到的cell: a% i+ M! _ b6 `" d( }3 e
int get_cell(const uint64_t table_id,const uint64_t column_id,ObObj*&cell);/ |; z. X* p3 c- e
//获取第cell_idx个cell+ l: C4 V- Y( f9 r+ ^
int raw_get_cell(const int64_t cell_idx,const ObObj*&cell,uint64_t&table_id,, W0 r5 a& |( J; H- i& R& @, J
uint64_t&column_id);4 y/ k! j( x4 }- f9 a& T3 J7 f
//获取本行的列数
' I$ `" u8 I: @9 N( Bint64_t get_column_num()const;
3 M- E8 \6 Z* i2 k8 ~- v/ S% R};
; D9 o7 u/ h/ o4 u0 V每一行数据(ObRow)包括多个列,每个列的内容包括所在的表
& x; r! `5 [' @* ?% S% ?0 S4 BID(table_id),列ID(column_id)以及列内容(cell)。ObRow提供两种访问方
8 S. K# E+ f4 C; x( @& \# r式:根据table_id和column_id随机访问某个列,以及根据列下标(cell_idx)获取某个
- P0 S, V$ y4 I指定列。- D0 H1 Y% o6 I
物理运算符接口如下:$ b T8 |, M5 b
//物理运算符接口
2 X( T2 m* |, n5 b$ U8 ?& k& R* Xclass ObPhyOperator
9 n$ l- s. r! A, U: B3 ~{
2 C9 n5 Z \( M9 h0 b. n5 {public:' i8 i! Q! k5 U+ r
//添加子运算符,所有非叶子节点物理运算符都需要调用该接口8 h. S n% F5 B8 F
virtual int set_child(int32_t child_idx,ObPhyOperator&child_operator);
' @; i5 R4 k" W% A# g7 D//打开物理运算符。申请资源,打开子运算符等. x3 q# Y- l* }0 U, [$ x
virtual int open()=0;. |. x9 |# F, x* L; k+ o+ }
//关闭物理运算符。释放资源,关闭子运算符等
9 W0 E, e9 X& q7 Dvirtual int close()=0;
& p: I, j1 o: l4 _//获得下一行数据内容
' }+ x2 T" Q, ]//@param[out]row下一行数据内容的引用
1 ~ h4 _9 M* }. h2 M//@return返回码,包括成功、迭代过程中出现错误以及迭代完成
( W9 M1 i) q/ k" t0 F# v# uvirtual int get_next_row(const ObRow*&row)=0;, ^' i% q: L+ m* y8 O$ N9 u
};
8 V+ l4 u i. T! D$ l/ V3 J* T: AObPhyOperator每次获取一行数据,使用方法如下:6 c+ g1 v) ?- @$ D; K
ObPhyOperator root_operator=root_operator_;//根运算符3 h3 ]/ A, M, L# h' s
root_operator->open();
$ G. X$ O5 a" K5 C3 C; E1 Y& n. WObRow*row=NULL;
$ t, | s' |4 z! `0 \8 y2 Rwhile(OB_SUCCESS==root_operator->get_next_row(row))
/ `( u Y7 w: O$ o( @0 O) r{7 Y% `% D4 [6 C9 ^* k2 [
Output(row);//输出本行
& j2 R+ f) j: ?- Q4 d}
: Y' K' ~, N4 h. O4 uroot_operator->close();
4 L, J- V/ j7 L为什么ObPhyOperator类中有一个set_child接口呢?这是因为所有的物理运算符构7 _: @) X2 _ a; ~
成一个树,每个物理运算的输出结果都可以认为是一个临时的二维表,树中孩子节
9 g5 c! y+ n a0 G. j* U5 M7 Z点的输出总是作为它的父亲节点的输入。例10-1中,叶子节点为一个TableScan类型: u7 O: `3 I) ~% `0 J8 y/ E2 w
的物理运算符(称为table_scan_op),它的父亲节点为一个HashGroupBy类型的物理* v: v6 t6 \" E
运算符(称为hash_group_by_op),接下来依次为Filter类型物理运算符filter_op,Sort
f/ C! Y9 k ^/ |! i* Q2 V类型物理运算符sort_op,Project类型物理运算符project_op,Limit类型物理运算符
4 ^& @ L! J( x$ F9 {6 `$ A9 Wlimit_op。其中,limit_op为根运算符。那么,生成物理运算符时将执行如下语句:+ m5 N5 R" ]6 @9 \* z) r
limit_op->set_child(0,project_op);! S2 v2 I6 a6 a# d8 G7 A+ O s
project_op->set_child(0,sort_op);5 |0 B( E. C5 g
sort_op->set_child(0,filter_op);0 J7 c- x" j# r; M2 q- ?
filter_op->set_child(0,hash_group_by_op);6 s+ a$ C; Z Z
hash_group_by_op->set_child(0,table_scan_op);) e; M0 J5 b9 Z9 L; O4 O# Z, B8 V
root_op=limit_op;
8 H4 {5 w/ C: K! @* }$ X, ZSQL最终执行时,只需要迭代root_op(即limit_op)就能够把需要的数据依次迭& `7 k9 ^ b" y
代出来。limit_op发现前一批数据迭代完成则驱动下层的project_op获取下一批数据,
: e% V3 V+ r2 r9 j( e: P, {) Xproject_op发现前一批数据迭代完成则驱动下层的sort_op获取下一批数据。以此类- W& N! j6 d& p1 ?8 L; W/ Q
推,直到最底层的table_scan_op不断地从原始表t1中读取数据。# c: i4 {& W- F2 i# N n
10.2.2 单表操作9 p3 i+ Q' i( @3 R/ ?
单表相关的物理运算符包括:7 r* u1 Q# \$ l- g: i' i* c
●TableScan:扫描某个表格,MergeServer将扫描请求发给请求的各个子表所在* ?! F. z6 p- N1 j+ \( Y
的ChunkServer,并将ChunkServer返回的结果按照子表范围拼接起来作为输出。如果' X( Q- \0 K8 T7 s; p
请求涉及多个子表,TabletScan可由多台ChunkServer并发执行。) _( e8 y* u; M8 G; C
●Filter:针对每行数据,判断是否满足过滤条件。& |1 z$ x) m6 f* F
●Projection:对输入的每一行,根据定义的输出表达式,计算输出结果行。
$ ]$ z% f( r" f, c& L; p●GroupBy:把输入数据按照指定列进行聚集,对聚集后的每组数据可以执行计# r7 `7 X4 U1 W: @
数(count)、求和(sum)、计算最小值(min)、计算最大值(max)、计算平均值: n. j. S1 F' E
(avg)等聚集操作。
& j. v7 o5 M0 h: x. K* Y●Sort:对输入数据进行整体排序,如果内存不够,需要使用外排序。
& k* z3 n3 `& o0 ^6 F( z●Limit(offset,count):返回行号在[offset,offset+count)范围内的行。
* A# X0 @& {2 q. S$ t●Distinct:消除某些列相同的重复行。: r- h w+ R! X* u
GroupBy、Distinct物理操作符可以通过基于排序的算法实现,也可以通过基于哈$ u' ^7 \4 l9 i S5 o
希的算法实现,分别对应HashGroupBy和MergeGroupBy,以及HashDistinct和# F, n `. q0 R
MergeDistinct。下面分别讨论排序算法和哈希算法。7 ~2 v9 |1 t4 u/ S& g: s5 r' N
1.排序算法
O* \: G b8 U5 J7 YMergeGroupBy、MergeDistinct以及Sort都需要使用排序算法。通用的<key,value
. |' _, ?; j/ k' t>排序器可以分为两个阶段:
. [9 q7 A% a% c' X9 Q0 ?& J●数据收集:在数据收集阶段,调用者将<key,value>对依次加入到排序器。如
% K! J4 r$ z) G, { C# S* x t* U果数据总量超过排序器的内存上限,需要首先将内存中的数据排好序,并存储到外' _/ a) l% p& Q( B, ~% O
部磁盘中。
& O1 x7 x& b$ Z●迭代输出:迭代第一行数据时,内存中可能有一部分未排序的数据,磁盘中也
* Q+ a* A. n; S' @可能有几路已经排好序的数据。因此,首先将内存中的数据排好序。如果数据总量! `' I0 D4 p0 N- d' `( _+ a: ^
不超过排序器内存上限,那么将内存中已经排好序的数据按行迭代输出(内排
5 u5 L1 x! @5 {序);否则,对内存和磁盘中的部分有序数据执行多路归并,一边归并一边将结果8 P' Z7 Z- q* G: q
迭代输出。; F4 D' z/ z: U
2.哈希算法
: p ]+ I: T: X- _HashGroupBy以及HashDistinct都需要使用哈希算法。假设需要对<key,value>对5 u8 N6 o! \1 Q) x8 s
按照key分组,那么首先使用key计算哈希值K,并将这个<key,value>对写入到第K个' Z" b3 Z* z2 t7 l4 k
桶中。不同的key可能对应相同的哈希桶,因此,还需要对每个哈希桶内的<
/ c8 O+ o1 }. Y$ [3 \, Y& [# Dkey,value>对排序,这样才能使得key相同的元组能够连续迭代出来。哈希算法的难8 C) D" g9 H3 X5 E7 s8 n2 ^
点在于数据总量超过内存上限的处理,由于篇幅有限,请自行思考。5 j0 T1 _/ l- | I' B8 F
10.2.3 多表操作& P+ w5 K) x: Z/ t# g! Z; R
多表相关的物理操作符主要是Join。最为常见的Join类型包括两种:内连接
+ n' t* f1 |9 y2 T5 U g& v4 U0 F(Inner Join)和左外连接(Left Outer Join),而且基本都是等值连接。如果需要连接/ S. n' F0 r% _$ @% K
多张表,可以先连接前两张表,再将前两张表连接生成的结果(相当于一张临时0 b$ ~# Q0 V4 B. c) X, P
表)与第三张表格连接,以此类推。
5 H2 M( b: o9 z两张表实现等值连接方式主要分为两类:基于排序的算法(MergeJoin)以及基2 g! ~3 j* j2 y8 f$ S) X
于哈希的算法(HashJoin)。对于MergeJoin,首先使用Sort运算符分别对输入表格预
0 S8 B/ g/ Z- C9 O& U$ b1 l+ h8 `处理,使得两张输入表都在连接列上排好序,接着按顺序迭代两张输入表,合并连* _4 F+ F- o4 m; l
接列相同的行并输出;对于HashJoin,首先根据连接列计算哈希值K,并分别将两张+ q" Z3 Y7 c! ~ A1 ^' h
输入表格的数据写入到第K个桶中。接着,对每个哈希桶按照连接列排序。最后,依
# G- a. Z% S2 M- s P) {次对每个哈希桶合并连接列相同的行并输出。
: Y$ P+ M) `0 }% ]子查询分为两种:关联子查询和非关联子查询,其中比较常用的是使用IN子句
% l8 E9 Z5 W8 d) g2 a的非关联子查询。举例如下:
' _! d7 A6 m2 ?& J+ t% O L6 {例10-3 假设有两张表格:item(商品表,包括商品号item_id,商品名5 b) I% ^/ p) q) E( L" r2 x; D n, `& l
item_name,分类号category_id,),category(类别表,包括分类号category_id,分" k* K# d, a! ?
类名category_name)。如果需要查询分类号出现在category表中商品,可以采用图10-' V. N8 h: G' c' b
4左边的IN子查询,而这个子查询将被自动转化为图10-4右边的等值连接。如果$ @7 m$ z8 C; S2 Q# ~
category表中的category_id列有重复,表连接之前还需要使用distinct运算符来删除重
- s. L1 q8 s% U& H# L) U( T+ H6 j复的记录。
& @; _5 j" D/ O图 10-4 IN子查询转化为等值连接
% h" y3 A, V {例10-4 例10-3中,如果category表只包含category_id为1~10的记录,那么,可7 w7 ? J0 O. a, [( ?) d
以将IN子查询写成图10-5中的常量表达式。
1 H8 R- x9 e) U. S4 v图 10-5 IN子查询转化为常量表达式
+ g4 u" M2 ~9 ], r转化为常量表达式后,MergeServer执行SQL计算时,可以将IN后面的常量列表
" P' H' u. @) q4 z$ |发送给ChunkServer,ChunkServer只返回category_id在常量列表中的商品记录,而不是
) p( \6 K8 _) R将所有的记录返回给MergeServer过滤,从而减少二者之间传输的数据量。
$ n+ h# P7 n, x) v$ ^OceanBase多表操作做得还很粗糙,例如不支持嵌套连接(Nested Loop Join),
! s% K- T/ w* M4 S" j不支持非等值连接,不支持查询优化等,后续将在合适的时间对这一部分代码进行# i1 X( C/ D4 ?/ V) N$ N4 a
重构。! h+ F+ f6 b4 y) t
10.2.4 SQL执行本地化/ E. l3 v: j q5 {0 o4 I( U
MergeServer包含SQL执行模块MS-SQL,ChunkServer也包含SQL执行模块CS-5 ^8 Z6 ~; c" P* \$ t
SQL,那么,如何区分二者的功能呢?多表操作由MergeServer执行,对于单表操
. o% t. _/ L( Z作,OceanBase设计的基本原则是尽量支持SQL计算本地化,保持数据节点与计算节
# j5 D# y9 D4 z% O6 |点一致,也就是说,只要ChunkServer能够实现的操作,原则上都应该由它来完成。
9 t4 L- m. H- E8 C' S- @& } T●TableScan:每个ChunkServer扫描各自子表范围内的数据,由MergeServer合并- I5 o5 I% u9 n- N9 X
ChunkServer返回的部分结果。
) }+ n4 ~9 r# y●Filter:对基本表的过滤集成在TableScan操作符中,由ChunkServer完成。对分, u7 G0 D* w- p0 z, @3 y5 L
组后的结果执行过滤(Having)集成在GroupBy操作符中,一般情况下由MergeServer/ z/ K* y+ F1 V& ]
完成;但是,如果能够确定每个分组的所有数据行只属于同一个子表,比如SQL请求" B8 p6 I% u4 @- t
只涉及一个tablet,那么,分组以及分组后的过滤操作符可以由ChunkServer完成。6 w$ y# k" |8 _, @. p9 \! F
●Projection:对基本表的投影集成在TableScan操作符中,由ChunkServer完成,
9 W# Q a0 ~, d7 j对最终结果的投影由MergeServer完成。8 \# Q( C8 c# H1 f; l0 B% f/ d
●GroupBy:如果SQL读取的数据只在一个子表上,那么由该子表所在的/ f* p) \% j# s) h2 I, ]9 Q) L# c
ChunkServer完成分组操作;否则,每台ChunkServer各自完成部分数据的分组操作,
3 }& V% Z; Y0 @执行聚合运算后得到部分结果,再由MergeServer合并所有ChunkServer返回的部分结
+ j% B, g% ]( E- n* ?果,对于属于同一个分组的数据再次执行聚合运算。某些聚合运算需要做特殊处8 Z! u0 E9 R- y" C0 m
理,比如avg,需要转化为sum和count操作发送给ChunkServer,MergeServer合并- C# A5 A. @# M; g
ChunkServer返回的部分结果后计算出最终的sum和count值,并通过sum/count得到avg
* a" B' e# M }# j1 Y3 ]的最终结果。 Z/ V3 g+ p4 J) z
●Sort:如果SQL读取的数据只在一个子表上,那么由该子表所在的ChunkServer
; ]" \0 k/ n: Q0 A! k完成排序操作;否则,每台ChunkServer各自完成部分数据的排序,并将排好序的部. T. S+ @4 W5 g, f) y4 z" |, ~
分数据返回MergeServer,再由MergeServer执行多路归并。
8 v8 R" u% g) W$ R; J6 W4 H●Limit:Limit操作一般由MergeServer完成,但是,如果请求的数据只在一个子" q( D/ l/ [5 q$ Z, I
表上,可以由ChunkServer完成,这往往会大大减少MergeServer与ChunkServer之间传" d' @. |4 ?2 o# Y, T
输的数据量。0 b' J5 H/ J4 T6 }6 _
●Distinct:Distinct与GroupBy类似。ChunkServer先完成部分数据的去重,再由3 g& c# `2 z# M
MergeServer进行整体去重。( a+ l+ ?/ G& W
例10-5 图10-2中的SQL语句为"select c1,sum(c2)from t1 where c3=10 group
/ j a0 |6 n5 _! z/ r ?by c1 having sum(c2)>=10 order by c1 limit 0,20"。执行步骤如下:; b V. c4 I3 f/ n6 P2 A, I0 e
1)ChunkServer调用TableScan操作符,读取子表t1中的数据,该操作符还将执行
: ^) E: H: u, F# }投影(Project)和过滤(Filter),返回的结果只包含c3=10的数据行,且每行只包含 }& Y0 W/ g8 l- l) } \
c1、c2、c3三列。
' }5 M- y q! f$ I2 N9 g2)ChunkServer调用HashGroupBy操作符(假设采用基于哈希的分组算法),按1 x! {- G2 a0 z; P. d
照c1对数据分组,同时计算每个分组内c2列的总和sum(c2)。
- p% p# I' z0 o" J3)每个ChunkServer将分组后的部分结果返回MergeServer,MergeServer将来自不+ Q/ o; P3 n" t* l
同ChunkServer的c1列相同的行合并在一起,再次执行sum运算。# C& G# V/ A2 B
4)MergeServer调用Filter操作符,过滤第3)步生成的最终结果,只返回! m6 \1 L4 q }3 J) l5 g( T+ n
sum(c2)>=10的行。* `5 o% p# @" g$ x3 a2 A9 z
5)MergeServer调用Sort操作符将结果按照c1排序。
" e' O( ?5 f2 k. N6)MergeServer调用Project操作符,只返回c1和sum(c2)这两列数据。- t9 t) `5 I5 |$ H
7)MergeServer调用Limit操作符执行分页操作,只返回前20条数据。; h& K' m) n& C! E- p7 _7 n
当然,如果能够确定请求的数据全部属于同一个子表,那么,所有的物理运算1 s6 V" G7 l& g4 U
符都可以由ChunkServer执行,MergeServer只需要将ChunkServer计算得到的结果转发6 ~0 c/ @5 A4 Y3 E4 x% j
给客户端。9 f9 y* c) T. f% s8 d& j* s
9 I7 U/ Q. _* a0 e6 D* ^
6 ]- g( Y1 W/ | |
|