背景

In database management systems (DBMS), a prepared statement or parameterized statement is a feature used to execute the same or similar database statements repeatedly with high efficiency. Typically used with SQL statements such as queries or updates, the prepared statement takes the form of a template into which certain constant values are substituted during each execution. (https://en.wikipedia.org/wiki...)

在数据库系统中,带参数的语句(parameterized statement),一方面,可能提供预编译的能力,以达到高效执行语句、进步性能的目标。另一方面,可能预防SQL注入攻打,安全性更好。以上两点是传统的数据库系统应用反对带参数语句的次要起因。

从数据库系统角度看,OpenMLDB 反对Parameterized query statement能进一步欠缺数据库查问能力。从业务角度上看,它使得OpenMLDB可能在规定引擎场景下,反对规定特色计算。

场景示例:规定引擎特色计算

SELECT SUM(trans_amount) as F_TRANS_AMOUNT_SUM, COUNT(user) as F_TRANS_COUNT,MAX(trans_amount) as F_TRANS_AMOUNT_MAX,MIN(trans_amount) as F_TRANS_AMOUNT_MIN,FROM t1 where user = 'ABC123456789' and trans_time between 1590115420000 and 1592707420000;

在示例中,咱们计算了用户ABC1234567892020-05-22 02:43:402020-06-20 07:43:40这段期间的交易总额,交易次数,最大交易金额,最小交易金额。这些特色将传递可给上游的组件(规定引擎)应用。

在理论场景中,不可能针对每个用户写一段SQL查问代码。因而,须要一个规定特色计算的模版,而用户,工夫区间则是动态变化的。
最简略的形式,就是写一段相似上面程序,把用户名,工夫区间作为变量拼接到一段SQL语句中。

String query = "SELECT "+  "SUM(trans_amount) as F_TRANS_AMOUNT_SUM, "+  "COUNT(user) as F_TRANS_COUNT,"+  "MAX(trans_amount) as F_TRANS_AMOUNT_MAX,"+  "MIN(trans_amount) as F_TRANS_AMOUNT_MIN,"+  "FROM t1 where user = '"+ user +"' and trans_time between "   + System.currentTimestamp()-30*86400000+ " and " + System.currentTimestamp();executor.execute(query);

这种实现办法比拟间接,但查问性能将很差,并且可能有SQL注入的危险。更为举荐的形式,是应用带参数查问(Parameterized query)

PreparedStatement stmt = conn.prepareStatement("SELECT "+  "SUM(trans_amount) as F_TRANS_AMOUNT_SUM, "+  "COUNT(user) as F_TRANS_COUNT,"+  "MAX(trans_amount) as F_TRANS_AMOUNT_MAX,"+  "MIN(trans_amount) as F_TRANS_AMOUNT_MIN,"+  "FROM t1 where user = ? and trans_time between ? and ? ");stmt.setString(1, user);stmt.setTimestamp(2, System.currentTimestamp()-30*86400000);stmt.setTimestamp(3, System.currentTimestamp())ResultSet rs = stmt.executeQuery();rs.next();

实现细节

在OpenMLDB中,反对一个新的语法性能,通常须要顺次实现语法解析、打算生成和优化、表达式Codegen、执行查问等步骤。必要时,还须要思考在客户端新增或者重构相干接口。Paramteried Query的反对根本就涵盖的对上述几个模块的批改和开发,因而,理解相干实现细节有助于大家疾速理解OpenMLDB的开发,特地是OpenMLDB Engine的开发。

下图是执行带参数查问流程示意图。

  1. 用户在应用程序JavaApplication中s应用JDBC(PrepraredStatement)来执行带参数查问。
  2. 客户端(TabletClient)提供接口ExecuteSQLParameterized来解决带参数的查问,并通过RPC调用服务端(Tablet)的Query服务。
  3. 服务端(Tablet)的依赖Engine模块进行查问编译和执行。
  4. 查问语句的编译须要通过SQL语法分析,打算生成优化,表达式Codegen三个次要阶段。编译胜利后,编译后果会寄存在以后执行会话(jizSeesion)的SQL上下文中(SqlContext)。如果以后查问语句曾经预编译过,则不须要反复编译。可间接从编译缓存中获取绝对应的编译产物寄存到RunSession的SqlContext中。
  5. 查问语句的执行须要调用RunSeesion的Run接口。执行后果run output会寄存到response的附件中,回传给TabletClient。最终寄存到ResultSet返回给JavaApplication

1. JDBC PreparedStatement

1.1 JDBC Prepared Statements 概览

Sometimes it is more convenient to use a `PreparedStatement` object for sending SQL statements to the database. This special type of statement is derived from the more general class, `Statement`, that you already know.If you want to execute a `Statement` object many times, it usually reduces execution time to use a `PreparedStatement` object instead.[[2]](Using Prepared Statements)

JDBC提供PreparedStatement给用户执行参数的SQL语句。用户能够应用PrepareStatment执行带参数的查问、插入、更新等操作。这个大节,咱们讲具体OpenMLDB的PrepareStatement执行带参数查问语句的细节。
1.2 OpenMLDB PreapredStatement的用法介绍

public void parameterizedQueryDemo() {  SdkOption option = new SdkOption();  option.setZkPath(TestConfig.ZK_PATH);  option.setZkCluster(TestConfig.ZK_CLUSTER);  option.setSessionTimeout(200000);  try {    SqlExecutor executor = new SqlClusterExecutor(option);    String dbname = "demo_db";    boolean ok = executor.createDB(dbname);    // create table    ok = executor.executeDDL(dbname, "create table t1(user string, trans_amount double, trans_time bigint, index(key=user, ts=trans_time));");    // insert normal (1000, 'hello')    ok = executor.executeInsert(dbname, "insert into t1 values('user1', 1.0, 1592707420000);");    ok = executor.executeInsert(dbname, "insert into t1 values('user1', 2.0, 1592707410000);");    ok = executor.executeInsert(dbname, "insert into t1 values('user1', 3.0, 1592707400000);");    ok = executor.executeInsert(dbname, "insert into t1 values('user2', 4.0, 1592707420000);");    ok = executor.executeInsert(dbname, "insert into t1 values('user2', 5.0, 1592707410000);");    ok = executor.executeInsert(dbname, "insert into t1 values('user2', 6.0, 1592707400000);");        PreparedStatement query_statement       = executor.getPreparedStatement(dbname, "select SUM(trans_amout), COUNT(trans_amout), MAX(trans_amout) from t1 where user=? and trans_time between ? and ?");        query_statement.setString(1, "user1");    query_statement.setLong(2, 1592707410000);    query_statement.setLong(3, 1592707420000);    com._4paradigm.openmldb.jdbc.SQLResultSet rs1      = (com._4paradigm.openmldb.jdbc.SQLResultSet) query_statement.executeQuery();    query_statement.setString(1, "user2");    query_statement.setLong(2, 1592707410000);    query_statement.setLong(3, 1592707420000);    com._4paradigm.openmldb.jdbc.SQLResultSet rs2      = (com._4paradigm.openmldb.jdbc.SQLResultSet) query_statement.executeQuery();        query_statement.setString(1, "user3");    query_statement.setLong(2, 1592707410000);    query_statement.setLong(3, 1592707420000);    com._4paradigm.openmldb.jdbc.SQLResultSet rs3      = (com._4paradigm.openmldb.jdbc.SQLResultSet) query_statement.executeQuery();  }  catch (Exception e) {    e.printStackTrace();  }}
  • Step 1: 结构executor。筹备数据库,表,表数据(如果需要的话)
  • Step 2: 应用带参数的查问语句新建一个PreparedStatement实例
PreparedStatement query_statement   = executor.getPreparedStatement(dbname, "select SUM(trans_amout), COUNT(trans_amout), MAX(trans_amout) from t1 where user=? and trans_time between ? and ?");
  • Step 3: 设置每一个地位上的参数值。

    query_statement.setString(1, "user1");query_statement.setLong(2, 1592707410000);query_statement.setLong(3, 1592707420000);
  • Step 4: 执行查问。获取查问后果。请留神,执行完一次查问后,PrepareStatement里的参数数据会主动清空。能够间接配置新参数值,进行新一轮查问

    com._4paradigm.openmldb.jdbc.SQLResultSet rs2= (com._4paradigm.openmldb.jdbc.SQLResultSet) query_statement.executeQuery();query_statement.setString(1, "user2");query_statement.setLong(2, 1592707410000);query_statement.setLong(23,  1592707420000);com._4paradigm.openmldb.jdbc.SQLResultSet rs2= (com._4paradigm.openmldb.jdbc.SQLResultSet) query_statement.executeQuery();

    1.3 PreparedStatement的实现细节

public class PreparedStatement implements java.sql.PreparedStatement {      //...      // 参数行    protected String currentSql;      // 参数数据      protected TreeMap<Integer, Object> currentDatas;      // 参数类型    protected TreeMap<Integer, com._4paradigm.openmldb.DataType> types;      // 上次查问的参数类型    protected TreeMap<Integer, com._4paradigm.openmldb.DataType> orgTypes;        //...}

PrepareaStatement继承了JDBC标准接口java.sql.PreparedStatement。它保护了查问编译和执行须要的一些基本要素:查问语句(currentSql), 参数数据(currentDatas) 参数类型(types)等。

  • 构建PrepareaStatement后,咱们初始化了PreparedStatement,并设置currentSql
  • 设置参数值后, currentDatas, types会被更新。
  • 执行查问时,query_statement.executeQuery()

    @Overridepublic SQLResultSet executeQuery() throws SQLException {  checkClosed();  dataBuild();  Status status = new Status();  com._4paradigm.openmldb.ResultSet resultSet = router.ExecuteSQLParameterized(db, currentSql, currentRow, status);  // ... 此处省略  return rs;}
  • 首先,执行dataBuild: 按参数类型和地位将参数数据集编码编码到currentRow中。值得注意的是,如果参数类型不发生变化,咱们能够复用原来的currentRow实例。

     protected void dataBuild() throws SQLException {// types has been updated, create new row container for currentRowif (null == this.currentRow || orgTypes != types) {  // ... 此处省略  this.currentRow = SQLRequestRow.CreateSQLRequestRowFromColumnTypes(columnTypes);  this.currentSchema = this.currentRow.GetSchema();  this.orgTypes = this.types;}// ... 此处currentRow初始化相干的代码   for (int i = 0; i < this.currentSchema.GetColumnCnt(); i++) {   DataType dataType = this.currentSchema.GetColumnType(i);   Object data = this.currentDatas.get(i+1);   if (data == null) {      ok = this.currentRow.AppendNULL();    } else {    // 省略编码细节    // if (DataType.kTypeInt64.equals(dataType)) {    //    ok = this.currentRow.AppendInt64((long) data);    // }     // ...    }    if (!ok) {      throw new SQLException("append data failed, idx is " + i);    }  }  if (!this.currentRow.Build()) {    throw new SQLException("build request row failed");  }  clearParameters();}}
  • 接着,调用客户端提供的带参数查问接口ExecuteSQLParameterized

2. TabletClient 和 Tablet

2.1 客户端tablet_client

客户端提供接口ExecuteSQLParameterized来反对带参数查问。

/// Execute batch SQL with parameter rowstd::shared_ptr<hybridse::sdk::ResultSet> ExecuteSQLParameterized(const std::string& db, const std::string& sql,                        std::shared_ptr<SQLRequestRow> parameter,                        ::hybridse::sdk::Status* status) override;

ExecuteSQLParameterized将从参数行parameter中提取参数类型、参数行大小等信息,装入QueryRequest,并把参数数据行装入roc附件中。客户端调用rpc,在服务端实现查问的编译和运行。

将参数行大小、分片数、参数类型列表装入QueryRequest

request.set_parameter_row_size(parameter_row.size());request.set_parameter_row_slices(1);for (auto& type : parameter_types) {  request.add_parameter_types(type);}

参数数据行寄存在rpc的附件中cntl->request_attachment()

auto& io_buf = cntl->request_attachment();if (!codec::EncodeRpcRow(reinterpret_cast<const int8_t*>(parameter_row.data()), parameter_row.size(), &io_buf)) {  LOG(WARNING) << "Encode parameter buffer failed";  return false;}

调用RPC

bool ok = client_.SendRequest(&::openmldb::api::TabletServer_Stub::Query, cntl, &request, response);

2.2 服务端Tablet

服务端tablet的Query函数负责从QueryRequest中获取参数行信息,而后调用接口engine_->Get()编译查问语句并调用接口session.Run()执行查问语句。

::hybridse::vm::BatchRunSession session;if (request->is_debug()) {  session.EnableDebug();}session.SetParameterSchema(parameter_schema);{  bool ok = engine_->Get(request->sql(), request->db(), session, status);  // ...}::hybridse::codec::Row parameter_row;auto& request_buf = static_cast<brpc::Controller*>(ctrl)->request_attachment();if (request->parameter_row_size() > 0 &&    !codec::DecodeRpcRow(request_buf, 0, request->parameter_row_size(), request->parameter_row_slices(),                         &parameter_row)) {  response->set_code(::openmldb::base::kSQLRunError);  response->set_msg("fail to decode parameter row");  return;}std::vector<::hybridse::codec::Row> output_rows;int32_t run_ret = session.run(parameter_row, output_rows);

想理解更多细节,能够浏览 tablet客户端 和 tablet 的源码实现。

3. Compile: 查问语句的编译

3.1 查问语句的编译

  • step 1: 对带参数查问语句来说,编译时,相比一般查问,须要额定配置参数类型信息。

    session.SetParameterSchema(parameter_schema);
  • step 2: 配置参数列表后,调用engine.Get(...)接口编译SQL语句
    查问语句的编译须要通过SQL语法分析(3.2. Parser: 语法解析),打算生成(3.3 Planner: 打算生成),表达式Codegen(3.4 Codegen: 表达式的代码生成)三个次要阶段。编译胜利后,编译后果会寄存在以后执行会话(RunSeesion)的SQL上下文中(SqlContext)。前面几个大节将顺次介绍带参数查问语句的编译过程。

如果以后查问语句曾经预编译过,则不须要反复编译。可间接从编译缓存中获取绝对应的编译产物寄存到RunSession的SqlContext中。咱们须要须要特地关注一下编译缓存的设计变动。对于带参数的查问来说,命中缓存须要同时匹配SQL和参数类型列表

// check if paramter types matches with target compile result or not.for (int i = 0; i < batch_sess->GetParameterSchema().size(); i++) {  if (cache_ctx.parameter_types.Get(i).type() != batch_sess->GetParameterSchema().Get(i).type()) {    status = Status(common::kEngineCacheError, "Inconsistent cache parameter type, expect " +                    batch_sess->GetParameterSchema().Get(i).DebugString()," but get ", cache_ctx.parameter_types.Get(i).DebugString());    return false;  }}

3.2. Parser: 语法解析

OpenMLDB的语法解释器是基于ZetaSQL的SQL的解释器开发的:除了笼罩Zetasql原有的语法能力,还额定反对了OpenMLDb特有语法个性。例如,为AI场景引入的非凡拼表类型LastJoin和窗口类型ROWS_RANGE等。对于OpenMLDB的语法解析以及新语法个性会陆续在将来的技术文章中论述。

SQL的Parameterized 语句应用?作为参数的占位符,这种占位符被ZetaSQL解释器解析为zetasql::ASTParameterExpr。因为ZetaSQL中曾经反对了Parameterized Query Statement的解析,所以咱们并不需要对语法解析模块作太多额定批改,仅须要将原来的限度关上,辨认这种参数表达式,将其转化为OpenMLDB的ParameterExpr类型的表达式节点并存放在语法树中。

xpression into ExprNodebase::Status ConvertExprNode(const zetasql::ASTExpression* ast_expression, node::NodeManager* node_manager,                             node::ExprNode** output) {  //...  base::Status status;  switch (ast_expression->node_kind()) {      //...      case zetasql::AST_PARAMETER_EXPR: {        const zetasql::ASTParameterExpr* parameter_expr =           ast_expression->GetAsOrNull<zetasql::ASTParameterExpr>();        CHECK_TRUE(nullptr != parameter_expr, common::kSqlAstError, "not an ASTParameterExpr")        // Only support anonymous parameter (e.g, ?) so far.           CHECK_TRUE(nullptr == parameter_expr->name(), common::kSqlAstError,                     "Un-support Named Parameter Expression ", parameter_expr->name()->GetAsString());        *output = node_manager->MakeParameterExpr(parameter_expr->position());        return base::Status::OK();      }      //...  }}

例如,上面这条参数查问语句:
SELECT col0 FROM t1 where col1 <= ?;
在语法解析后,将生成如下查问语法树:

+-list[list]:  +-0:    +-node[kQuery]: kQuerySelect      +-distinct_opt: false      +-where_expr:      |  +-expr[binary]      |    +-<=[list]:      |      +-0:      |      |  +-expr[column ref]      |      |    +-relation_name: <nil>      |      |    +-column_name: col1      |      +-1:      |        +-expr[parameter]      |          +-position: 1      +-group_expr_list: null      +-having_expr: null      +-order_expr_list: null      +-limit: null      +-select_list[list]:      |  +-0:      |    +-node[kResTarget]      |      +-val:      |      |  +-expr[column ref]      |      |    +-relation_name: <nil>      |      |    +-column_name: col0      |      +-name: <nil>      +-tableref_list[list]:      |  +-0:      |    +-node[kTableRef]: kTable      |      +-table: t1      |      +-alias: <nil>      +-window_list: []这里能够重点关注一下过滤条件,` where col1 <= ?`被解析为:+-where_expr:      |  +-expr[binary]      |    +-<=[list]:      |      +-0:      |      |  +-expr[column ref]      |      |    +-relation_name: <nil>      |      |    +-column_name: col1      |      +-1:      |        +-expr[parameter]      |          +-position: 1

3.3 Planner: 打算生成

逻辑打算
逻辑打算阶段,带参数查问和一般参数并没有什么区别。因而,本文并不打算开展逻辑打算的细节。上面这条参数查问语句:

SELECT col0 FROM t1 where col1 <= ?;
逻辑打算如下:

: +-[kQueryPlan]  +-[kProjectPlan]        +-table: t1        +-project_list_vec[list]:          +-[kProjectList]            +-projects on table [list]:            |  +-[kProjectNode]            |    +-[0]col0: col0  +-[kFilterPlan]    +-condition: col1 <= ?1  +-[kTablePlan]

对逻辑打算以及物理打算细节感兴趣的读者能够关注咱们专栏。后续会陆续推出介绍引擎技术细节的系列文章。

物理打算
在物理打算生成阶段,为了反对带参数查问,要实现两件事:

首先,在物理打算上下文,表达式剖析上下文以及CodeGen上下文中保护参数类型列表。

在带参数查问语句中,最终执行应用的参数是用户动静指定的,所以参数类型也是内部动静指定。为此,咱们提供了相干接口,使用户在编译SQL时,能够配置参数类型列表(如果有参数的话)。这个列表最终会寄存进物理打算上下文,表达式剖析上下文以及CodeGen上下文中。

// 物理打算上下文class PhysicalPlanContext {  // ...  private:    const codec::Schema* parameter_types_;}// 表达式剖析上下文class ExprAnalysisContext {    // ...  private:    const codec::Schema* parameter_types_;}// Codegen上下文class CodeGenContext { // ... private:      const codec::Schema* parameter_types_;}

其次,依据参数类型列表实现参数表达式的类型推断。
Parameterized query语句实现语法解释后,简直就是一棵一般的查问语句生成的语法树。惟一的区别是,parameterized query的语法树里有参数表达式节点(ParamterExpr)。因为参数的类型既与查问上游表的schema无关,也不是常量。所以,咱们无奈间接对这个参数表达式进行类型推断。这使得咱们在打算生成阶段,特地是表达式的类型推断过程中,须要对ParamterExpr进行特地解决。具体的做法是:在推断ParamterExpr输入类型时,须要依据参数所在位置从参数类型列表中找到相应的类型。

Status ParameterExpr::InferAttr(ExprAnalysisContext *ctx) {    // ignore code including boundary check and nullptr check      // ...    type::Type parameter_type = ctx->parameter_types()->Get(position()-1).type();    node::DataType dtype;    CHECK_TRUE(vm::SchemaType2DataType(parameter_type, &dtype), kTypeError,               "Fail to convert type: ", parameter_type);    SetOutputType(ctx->node_manager()->MakeTypeNode(dtype));    return Status::OK();}

还是之前那个SQL语句,物理打算生成后果如下:

SIMPLE_PROJECT(sources=(col0))  FILTER_BY(condition=col1 <= ?1, left_keys=, right_keys=, index_keys=)    DATA_PROVIDER(table=auto_t0)

其中,FILTER_BY节点中的过滤条件就蕴含了参数表达式condition=(col1 <= ?1)

3.4 Codegen: 表达式的代码生成

Codegen模块负责剖析每个打算节点的表达式列表,而后进行一系列表达式和函数的代码生成解决。codegen后,每一个须要计算表达式的打算节点都将生成至多一个codegen函数。这些函数负责计算表达式的计算。

Codegen函数减少一个参数
OpenMLDB的通过LLVM将每一个波及表达式计算的节点生成中间代码(IR)。具体地实现形式是为每一个节点的表达式列表生成相似@__internal_sql_codegen_6的函数(这些函数将在执行语句的过程中,被调用(4 Run: 查问语句的执行):

; ModuleID = 'sql'source_filename = "sql"define i32 @__internal_sql_codegen_6(i64 /*row key id*/,                                      i8* /*row ptr*/,                                      i8* /*rows ptr*/,                                      i8** /*output row ptr ptr*/) {__fn_entry__:// 此处省略}

这个函数的参数次要蕴含一些int_8指针,这些指针指向数据行(row ptr)或者数据集(rows ptr)(聚合计算依赖数据集)。函数体负责每一个表达式的计算,并将后果按程序编码成行,并将编码地址到最初一个i8**输入参数上。

当表达式列表中蕴含参数表达式的时候,咱们还额定须要取得参数数据,因而,须要做的就是在原来的函数构造上,新增一个指向参数行的指针(parameter_row ptr)。

Status RowFnLetIRBuilder::Build(...) { // 此处省略    std::vector<std::string> args;    std::vector<::llvm::Type*> args_llvm_type;    args_llvm_type.push_back(::llvm::Type::getInt64Ty(module->getContext()));    args_llvm_type.push_back(::llvm::Type::getInt8PtrTy(module->getContext()));    args_llvm_type.push_back(::llvm::Type::getInt8PtrTy(module->getContext()));    args_llvm_type.push_back(::llvm::Type::getInt8PtrTy(module->getContext())); // 新增一个int8ptr类型的参数    args_llvm_type.push_back(        ::llvm::Type::getInt8PtrTy(module->getContext())->getPointerTo());      // ...}

于是,反对参数表达式后,codegen函数的构造就变成如下样子:

; ModuleID = 'sql'source_filename = "sql"define i32 @__internal_sql_codegen_6(i64 /*row key id*/,                                      i8* /*row ptr*/,                                      i8* /*rows ptr*/,                                      i8* /*parameter row ptr*/,                                      i8** /*output row ptr ptr*/) {__fn_entry__:// 此处省略}

参数表达式的codegen
参数行和一般的数据行一样,遵循OpenMLDB的编码格局,参数行的第0个元素就是参数查问语句中的第1个参数,第1个元素就是第2个参数,顺次类推。因而,计算参数表达式实际上就是从参数行中读取相应地位的参数。

// Get paramter item from parameter row// param parameter// param output// returnStatus ExprIRBuilder::BuildParameterExpr(const ::hybridse::node::ParameterExpr* parameter, NativeValue* output) {       // ...    VariableIRBuilder variable_ir_builder(ctx_->GetCurrentBlock(), ctx_->GetCurrentScope()->sv());    NativeValue parameter_row;      // 从全局scope中获取参数行parameter_row    CHECK_TRUE(variable_ir_builder.LoadParameter(&parameter_row, status), kCodegenError, status.msg);       // ...      // 从参数行中读取相应地位的参数    CHECK_TRUE(        buf_builder.BuildGetField(parameter->position()-1, slice_ptr, slice_size, output),        kCodegenError, "Fail to get ", parameter->position(), "th parameter value")    return base::Status::OK();}

于是,后面例子中的查问语句的Filter节点的条件col1 < ? 会生成如下代码:

; ModuleID = 'sql'source_filename = "sql"define i32 @__internal_sql_codegen_6(i64, i8*, i8*, i8*, i8**) {__fn_entry__:  %is_null_addr1 = alloca i8, align 1  %is_null_addr = alloca i8, align 1  // 获取行指针row = {col0, col1, col2, col3, col4, col5}  %5 = call i8* @hybridse_storage_get_row_slice(i8* %1, i64 0)  %6 = call i64 @hybridse_storage_get_row_slice_size(i8* %1, i64 0)  // Get field row[1] 获取数据col1  %7 = call i32 @hybridse_storage_get_int32_field(i8* %5, i32 1, i32 7, i8* nonnull %is_null_addr)  %8 = load i8, i8* %is_null_addr, align 1  // 获取参数行指针paramter_row = {?1}  %9 = call i8* @hybridse_storage_get_row_slice(i8* %3, i64 0)  %10 = call i64 @hybridse_storage_get_row_slice_size(i8* %3, i64 0)  // Get field of paramter_row[0] 获取第一个参数  %11 = call i32 @hybridse_storage_get_int32_field(i8* %9, i32 0, i32 7, i8* nonnull %is_null_addr1)  %12 = load i8, i8* %is_null_addr1, align 1  %13 = or i8 %12, %8  // 比拟 col1 <= ?1  %14 = icmp sle i32 %7, %11  // ... 此处省略多行  // 将比拟后果%14编码输入  store i1 %14, i1* %20, align 1  ret i32 0}

在此,咱们并不打算开展codegen的具体细节。后续会陆续更新Codegen设计和优化相干的技术文章。如果大家感兴趣,能够继续关注OpenMLDB技术专栏。

4. Run: 查问语句的执行

  • 查问语句编译后,会将编译产物寄存在以后运行会话(RunSession)中。
  • RunSession提供Run接口反对查问语句的执行。对带参数查问语句来说,执行查问时,相比一般的查问,须要额定传入参数行的信息。
    session.run(parameter_row, outputs)
  • 参数行paramter_row会寄存在运行上下文RunContext中:
RunnerContext ctx(&sql_ctx.cluster_job, parameter_row, is_debug_);
  • 带参数查问过程中,表达式的计算可能依赖动静传入的参数。所以,咱们须要在执行打算的时候,从运行上下文中获取参数行,并带入到表达式函数中计算。以TableProject节点为例,
  • 对于一般查问来说,实现TableProject就是遍历表中每一行,而后为每一个行作RowProject操作。在带参数的查问场景中,因为表达式的计算除了依赖数据行还可能依赖参数。所以,咱们须要从运行行下文中获取参数行,而后project_gen_.Gen(iter->GetValue(), parameter)

    std::shared_ptr<DataHandler> TableProjectRunner::Run(RunnerContext& ctx,const std::vector<std::shared_ptr<DataHandler>>& inputs) {  // ... 此处省略局部代码  // 从运行上下文中获取参数行(如果没有则取得一个空的行指针  auto& parameter = ctx.GetParameterRow();  iter->SeekToFirst();  int32_t cnt = 0;  while (iter->Valid()) {    if (limit_cnt_ > 0 && cnt++ >= limit_cnt_) {      break;    }    // 遍历表中每一行,计算每一个行的表达式列表    output_table->AddRow(project_gen_.Gen(iter->GetValue(), parameter));    iter->Next();  }  return output_table;}const Row ProjectGenerator::Gen(const Row& row, const Row& parameter) {return CoreAPI::RowProject(fn_, row, parameter, false);}
  • CoreAPI::RowProject函数数据行和参数行来计算表达式列表。它最重要的工作就是调用fn函数。fn函数是查问语句的编译期依据表达式列表Codegen而成的函数。在大节表达式的代码生成(3.4 Codegen: 表达式的代码生成)中咱们曾经介绍过了,咱们在codegen函数的的参数列表中减少了一个参数行指针。

    // 基于输出数据行和参数行计算表达式列表并输入hybridse::codec::Row CoreAPI::RowProject(const RawPtrHandle fn,                                       const hybridse::codec::Row row,                                       const hybridse::codec::Row parameter,                                       const bool need_free) {// 此处省略局部代码auto udf = reinterpret_cast<int32_t (*)(const int64_t, const int8_t*,                                      const int8_t* /*paramter row*/,                                       const int8_t*, int8_t**)>(const_cast<int8_t*>(fn));    auto row_ptr = reinterpret_cast<const int8_t*>(&row);auto parameter_ptr = reinterpret_cast<const int8_t*>(&parameter);int8_t* buf = nullptr;uint32_t ret = udf(0, row_ptr, nullptr, parameter_ptr, &buf);// 此处省略局部代码 return Row(base::RefCountedSlice::CreateManaged(            buf, hybridse::codec::RowView::GetSize(buf)));}

    将来的工作

PreparedStatement的预编译在服务端tablet上实现,预编译产生的编译后果会缓存在tablet上。下次查问时,只有SQL语句和参数类型匹配胜利,即可复用编译后果。但这就意味着,每次客户端执行一次查问,都须要将SQL语句和参数类型传输到服务端tablet上。当查问语句很长时,这部分开销就很可寄存观。因而,咱们的设计仍有优化的空间。能够思考在服务端产生一个惟一的预编译查问QID,这个QID会传回给客户端,保留在PrepareStatemetn的上下文中。只有查问参数的类型不产生扭转,客户端就能够通过QID和参数执行查问。这样,能够缩小查问语句的传输开销。

std::shared_ptr<hybridse::sdk::ResultSet>       ExecuteSQLParameterized(const std::string& db, const std::string& qid,                              std::shared_ptr<SQLRequestRow> parameter,                              ::hybridse::sdk::Status* status) override;

欢送更多开发者关注和参加OpenMLDB开源我的项目。