operator does not exist: bigint = bigint[]
主线位置:查询主线的
bind和Parse。bind(&ids)记下的类型是INT8[],prepare把它放进Parse的参数类型列表,服务端按这个类型解析 SQL,id IN ($1)在解析阶段就失败了。bind怎么记类型见 Query、QueryAs 与 SqlSafeStr,Parse带哪些字段见扩展查询。
flowchart TB
vec["bind Vec i64"] --> ty["类型记为 INT8 数组"]
ty --> parse["Parse 声明 $1 类型"]
parse --> sql{"SQL 怎么写"}
sql -->|"id IN ($1)"| err["没有这个运算符"]
sql -->|"id = ANY($1)"| ok["逐个元素比较"]
本页实验在 M2 Max、macOS、rustc 1.99.0、Postgres 16.14 上运行,依赖 sqlx = "=0.9.0"。表 item 有 id 为 1、2、3 的三行。
现象
src/bin/in_list.rs 的第一段:
let ids: Vec<i64> = vec![1, 3];
let r = sqlx::query_scalar::<_, i64>("select id from item where id in ($1)")
.bind(&ids)
.fetch_all(&pool)
.await;
println!("in ($1) -> {}", r.unwrap_err());
in ($1) -> error returned from database: operator does not exist: bigint = bigint[] at line 647
at line 647 是服务端 C 源码的行号(见错误的「0.9 的 at line N」)。换成 query!,错误提前到编译期,但说的是另一件事(src/bin/in_list_macro.rs):
let r = sqlx::query!("select id from item where id in ($1)", &ids[..])
.fetch_all(&pool)
.await?;
error[E0308]: mismatched types
--> src/bin/in_list_macro.rs:5:64
|
5 | let r = sqlx::query!("select id from item where id in ($1)", &ids[..])
| ^
| |
| expected `i64`, found `&[i64]`
| expected due to the type of this binding
出处
IN ($1) 在 SQL 里就是 id = $1,一个占位符只代表一个值,不会被展开成一串。两种报法的区别在于 $1 的类型是谁定的。
手写查询时由 sqlx 定。PgArguments::add 记下每个值的 type_info,Vec<T> 的 Type 是 T::array_type_info(),i64 给出 INT8[]。prepare 把这些类型的 OID 放进 Parse:
// build a list of type OIDs to send to the database in the PARSE command
// we have not yet started the query sequence, so we are *safe* to cleanly make
// additional queries here to get any missing OIDs
let param_types = conn.resolve_types(arg_types).await?;
// flush and wait until we are re-ready
conn.wait_until_ready().await?;
// next we send the PARSE command to the server
conn.inner.stream.write_msg(Parse {
param_types: ¶m_types,
query: sql,
statement: id,
})?;
服务端拿到「$1 是 bigint[]」,去找 bigint = bigint[] 这个运算符,没有,Parse 失败。
query! 时由服务端定。宏不知道你要传什么,只发 SQL 让服务端推断参数类型,再要求实参是对应的 Rust 类型。对两种写法调 Executor::describe(src/bin/in_list_types.rs):
select id from item where id in ($1) params Some(Left([PgTypeInfo(Int8)]))
select id from item where id = any($1) params Some(Left([PgTypeInfo(Int8Array)]))
IN ($1) 推出来是 INT8,宏就要一个 i64。
修法:= ANY($1)
Postgres 的 = ANY(数组) 拿左边的值和数组的每个元素比较,正好是 IN 的意思,而且整个数组只占一个参数。in_list.rs 的其余部分:
let r: Vec<i64> = sqlx::query_scalar("select id from item where id = any($1) order by id")
.bind(&ids)
.fetch_all(&pool)
.await?;
println!("= any($1) -> {r:?}");
let empty: Vec<i64> = vec![];
let r: Vec<i64> = sqlx::query_scalar("select id from item where id = any($1)")
.bind(&empty)
.fetch_all(&pool)
.await?;
println!("any(empty) -> {r:?}");
let r: Vec<i64> = sqlx::query_scalar("select id from item where id <> all($1) order by id")
.bind(&ids)
.fetch_all(&pool)
.await?;
println!("<> all($1) -> {r:?}");
let r: Vec<i64> = sqlx::query_scalar("select id from item where id <> any($1) order by id")
.bind(&ids)
.fetch_all(&pool)
.await?;
println!("<> any($1) -> {r:?}");
// ...
let r = sqlx::query!("select id from item where id = any($1) order by id", &ids[..])
.fetch_all(&pool)
.await?;
println!("query! any -> {:?}", r.iter().map(|r| r.id).collect::<Vec<_>>());
= any($1) -> [1, 3]
any(empty) -> []
<> all($1) -> [2]
<> any($1) -> [1, 2, 3]
query! any -> [1, 3]
- 空数组得到空结果,不需要特判。
NOT IN对应的是<> ALL($1)。<> ANY($1)的意思是「和其中某一个不相等」,只要数组里有两个不同的值,每一行都满足,于是三行全回来了。query!的实参要是切片:&ids[..]或&ids(&Vec<i64>会自动转成&[i64])。直接传ids报expected `&[i64]`, found `Vec<i64>`。
这些写法和注意事项都在 sqlx 的 FAQ 里,它还给出了不展开 IN 列表的理由:「comma-expansion means each possible length of the array generates a different query」。
另一种修法:QueryBuilder
不在 Postgres 上、或者确实需要 IN (...) 的文本时,用 QueryBuilder 的 separated 逐个 push_bind:
async fn by_builder(pool: &sqlx::PgPool, ids: &[i64]) -> sqlx::Result<Vec<i64>> {
let mut qb: QueryBuilder<Postgres> = QueryBuilder::new("select id from item where id in (");
let mut sep = qb.separated(", ");
for id in ids {
sep.push_bind(*id);
}
sep.push_unseparated(") order by id");
println!(" sql: {}", qb.sql().as_str());
qb.build_query_scalar().fetch_all(pool).await
}
builder [1, 3]
sql: select id from item where id in ($1, $2) order by id
-> [1, 3]
builder []
sql: select id from item where id in () order by id
-> error returned from database: syntax error at or near ")" at line 1244
两个代价:
- 列表为空时生成
in (),是语法错误,调用方要先判空。 - 每种长度是一条不同的 SQL,各占语句缓存的一个位置。同一条连接上让长度从 1 跑到 5,两种写法各执行一次(
src/bin/in_list_cache.rs),再看pg_prepared_statements:
select id from item where id in ($1)
select id from item where id = any($1)
select id from item where id in ($1, $2)
select id from item where id in ($1, $2, $3)
select id from item where id in ($1, $2, $3, $4)
select id from item where id in ($1, $2, $3, $4, $5)
select statement from pg_prepared_statements where statement like '%from item where id%' order by prepare_time
= any($1) 只有一条,IN 列表五种长度五条(最后一行是这条查询自己)。语句缓存默认 100 条,LRU;长度分布广时,IN 列表会把别的语句挤出去,见扩展查询。每个元素一个参数,列表上万时还会撞上 65535 个参数的上限,见 too many arguments for query。
怎么确认没中招
- 搜 SQL 里的
in ($:后面跟单个占位符的,都是这个坑。 - 搜
<> any(/!= any(:多半本意是<> all(。 - 用
QueryBuilder拼IN列表的地方,看调用前有没有处理空列表。
小结
- 一个占位符只代表一个值;
IN ($1)绑Vec时,$1被声明成bigint[],服务端报operator does not exist: bigint = bigint[]。query!里同样的 SQL 要求实参是i64。 - Postgres 上写
= ANY($1),绑&Vec或切片:一个参数、一条预备语句,空数组返回空结果。NOT IN写<> ALL($1),不是<> ANY($1)。 QueryBuilder的separated+push_bind能生成IN ($1, $2, ...),但空列表是语法错误,每种长度一条预备语句,长列表受参数个数上限限制。