写点什么

OpenMLDB:一文了解带参数查询语句(paramterized query statement) 的细节

发布于: 2021 年 10 月 21 日
OpenMLDB:一文了解带参数查询语句(paramterized query statement) 的细节

背景

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/Prepared_statement)

在数据库系统中,带参数的语句(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;
复制代码

在示例中,我们计算了用户`ABC123456789` 从`2020-05-22 02:43:40` 到 `2020-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 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 rowstd::shared_ptrhybridse::sdk::ResultSetExecuteSQLParameterized(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_castbrpc::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`类型的表达式节点并存放在语法树中。

/// Convert zetasql::ASTExpression 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->GetAsOrNullzetasql::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::vectorstd::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_rowCHECK_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_ptrhybridse::sdk::ResultSetExecuteSQLParameterized(const std::string& db, const std::string& qid,std::shared_ptr<SQLRequestRow> parameter,::hybridse::sdk::Status* status) override;

欢迎更多开发者关注和参与OpenMLDB开源项目。

发布于: 2021 年 10 月 21 日阅读数: 21
用户头像

AI for every developer,AI for everyone 2021.06.21 加入

还未添加个人简介

评论

发布
暂无评论
OpenMLDB:一文了解带参数查询语句(paramterized query statement) 的细节