PBE协议

背景与概念

我们已经了解了一个SQL的执行,经历了parse、analyze、rewrite、plan、execute的全流程。那么思考一下,假如有一条sql语句,这条语句会执行几千次几万次,每一次都需要完整的执行上述的所有流程么?很明显,不用。
对于parse部分,同一条sql,执行千万遍,每次的parsetree也都是相同的,因此完全可以只执行一次就够了,将parsetree保存下来,之后就可以直接用了。
对于analyze、rewrite部分,如果没有执行ddl语句去修改SQL用到的表的元数据信息,那么其实也可以执行一次。
对于plan部分,可能受到的影响大一些,DDL、表数据量发生大量变化的时候,都有可能会导致计划发生变化,但在通常的TP场景情况下这种可能性不大。
对于execute部分,这部分理论上就不是十分的方便复用了,因为每一次写操作都可能导致一条sql的结果发生变化。

因此我们可不可以思考一种方案,对于这些重复执行的SQL的每个环节抽象出来,将parsetree、querytree、plan等可以复用的东西,在第一次执行的时候保存下来,之后执行的取出来直接使用,这样不就可以省略下来一大串的流程,提高性能了吗!当然对于一些部分,我们肯定也需要那么一套机制让这些保存下来的东西,在执行了DDL等场景之后失效掉。

但是在实际中,一条完全相同的SQL重复执行多遍的场景有,但是更多的是另一种场景,SQL的结构完全相同,但只是其中的某几个数值不同。例如学生根据自己的学号查询信息,会用到这么一条语句:select * from students where id = ?;每一个学生都会用这条语句,但每个学生的学号都不相同。

对于这种情况,我们可以考虑一种方案,在生成计划的时候,将这些会变得数值以参数的形式替代,之后每次调用查询的时候,先根据语句中的数值将计划中的参数补充完整,之后再去执行,也可以省下来大把的流程时间。
说到这里聪明的大家应该已经想到了pg中的prepare-execute语法,或者java中的preparedStatement接口。没错,就是他们,这种执行方式在数据库内核中对应的名称就是PBE。

按照我们所预想的方案,其所对应的环节可以分为三个:
P(parse),解析SQL语句,生成parsetree、querytree并保存起来。
B(bind),若是第一次调用则生成并保存计划,若已有计划则直接找出来使用,若计划需要参数则将参数补充完整。
E(execute),根据已经完整的计划去执行。

方案实现

数据结构

分析可知,我们在会话的范围内需要有一个用来存储各种计划的位置,需要有一些数据结构用来描述保存的语句、保存的各种tree、保存的plan、参数描述等。
因此按照如下方式进行组织:

PreparedStatement NodeTag node char* sqlstr int len CachedPlanSource psrc ... CachedPlanSource Node* raw_parse_tree; int num_params List* query_list; CachedPlan* gplan; CachedPlan* cplan; ... CachedPlan List* stmt_list; bool dependsOnRole ...

preparedStatement结构抽象表示保存的准备语句,并存在在会话级别的哈希表xxx中,preparedStatement中的name、sqlstr、len等存储一条准备语句最基本的信息,CachedPlanSource psrc用来存储计划缓存的所有东西,包括parsetree、querytree、plan、参数信息等等。

在CachedPlanSource中,raw_parse_tree存储parsetree,query_list存储querytree。但是其计划并非直接存储,而是额外封装了一层CachedPlan表示,但是我们发现其有两个,cplan和gplan,这两个分别表示不带参数的常规计划(custom plan)和带参数的模板计划(generic plan)。另外其所携带的参数数量、参数列表、有效性标志等等,还有很多东西,都存在这个结构体中,它相当于就是优化器产生和需要的所有东西,都在这里了。

CachedPlan中,stmt_list中存储的便是计划树了。

当然以上只是比较重要的骨架部分,还有很多实现的细节,以及各种场景各种特性所涉及到的东西,例如GPC相关的等等。

P阶段

p阶段主要是用来解析SQL语句,生成parsetree、querytree并保存起来。
流程大致:

1、检查会话之中是否已经存在同名的准备语句
2、根据sql的name、sql内容创建一个PreparedStatement结构,存入会话之中
3、创建CachedPlanSource,完善PreparedStatement
4、进行词法语法解析,生成parsetree,填入完善CachedPlanSource
5、进行语义分析和重写,生成querytree,填入完善CachedPlanSource

