ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

Apache DataFusion 预编译语句(PREPARE/EXECUTE):从占位符参数到可复用查询的实现原理与实战

2026/9/25 9:31:37 拓冰建站 浏览量
Apache DataFusion 预编译语句(PREPARE/EXECUTE):从占位符参数到可复用查询的实现原理与实战 大数据数据分析后端【免费下载链接】datafusionApache DataFusion SQL Query Engine项目地址https://gitcode.com/gh_mirrors/datafu/datafusion点击查看免费下载DataFusion 通过PREPARE/EXECUTE语句支持 SQL 预编译先把带$1、$2等占位符的查询编译并保存在会话中之后再以不同参数值反复执行。本文基于官方文档 Prepared Statements 的完整用法展开并结合datafusion源码与 SQL 逻辑测试讲清预编译语句的类型推断、位置参数、执行期的参数校验与类型转换机制帮助你在应用层高效复用高频查询模板。1. 基本概念与语法PREPARE语句允许创建并存储一条带占位符参数的 SQL 语句预编译语句随后可以携带不同参数被高效地重复执行。其语法形如PREPARE 语句名([参数类型, ...]) AS SELECT 语句;官方文档给出的标准示例创建名为greater_than的预编译语句选出所有列a大于给定参数的记录PREPARE greater_than(INT) AS SELECT * FROM example WHERE a $1;创建完成后即可按需传入参数执行EXECUTE greater_than(20);占位符采用$n形式的数字位置参数$1、$2按出现顺序与EXECUTE时传入的参数列表一一对应。需要注意的是占位符编号后必须紧跟数字$foo这类命名占位符不被支持——逻辑测试 prepare.slt 中明确断言了对PREPARE my_plan(INT) AS SELECT ... WHERE age $foo会报错Unknown placeholder: $foo。Rust API 用法在 Rust 侧预编译语句通过SessionContext::sql直接以 SQL 文本下发文档给出的完整示例如下注册 CSV 表后执行PREPARE再用EXECUTE取回 DataFrameuse datafusion::prelude::*; #[tokio::main] async fn main() - datafusion::error::Result() { // 注册表 let ctx SessionContext::new(); ctx.register_csv(example, tests/data/example.csv, CsvReadOptions::new()).await?; // 创建预编译语句 greater_than let prepare_sql PREPARE greater_than(INT) AS SELECT * FROM example WHERE a $1; ctx.sql(prepare_sql).await?; // 执行预编译语句 greater_than let execute_sql EXECUTE greater_than(20); let df ctx.sql(execute_sql).await?; // 执行并打印结果 df.show().await?; Ok(()) }这段代码并非只是示意datafusion/core/src/lib.rs 通过doc_comment::doctest!将 prepared_statements.md 中的 Rust 示例直接注册为 doctest也就是说文档里的每一段 Rust 代码都会被 CI 编译并实际运行保证了示例与当前代码库行为的一致性示例中tests/data/example.csv是相对于datafusion/core目录的路径。2. 参数类型推断如果PREPARE时不显式指定参数类型DataFusion 会在执行时自动推断。文档对应示例PREPARE greater_than AS SELECT * FROM example WHERE a $1; EXECUTE greater_than(20);此时$1的期望类型由所在表达式的类型推导链决定例如与列a的比较运算会给出类型约束。从逻辑测试可以看到无类型声明的用法覆盖面更广不仅限于SELECT ... WHERE还适用于投影和VALUESPREPARE my_plan AS SELECT $1; EXECUTE my_plan(Foo); -- 返回 Foo PREPARE my_plan AS SELECT * FROM person WHERE first_name LIKE $1; EXECUTE my_plan(j%);以上取自 prepare.slt 的测试用例。实践建议能明确写出参数类型时建议显式声明如PREPARE greater_than(INT)因为显式声明会在执行期把传入参数强制转换为声明类型见下文第 4 节行为更可控省略类型声明则依赖推断适合参数语义灵活例如LIKE模式串、通用字符串列的场景。3. 位置参数多参数当语句包含多个参数时使用位置参数按序传递。文档示例PREPARE greater_than(INT, DOUBLE) AS SELECT * FROM example WHERE a $1 AND b $2; EXECUTE greater_than(20, 23.3);对应 Rust 侧只需替换 SQL 文本即可// 创建预编译语句 greater_than let prepare_sql PREPARE greater_than(INT, DOUBLE) AS SELECT * FROM example WHERE a $1 AND b $2; ctx.sql(prepare_sql).await?; // 执行预编译语句 greater_than let execute_sql EXECUTE greater_than(20, 23.3); let df ctx.sql(execute_sql).await?;占位符在语句体内可任意位置、任意多次出现包括IN、ANY/ALL子查询比较、GROUP BY/HAVING、甚至LIMIT子句。例如逻辑测试中验证过-- LIMIT 子句中的参数对应 issue #12294 场景 PREPARE get_N_rand_ints_from_last_run(INT) AS SELECT id FROM test WHERE run_id foo ORDER BY random() LIMIT $1; EXECUTE get_N_rand_ints_from_last_run(2); -- 子查询比较中复用同一参数 PREPARE my_plan AS SELECT id FROM person WHERE $1 IN (SELECT age FROM person); EXECUTE my_plan(20);此外EXECUTE传入的实参不要求必须是字面量测试中EXECUTE my_plan6(10 10)这类非字面量表达式也能正确执行——执行器会先把参数表达式做常量简化见下节。4. 源码剖析EXECUTE 的执行链路预编译语句的执行入口在 datafusion/core/src/execution/context/mod.rs 的execute_prepared方法。从源码可以确认以下关键行为按名称查找语句self.state.read().get_prepared(name)从会话状态中取出已存储的Prepared对象包含逻辑计划与参数字段元数据找不到则报Prepared statement {name} does not exist。参数必须是可简化为字面量的表达式execute_prepared使用ExprSimplifier逐个简化传入的参数表达式只接受Expr::Literal源码注释即 “Only allow literals as parameters for now”无法简化为字面量的表达式会触发Unsupported parameter type错误。这解释了为什么EXECUTE my_plan6(10 10)能工作——10 10先被简化为字面量20。参数个数校验若预编译时声明了参数类型而数量与传入不符报错Prepared statement {name} expects {n} parameters, but {m} provided。按声明类型强制转换当prepared.fields非空时每个实参都会向声明的类型做 cast。这正是文档中PREPARE greater_than(INT)的语义保证——EXECUTE greater_than(20)会把字符串20转为整数 20 参与比较而EXECUTE my_plan6(foo)会报Cast error: Cannot cast string foo to value of Int32 type均有 prepare.slt 中的用例佐证。另外PREPARE语句在逻辑计划层面会生成Prepare节点。逻辑计划构建入口在 datafusion/expr/src/logical_plan/builder.rs 的prepare方法而优化器同样会作用于预编译语句——逻辑测试验证了OptimizeProjections规则会对PREPARE my_plan(INT) AS SELECT id $1 FROM person把投影下推01)Prepare: my_plan [Int32] 02)--Projection: person.id $1 03)----TableScan: person projection[id]也就是说预编译语句享受与直接执行 SQL 相同的优化器流水线占位符参数会作为表达式保留在投影中参与类型推导。5. 生命周期管理DEALLOCATE 与重名处理预编译语句的生命周期与所在会话绑定可用DEALLOCATE显式释放也可写作DEALLOCATE PREPARE namePREPARE my_plan(STRING, STRING) AS SELECT * FROM (VALUES(1, $1), (2, $2)) AS t (num, letter); EXECUTE my_plan(Foo, Bar); DEALLOCATE my_plan; -- 释放后 EXECUTE 将报 does not exist EXECUTE my_plan(Foo, Bar); -- error: Prepared statement my_plan does not existprepare.slt 完整覆盖了这一生命周期语义重名冲突对已存在的名称再次PREPARE会报Prepared statement my_plan already existsDEALLOCATE释放后可以立即用同名重新创建EXECUTE必须带名称EXEC()、EXECUTE()、EXEC(any-string)等无名称写法统一报EXECUTE statement requires a name而不是 panicEXECUTE可以不带参数列表对无参语句EXECUTE my_plan2合法对声明了 1 个参数的语句省略参数则报expects 1 parameters, but 0 provided。6. 常见错误速查综合官方文档语义与逻辑测试断言预编译语句相关的典型报错及含义如下报错信息触发场景SQL error: ParserErrorPREPARE后缺少名称如PREPARE AS SELECT ...或占位符出现在不支持的位置如age IS $1Unknown placeholder: $foo使用了非数字编号的占位符Prepare specifies 1 data types but query has 2 parameters声明的参数类型个数与语句中出现的$n个数不一致多声明或少声明均会报错Prepared statement {name} does not existEXECUTE/DEALLOCATE未创建的或已释放的语句Prepared statement {name} already exists重复创建同名语句Prepared statement {name} expects N parameters, but M providedEXECUTE实参个数不匹配Cast error: Cannot cast ...实参值无法转换为PREPARE时声明的参数类型一个容易被忽略的边界如果PREPARE时声明了类型但语句体里根本没有参数如PREPARE my_plan(INT) AS SELECT id, age FROM person WHERE age 10同样会报Prepare specifies 1 data types but query has 0 parameters——类型声明必须与语句体内实际使用的占位符严格对应。7. 用 EXPLAIN 观察预编译计划DataFusion 支持对预编译语句本身做EXPLAIN便于验证占位符处的类型推导结果-- PREPARE 阶段的逻辑计划占位符处显示为 $1 EXPLAIN PREPARE my_plan(INT, INT) AS SELECT $1 AS one, $2 AS two; -- logical_plan -- 01)Prepare: my_plan [Int32, Int32] -- 02)--Projection: $1 AS one, $2 AS two -- 03)----EmptyRelation: rows1 -- EXECUTE 阶段的逻辑计划显示代入后的具体参数值 EXPLAIN EXECUTE my_plan(10*2 1, Foo); -- logical_plan Execute: my_plan params[Int64(21), Utf8(Foo)]可以看出PREPARE的EXPLAIN展示的是带类型标注的模板计划而EXECUTE的EXPLAIN会展示参数表达式简化后的具体标量值且EXPLAIN PREPARE不产生副作用不会真正占用语句名称可重复执行。8. 小结DataFusion 的预编译语句提供了一套完整的PREPARE → EXECUTE → DEALLOCATE工作流PREPARE以$n位置占位符存储查询模板并可选声明参数类型EXECUTE以字面量参数反复执行并在执行期做类型推断与强制转换DEALLOCATE释放会话级资源。从源码看execute_prepared参数必须先可简化为字面量且会按声明类型做 cast 与个数校验从 prepare.slt 看LIKE、IN/ANY/ALL子查询、HAVING、LIMIT等场景以及各类错误路径都有测试保障。对于需要以不同参数高频复用同一查询模板的应用例如参数化报表、API 后端的查询封装这一机制配合显式参数类型声明可以获得既灵活又类型安全的 SQL 使用方式。赞分享大数据数据分析后端【免费下载链接】datafusionApache DataFusion SQL Query Engine项目地址https://gitcode.com/gh_mirrors/datafu/datafusion点击查看免费下载相关推荐Presto PREPARE 语句详解预编译 SQL、参数占位符与配套命令实战Presto PREPARE 语句详解预编译 SQL、参数占位符与配套命令实战 Presto 的 PREPARE 语句用于在当前会话中按名称预编译一条 SQL大数据数据库后端drizzle-orm-pg 0.12.0-beta.40预编译语句、占位符与查询构建器 execute 支持全解析drizzle orm pg 0.12.0 beta.40预编译语句、占位符与查询构建器 execute 支持全解析 drizzle orm pg 0.12.后端数据库ORMlibpqxx 预编译语句Prepared Statements实战指南从 prepare 到 exec_prepared 的完整用法与底层原理libpqxx 预编译语句Prepared Statements实战指南从 prepare 到 exec_prepared 的完整用法与底层原理 预编译语网络安全密码学上一篇Riot.js 中集成 tsParticles 粒子动画riot-particles-demo 的启动、测试与构建实战指南下一篇从30秒到3秒Easy Dataset启动速度优化全指南创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考