|
10.2 只读事务( t6 O, u9 _" z3 ?, }5 G8 L, X# j2 O
只读事务(SELECT语句),经过词法分析、语法分析,预处理后,转化为逻辑: m3 |3 `1 {' l: k( ^
查询计划和物理查询计划。以SQL语句select c1,c2 from t1 where id=1 group by c1& E) o& u# |: z' l
order by c2为例,MergeServer收到该语句后将调用ObSql类的静态方法 `' m) V$ u0 S9 [% w( I
direct_execute,执行步骤如下:
+ `, h" M4 X7 T! E) y5 H! U1)调用flex、bison解析SQL语句生成一个语法树。
& {6 W9 B3 l8 h7 |$ t2)解析语法树,生成逻辑执行计划ObSelectStmt。ObSelectStmt结构中记录了
" r0 Y* f/ u: y- m+ Y) V! rSQL语句扫描的表格名(t1),投影列(c1,c2),过滤条件(id=1),分组列2 c$ |7 _3 V. ~9 d8 c6 T
(c1)以及排序列(c2)。
; y% n+ m( ?( |4 a* F! a3)根据逻辑执行计划生成物理执行计划。ObSelectStmt只是表达了一种意图," {# I! \7 K. Y: Z3 I6 i! Y; `+ f
但并不知道实际如何执行,ObTransformer类的generate_physical_plan将ObSelectStmt转
7 g8 n( \% x+ Q! X3 ^$ i0 ~化为物理执行计划。; _3 U2 }1 Q) x4 o( n W. c9 q
逻辑查询计划的改进以及物理查询计划的选择,即查询优化器,是关系数据库
5 I4 ^% v- @! u( F最难的部分,OceanBase目前在这一部分的工作不多。因此,本节不会涉及太多关于
2 d1 H: ^, m: o" R' w如何生成物理查询计划的内容,下面仅以两个例子说明OceanBase的物理查询计划。
2 j1 ? v" a5 h) }) n例10-1 假设有一个单表SQL语句如图10-2所示。. N' z. N3 ?# ]7 B, |% f
图 10-2 单表物理查询计划示例- T& Z1 y" ~1 Y, Y1 Y: A
单表SQL语句执行过程如下:
$ C6 A5 D) c- m/ b( }6 ?* v1)调用TableScan操作符,读取子表t1中的数据,该操作符还将执行投影
3 W( B% q" S4 w4 w/ e& t. I(Project)和过滤(Filter),返回的结果只包含c3=10的数据行,且每行只包含c1、# Z: K: Y. i2 g3 m! o
c2、c3三列。" v% ?# [5 e! ~6 j
2)调用HashGroupBy操作符(假设采用基于哈希的分组算法),按照c1对数据2 B4 ?2 M' j# C! ]# b
分组,同时计算每个分组内c2列的总和。0 T ]# \4 ^; X
3)调用Filter操作符,过滤分组后生成的结果,只返回上一层sum(c2)>=10的
( q0 ]* c& Y: r: j4 o行。4 D3 I4 O9 D; g/ C/ G
4)调用Sort操作符将结果按照c1排序。/ ?2 e* W1 a* N/ [" B
5)调用Project操作符,只返回c1和sum(c2)这两列数据。& f) X# S2 D4 F: z" B0 B* F
6)调用Limit操作符执行分页操作,只返回前20条数据。, U9 [/ q6 A/ |2 j. ^/ w% s {
例10-2 假设有一个需要联表的SQL语句如图10-3所示。
1 D2 W1 P9 T: N4 Y) l+ r3 j* T图 10-3 多表物理查询计划示例8 m& p5 U' F! q" L2 Z$ z
多表SQL语句执行过程如下:' c2 Z; a, ?$ R P7 F
1)调用TableScan分别读取t1和t2的数据。对于t1,使用条件c3=10对结果进行过4 j. H8 U+ r0 c0 v
滤,t1和t2都只需要返回c1,c2,c3这三列数据。% f, q- s3 F+ O, ? S" W
2)假设采用基于排序的表连接算法,t1和t2分别按照t1.c2和t2.c2排序后,调用
; T: k2 i2 m, T) j9 v, zMerge Join运算符,以t1.c2=t2.c2为条件执行等值连接。+ r* C; L! Z: f, b
3)调用HashGroupBy运算符(假设采用基于哈希的分组算法),按照t1.c1对数
0 ^2 S3 E5 |* @) m, E0 W z2 g据分组,同时计算每个分组内t2.c3列的总和。
2 j# ]( y3 O1 g; O4)调用Filter运算符,过滤分组后的生成的结果,只返回上一层sum(t2.c3)>0 B7 c/ h# E8 E; j& @4 }
=10的行。
1 z9 K6 w( c; H+ {( ^4 \5)调用Sort操作符将结果按照t1.c1排序。
8 W/ A7 u& T1 a% l6)调用Project操作符,只返回t1.c1和sum(t2.c3)这两列数据。& @! d1 C; x9 j! P- e. w$ j& T
7)调用Limit操作符执行分页操作,只返回前20条数据。
3 k7 h( W O. A- [" K& [10.2.1 物理操作符接口* {* n. \3 o' L
9.4.2节介绍一期分布式存储引擎中的迭代器接口为ObIterator,通过它,可以将
% m8 l0 {8 `9 H/ b5 L! ^0 Q读到的数据以cell为单位逐个迭代出来。然而,数据库操作总是以行为单位的,因/ }, p7 O" t. u0 m6 C$ P$ R
此,二期实现数据库功能层时考虑将基于cell的迭代器修改为基于行的迭代器。3 n$ w# {( X! K9 E
行迭代器接口如下:
2 c2 D/ c( ^4 O+ h9 E1 m//ObRow表示一行数据内容2 d. b6 t3 Y& _8 q; D) t
class ObRow: C$ f8 m4 |5 c9 f& C
{
# J8 j% n, G- G0 }4 Q. d J8 N) Npublic:$ w3 u! M6 L1 q" v! H& F
//根据表ID以及列ID获得指定cell1 w$ _4 Z8 A+ Z9 ?, X& i& v7 c. z$ d
//@param[in]table_id表格ID3 C2 Q, N4 }: q
//@param[in]column_id列ID
[ s8 A4 M3 }- n//@param[out]cell读到的cell
3 X9 b: K5 X" G$ Z( {$ J+ g( Cint get_cell(const uint64_t table_id,const uint64_t column_id,ObObj*&cell);; L+ h; Z Q# X k) u
//获取第cell_idx个cell7 k+ \7 ]3 `, Z5 ^
int raw_get_cell(const int64_t cell_idx,const ObObj*&cell,uint64_t&table_id,
- E% `, y- K1 G9 luint64_t&column_id);! T- _. R* ^) y4 U' i
//获取本行的列数
+ _ i. x: C: T" H! r' @int64_t get_column_num()const;
O& |' Z* d* s0 x! }+ Z};
! a! g$ ]; W' [9 r' F# D每一行数据(ObRow)包括多个列,每个列的内容包括所在的表
7 [; \) H; E6 h( AID(table_id),列ID(column_id)以及列内容(cell)。ObRow提供两种访问方
1 `' Z* B, q4 t式:根据table_id和column_id随机访问某个列,以及根据列下标(cell_idx)获取某个
$ r3 o, p- G* b$ S) Z( F指定列。
! L; y1 v% e; [9 M. Y N* K' k物理运算符接口如下:" W, U! q; M. F0 R* g8 Y
//物理运算符接口
* `4 D% B" d( c5 E# `5 d( Nclass ObPhyOperator5 Y* o( A+ w. q" y
{
2 l& f0 C* R B0 O+ ?& a- p- jpublic:( h3 ?# B- S" C8 S1 K. f# b. c
//添加子运算符,所有非叶子节点物理运算符都需要调用该接口
* L5 j6 [( X0 `; y( F$ E% i5 J$ tvirtual int set_child(int32_t child_idx,ObPhyOperator&child_operator);
4 z" s5 g+ h: z! {8 v//打开物理运算符。申请资源,打开子运算符等
5 I* N H& g4 e, H- Dvirtual int open()=0;( ?' ]* C& D& M% W/ f
//关闭物理运算符。释放资源,关闭子运算符等
# P8 i( v$ e# [6 C" ]" dvirtual int close()=0;
6 @2 ?. }) L) o5 L7 `//获得下一行数据内容+ V9 C* E- m ]
//@param[out]row下一行数据内容的引用# s* Z! ?5 |/ y- Z3 l( e% }
//@return返回码,包括成功、迭代过程中出现错误以及迭代完成
3 {+ }; }( S0 Ovirtual int get_next_row(const ObRow*&row)=0;
4 h* T/ \6 w+ R# ?/ s};
?: i1 J7 x( I" L4 M6 fObPhyOperator每次获取一行数据,使用方法如下:
$ I8 j! v4 w% s! O5 w7 i4 L) J z4 A1 NObPhyOperator root_operator=root_operator_;//根运算符
5 w$ @ b* e4 C* y5 proot_operator->open();
# w2 x# @! x" x' I- `* P3 MObRow*row=NULL;
; f" }% M/ W2 R! x; r# a- awhile(OB_SUCCESS==root_operator->get_next_row(row))6 u( }: i. H$ J) t
{
5 Y5 X8 ]' a. o9 _- E. h6 WOutput(row);//输出本行
# Z2 }2 G/ c, X) ?$ G0 p}
- y8 B% ~3 t6 n7 s. V5 _root_operator->close();
/ x) t% @( H5 P. Z为什么ObPhyOperator类中有一个set_child接口呢?这是因为所有的物理运算符构
/ T2 X7 \; [9 r" Z1 E7 U: q成一个树,每个物理运算的输出结果都可以认为是一个临时的二维表,树中孩子节
0 R# W2 D6 p* e X: W8 t点的输出总是作为它的父亲节点的输入。例10-1中,叶子节点为一个TableScan类型
% g2 @. m& | ]% D) c的物理运算符(称为table_scan_op),它的父亲节点为一个HashGroupBy类型的物理5 |" D( ~! T; Q' P
运算符(称为hash_group_by_op),接下来依次为Filter类型物理运算符filter_op,Sort
6 O6 M+ W, }: G6 {6 l( V类型物理运算符sort_op,Project类型物理运算符project_op,Limit类型物理运算符
" y; \6 Y/ l- h6 Ulimit_op。其中,limit_op为根运算符。那么,生成物理运算符时将执行如下语句:
, B- b7 E# T- v" Wlimit_op->set_child(0,project_op);; B( L* F V3 s
project_op->set_child(0,sort_op);( U: G' j- i6 q' v/ W" D7 w
sort_op->set_child(0,filter_op);. q5 l: M, K3 `& A7 ?
filter_op->set_child(0,hash_group_by_op);# ~) j$ s8 ]; N* z2 i& X/ e( L1 T" z
hash_group_by_op->set_child(0,table_scan_op);1 o" D) V# P9 L6 l8 F
root_op=limit_op;
) ~ C- V. b# u' P# LSQL最终执行时,只需要迭代root_op(即limit_op)就能够把需要的数据依次迭0 I6 k8 K' F% m8 S
代出来。limit_op发现前一批数据迭代完成则驱动下层的project_op获取下一批数据,5 E9 T5 Q, v. k, X: M
project_op发现前一批数据迭代完成则驱动下层的sort_op获取下一批数据。以此类
$ Q8 _5 ?0 f. L推,直到最底层的table_scan_op不断地从原始表t1中读取数据。
9 I2 i- m% D" G+ O10.2.2 单表操作" m5 B+ a, _# q0 F4 F
单表相关的物理运算符包括:- L2 {) @1 ?: W
●TableScan:扫描某个表格,MergeServer将扫描请求发给请求的各个子表所在
; h% q/ c* r+ Z7 ]: V的ChunkServer,并将ChunkServer返回的结果按照子表范围拼接起来作为输出。如果) x* I- j1 G* j
请求涉及多个子表,TabletScan可由多台ChunkServer并发执行。* o+ |' n( M3 P2 b: B/ }4 h
●Filter:针对每行数据,判断是否满足过滤条件。
5 h3 J! z* }4 ~# J9 p$ }+ [●Projection:对输入的每一行,根据定义的输出表达式,计算输出结果行。
# d: b1 {; r7 D' m2 W●GroupBy:把输入数据按照指定列进行聚集,对聚集后的每组数据可以执行计
0 @* F Q P- J( T6 ?数(count)、求和(sum)、计算最小值(min)、计算最大值(max)、计算平均值
2 Q ` p! Q) j& |0 ~' _(avg)等聚集操作。, Z% w) X6 W/ t9 ^7 R, H8 | R
●Sort:对输入数据进行整体排序,如果内存不够,需要使用外排序。- _, Q, G$ }6 H9 Q/ |" o: J8 u
●Limit(offset,count):返回行号在[offset,offset+count)范围内的行。
" c6 f; U. m7 W8 l: P+ Q●Distinct:消除某些列相同的重复行。
. F/ ?, t( G9 _GroupBy、Distinct物理操作符可以通过基于排序的算法实现,也可以通过基于哈
# P! P4 B/ X+ Q4 ~* b2 w希的算法实现,分别对应HashGroupBy和MergeGroupBy,以及HashDistinct和
. S) F5 Y& u+ j8 e4 ]$ b- [MergeDistinct。下面分别讨论排序算法和哈希算法。' g4 f2 q% Q- }
1.排序算法
- T1 o: r1 X% Y3 EMergeGroupBy、MergeDistinct以及Sort都需要使用排序算法。通用的<key,value
# o+ `' P* Y) B8 F>排序器可以分为两个阶段:0 A6 p( m$ P" `7 M K
●数据收集:在数据收集阶段,调用者将<key,value>对依次加入到排序器。如
9 d1 d- y2 @6 U# [* r1 a$ l( r, t果数据总量超过排序器的内存上限,需要首先将内存中的数据排好序,并存储到外0 H# x$ E, f" [! K8 m* U4 U8 h8 J
部磁盘中。, q" L2 `% y. ]( m; B6 u! c) w. ^
●迭代输出:迭代第一行数据时,内存中可能有一部分未排序的数据,磁盘中也
" f+ f3 _! u4 u- V, S" d9 k& Q8 L可能有几路已经排好序的数据。因此,首先将内存中的数据排好序。如果数据总量
" }6 W$ C! f8 x不超过排序器内存上限,那么将内存中已经排好序的数据按行迭代输出(内排
' y: a7 m/ v3 `. V5 }- O) h9 a$ I序);否则,对内存和磁盘中的部分有序数据执行多路归并,一边归并一边将结果
, D! k* U, g# ~( B6 K& m迭代输出。2 d2 U [1 o- G0 F* z# F" R% c
2.哈希算法/ X/ W/ M( O2 d0 h0 h7 O, {
HashGroupBy以及HashDistinct都需要使用哈希算法。假设需要对<key,value>对" ~: I u2 X1 Z v
按照key分组,那么首先使用key计算哈希值K,并将这个<key,value>对写入到第K个& M6 X# Q8 j. p2 o
桶中。不同的key可能对应相同的哈希桶,因此,还需要对每个哈希桶内的<8 s$ W0 O' r1 N+ G* [
key,value>对排序,这样才能使得key相同的元组能够连续迭代出来。哈希算法的难
' Y- j5 V% D q点在于数据总量超过内存上限的处理,由于篇幅有限,请自行思考。% j# i" [) Z$ ~$ |4 u
10.2.3 多表操作
. w @. ^- z+ Q5 y$ O9 Y多表相关的物理操作符主要是Join。最为常见的Join类型包括两种:内连接
1 [! c, z+ X4 }(Inner Join)和左外连接(Left Outer Join),而且基本都是等值连接。如果需要连接
4 n) ?5 c3 m7 A/ ]5 F3 e! I多张表,可以先连接前两张表,再将前两张表连接生成的结果(相当于一张临时6 [# ]+ a6 \; l J% W* a
表)与第三张表格连接,以此类推。
& K, n) U0 ^' R+ U两张表实现等值连接方式主要分为两类:基于排序的算法(MergeJoin)以及基$ o( `" Y" O6 ]5 y Y1 e
于哈希的算法(HashJoin)。对于MergeJoin,首先使用Sort运算符分别对输入表格预9 h: J: q4 ? i; y5 ^2 a& y
处理,使得两张输入表都在连接列上排好序,接着按顺序迭代两张输入表,合并连
# v' q' l L5 z1 i( e, p接列相同的行并输出;对于HashJoin,首先根据连接列计算哈希值K,并分别将两张
8 w/ D/ f% ]; h输入表格的数据写入到第K个桶中。接着,对每个哈希桶按照连接列排序。最后,依
5 u: F3 V; n& f) }0 F次对每个哈希桶合并连接列相同的行并输出。9 J/ o' e5 O- Q4 X# [/ r2 F/ i* T) Z
子查询分为两种:关联子查询和非关联子查询,其中比较常用的是使用IN子句
# Y( I5 j2 H( v% |3 h }的非关联子查询。举例如下:, w u. U# c+ T! T0 U- ~& f
例10-3 假设有两张表格:item(商品表,包括商品号item_id,商品名
* s" f( J0 R$ D) E. U5 t" qitem_name,分类号category_id,),category(类别表,包括分类号category_id,分, q# L- c6 X' I( R$ v3 c, V& O) y; r
类名category_name)。如果需要查询分类号出现在category表中商品,可以采用图10-
: C* q- I7 ^/ I4左边的IN子查询,而这个子查询将被自动转化为图10-4右边的等值连接。如果
" v5 e# S: U4 P8 H: i$ mcategory表中的category_id列有重复,表连接之前还需要使用distinct运算符来删除重4 \3 C1 p2 t4 y$ F" Q- R/ @
复的记录。
# ~- n4 ~% l: e6 x7 E* ^图 10-4 IN子查询转化为等值连接
6 `$ S* K/ O1 `; N* S- o例10-4 例10-3中,如果category表只包含category_id为1~10的记录,那么,可- [$ d5 V. B+ _# _* F
以将IN子查询写成图10-5中的常量表达式。
, x- ~2 c& i. l* @图 10-5 IN子查询转化为常量表达式
: I9 T" ^+ n; C: K" l4 g转化为常量表达式后,MergeServer执行SQL计算时,可以将IN后面的常量列表5 `* F3 k4 v" J: D
发送给ChunkServer,ChunkServer只返回category_id在常量列表中的商品记录,而不是- G% q: _* w" L& z4 {
将所有的记录返回给MergeServer过滤,从而减少二者之间传输的数据量。
. [ U3 S) g9 h7 V3 E5 F/ i/ i$ VOceanBase多表操作做得还很粗糙,例如不支持嵌套连接(Nested Loop Join),7 _/ b' U4 o, P( M3 s0 a. {
不支持非等值连接,不支持查询优化等,后续将在合适的时间对这一部分代码进行7 m4 k3 m s* m. l* g7 z: m
重构。
, V; ]0 Y& l0 u8 j8 o. z( W5 w10.2.4 SQL执行本地化5 T% g5 v+ \2 Y4 c4 h7 b! c
MergeServer包含SQL执行模块MS-SQL,ChunkServer也包含SQL执行模块CS-' o% u1 B% X( f) ], L, c4 K
SQL,那么,如何区分二者的功能呢?多表操作由MergeServer执行,对于单表操6 F& f! Y) r0 V
作,OceanBase设计的基本原则是尽量支持SQL计算本地化,保持数据节点与计算节
/ G7 N c( `) |. W' b6 W6 R [点一致,也就是说,只要ChunkServer能够实现的操作,原则上都应该由它来完成。) s* w/ f5 o# r; g
●TableScan:每个ChunkServer扫描各自子表范围内的数据,由MergeServer合并
- ` s1 a& Y/ ^7 P$ V9 J1 FChunkServer返回的部分结果。
8 I O, n& q4 |0 n9 m●Filter:对基本表的过滤集成在TableScan操作符中,由ChunkServer完成。对分
! f/ |& O% B; C# A0 t组后的结果执行过滤(Having)集成在GroupBy操作符中,一般情况下由MergeServer' _6 R3 N! i% Q' Y
完成;但是,如果能够确定每个分组的所有数据行只属于同一个子表,比如SQL请求9 j0 ]3 C% k7 N
只涉及一个tablet,那么,分组以及分组后的过滤操作符可以由ChunkServer完成。
8 S* w4 x: l' j/ d! K' k: [8 H●Projection:对基本表的投影集成在TableScan操作符中,由ChunkServer完成,
$ `( ?, n4 \% x& S, \. O1 G对最终结果的投影由MergeServer完成。
# p* t7 N$ W- i% ~2 I3 @0 k2 s●GroupBy:如果SQL读取的数据只在一个子表上,那么由该子表所在的
$ z9 y) y( z2 aChunkServer完成分组操作;否则,每台ChunkServer各自完成部分数据的分组操作,
2 h+ G" z% M" J8 ?1 s7 p6 }, f执行聚合运算后得到部分结果,再由MergeServer合并所有ChunkServer返回的部分结
4 \* y# t+ O' e0 }# a- Q果,对于属于同一个分组的数据再次执行聚合运算。某些聚合运算需要做特殊处2 T2 Q$ J! Y) \5 b/ S) _
理,比如avg,需要转化为sum和count操作发送给ChunkServer,MergeServer合并
: U/ ]$ b H* P, {+ U- C- BChunkServer返回的部分结果后计算出最终的sum和count值,并通过sum/count得到avg. L2 u" o9 M+ R
的最终结果。! @5 S, h+ {! ^# o
●Sort:如果SQL读取的数据只在一个子表上,那么由该子表所在的ChunkServer( W( Q) F: i, N* w. j4 F
完成排序操作;否则,每台ChunkServer各自完成部分数据的排序,并将排好序的部
$ b; M1 c' j. c$ q7 }分数据返回MergeServer,再由MergeServer执行多路归并。* a# m' I U3 d+ X3 s8 I y
●Limit:Limit操作一般由MergeServer完成,但是,如果请求的数据只在一个子
+ e2 v* y w9 d# ?9 i" [表上,可以由ChunkServer完成,这往往会大大减少MergeServer与ChunkServer之间传
- I" J% @% u( ^# | K输的数据量。: b" |( A3 A; ?8 E# X, i1 Q+ C" C
●Distinct:Distinct与GroupBy类似。ChunkServer先完成部分数据的去重,再由
9 m% ]8 s7 {7 x9 bMergeServer进行整体去重。2 k2 b+ U& e8 J# ?" A" }8 u D
例10-5 图10-2中的SQL语句为"select c1,sum(c2)from t1 where c3=10 group! L) F0 }8 n% ^& E
by c1 having sum(c2)>=10 order by c1 limit 0,20"。执行步骤如下:
) J/ L9 y- R* ^. v/ J1)ChunkServer调用TableScan操作符,读取子表t1中的数据,该操作符还将执行: V) s f8 p# f6 o9 Y
投影(Project)和过滤(Filter),返回的结果只包含c3=10的数据行,且每行只包含1 ?3 W) d% U3 l9 p- R
c1、c2、c3三列。
% W) v' n" ?! D/ @7 H( i& ~2)ChunkServer调用HashGroupBy操作符(假设采用基于哈希的分组算法),按
6 G" q7 X+ \$ c7 }" _5 |" d; d8 b照c1对数据分组,同时计算每个分组内c2列的总和sum(c2)。
; V( {3 ^$ Z" L3)每个ChunkServer将分组后的部分结果返回MergeServer,MergeServer将来自不
8 i0 G5 E2 a v; O1 o: G: g' i! P# ^同ChunkServer的c1列相同的行合并在一起,再次执行sum运算。
' D5 T+ J) T- e2 ]( T0 N4)MergeServer调用Filter操作符,过滤第3)步生成的最终结果,只返回$ F& X9 R, S5 S/ Y
sum(c2)>=10的行。
+ X5 n! P3 T! S, i8 q5)MergeServer调用Sort操作符将结果按照c1排序。
9 \ v: m1 O5 ]4 H$ }" I6)MergeServer调用Project操作符,只返回c1和sum(c2)这两列数据。
& c7 V( L/ a' M, h* e& Q7)MergeServer调用Limit操作符执行分页操作,只返回前20条数据。, r# p* L8 I6 k. C3 \5 S+ y
当然,如果能够确定请求的数据全部属于同一个子表,那么,所有的物理运算
; I" c7 z& H4 m6 B8 S5 {符都可以由ChunkServer执行,MergeServer只需要将ChunkServer计算得到的结果转发4 A9 u* E2 Q: O
给客户端。) J" @& L7 x c5 Y, G4 V- c
2 d$ U& `. ^/ ?; p& ]
% s# _8 z1 n: A |
|