在这些步骤中,我们需要考虑带参数的情况,好在查询编译器已经支持接收带参数的输入,并生成对应的带参分析树查询树等,因此这里不再进行赘述其细节。

B阶段

B(bind),若是第一次调用则生成并保存计划,若已有计划则直接找出来使用,若计划需要参数则将参数补充完整。
当然B阶段需要做的不仅这么简单,我们还需要去提前校验已经保存的计划是不是失效了。

首先从输入输出的角度分析B阶段:
输入:所执行的准备语句的各种数据(CachedPlanSource)、本次执行的参数
输出:完整的执行计划。

大致流程如下:
1、根据name等,找出来会话中存放的对应的PreparedStatement结构。
2、验证其中的CachedPlanSource psrc是否已经失效,如果是,则重新生成一个,如果有效则直接使用即可。
3、如果是有参数的,则根据psrc中的参数信息,预处理本次输入的参数,处理成便于我们绑定和使用的格式。如果没有则不需要此步骤。
4、如果psrc内已经有计划(cplan or gplan),则检查计划是否已经失效。如果是,则删除特。
5、判断本次执行所需要的计划类型是cplan还是gplan
6、若已经有计划,但是与本次所需类型不符合,则删除特。
7、到此我们无论如何都已经有一个符合要求的计划了,如果需要参数的话,则绑定步骤3中处理好的参数。
8、到此已经有一个完整的计划了。

E阶段

E阶段就简单了起来,计划也有了,也是完整的,执行就完事了。

失效清理

计划失效一般存在于执行了DDL语句,例如修改了表结构、增删了索引等,都会导致计划发生变化、失效。
失效清理存在于两种场景,第一种是自身会话执行了DDL导致自身的缓存计划失效,第二种是别的会话执行了DDL导致自身的缓存计划失效。

实际使用场景

我们已经知道了PBE是在数据库内部如何实现的,那么我们应该怎么去使用呢?常见的场景主要有两个,一个是通过prepare-execute语法,一个是通过jdbc、odbc等的驱动接口使用。
对于这两种场景,实际上都是对上面代码实现的各个模块进行了各自的封装而已。

prepare-execute语法

prepare-execute语法,例如

prepare query1(int) as select * from students where id = $1;
execute query1(123456);
execute query1(234567);
....

通过p-e语法来使用PBE,数据库内部逻辑比较简单。
prepare语句的执行,对应的是P阶段,其在数据库内核中的实现接口为ExecPrepare函数。
execute语句的执行,对应的是BE阶段,其在数据库内核中的接口对应ExecuteQuery函数。

报文驱动接口

最常见的方式就是使用JDBC驱动,调用里面的preparedStatement接口来使用。
jdbc客户端与数据库通信的方式是使用报文的方式。因此我们需要了解jdbc每个接口所发送的报文,数据库对于每条报文都是怎么处理的。

// 会在jdbc客户端驱动的缓冲区内拼装一条报文,格式如:
// Q len ... sql ...
// 主要有Q报文头,报文长度len,查询语句内容,还有一些jdbc为这条SQL生成的名称之类的。
p1.preparedStatement("select * from stduents where id = ?");

// 会在jdbc客户端驱动的缓冲区内追加一条报文
// B len len1 param1 ....
// 主要有B报文头,报文长度len,以及每个参数。
p1.bindint(1,123456);

// 会在jdbc客户端驱动的缓冲区中追加一条报文
// E len ...
// 之后会将缓冲区中的三条报文一起发送给数据库服务器
p1.execute()


// 之后只需要发送B与E报文即可,因为parse部分只需要执行一遍。

其对应的处理报文的函数分别为:
exec_parse_message()
exec_bind_message()
exec_execute_message()
三个函数与上一章节所述的实现PBE阶段完全对应,因此在这里不在赘述。

代码实现

以exec_parse_message()、exec_bind_message()、exec_execute_message()三个函数为例子:

JDBC接口的更多操作

通过上一节我们已经知道了jdbc的方式的基础原理,那么在jdbc之中我们还有很多的操作,例如fetch、batch等操作。这些操作对应的实现方式:

batch

功能为每次返回一批数据,这样可以防止结果数据量太大,一次全部返回导致客户端内存不够。
因此数据库内

fetch

  • 2
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值