关于人工智能:OpenMLDB一文了解带参数查询语句paramterized-query-statement

0次阅读

共计 18930 个字符,预计需要花费 48 分钟才能阅读完成。

背景

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()

    @Override
    public 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 currentRow
    if (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 row
std::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 ExprNode
base::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
// return
Status 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 开源我的项目。

正文完
 0