too many arguments for query: 65536
主线位置:查询主线的
PgConnection::run第一步。arguments是Some时,run先检查参数个数能不能放进u16,放不进就直接返回,连Parse都没发。扩展查询的整体流程见扩展查询协议与语句缓存。
flowchart TB
qb["push_values 拼 N 行"] --> run["PgConnection::run"]
run --> chk{"参数个数 放得进 u16"}
chk -->|"否"| err["Error::Protocol"]
chk -->|"是"| prep["Parse Bind Execute"]
fix["UNNEST 每列一个数组"] --> two["参数个数等于列数"]
two --> run
本页实验在 M2 Max、macOS、rustc 1.99.0、Postgres 16.14 上运行,依赖 sqlx = "=0.9.0"。
现象
用 QueryBuilder::push_values 做批量插入,行数一多就报:
encountered unexpected or invalid data: PgConnection::run(): too many arguments for query: 65536 (sqlx_postgres::connection::executor:222)
数据库没报错,服务端什么也没收到。报错的分类也容易误导:前半句 encountered unexpected or invalid data 是 Error::Protocol 的 Display,这个变体的文档说它「should indicate there is a programming error in a SQLx driver or there is something corrupted with the connection」,可这里既不是驱动的 bug,连接也没坏。
最小复现
src/bin/too_many_args.rs,两列的表,每行两个参数:
async fn bulk(pool: &sqlx::PgPool, rows: i64) -> Result<u64, sqlx::Error> {
let mut qb: QueryBuilder<Postgres> = QueryBuilder::new("insert into items (id, name) ");
qb.push_values(0..rows, |mut b, i| {
b.push_bind(i).push_bind(format!("item {i}"));
});
Ok(qb.build().execute(pool).await?.rows_affected())
}
依次插 32767 行、32768 行,然后在同一条连接上 select 1;再用一列参数、一列字面量凑出恰好 65535 个参数:
--- QueryBuilder::push_values,每行 2 个参数 ---
32767 行 = 65534 个参数 => Ok(32767)
32768 行 = 65536 个参数 => Err
Display: encountered unexpected or invalid data: PgConnection::run(): too many arguments for query: 65536 (sqlx_postgres::connection::executor:222)
Debug: Protocol("PgConnection::run(): too many arguments for query: 65536 (sqlx_postgres::connection::executor:222)")
出错后同一条连接还能用: select 1 = 1
--- 恰好 65535 个参数 ---
65535 => Ok(65535)
上限是 65535,含 65535。QueryBuilder::push_bind 的文档写的是 Postgres「asserts that the number of parameters is in the range [0, 65535)」,按这个说法 65535 应该不行,实际可以。
出处
检查在 PgConnection::run 里,第 221 行:
let format = if let Some(mut arguments) = arguments {
// Check this before we write anything to the stream.
//
// Note: Postgres actually interprets this value as unsigned,
// making the max number of parameters 65535, not 32767
// https://github.com/launchbadge/sqlx/issues/3464
// https://www.postgresql.org/docs/current/limits.html
let num_params = u16::try_from(arguments.len()).map_err(|_| {
err_protocol!(
"PgConnection::run(): too many arguments for query: {}",
arguments.len()
)
})?;
num_params 要写进 Bind 消息的参数个数字段,这个字段两个字节。转换失败就在写任何东西之前返回,所以写缓冲是干净的,连接照常可用(上面的 select 1)。报错末尾的 (sqlx_postgres::connection::executor:222) 是 err_protocol! 用 module_path!() 和 line!() 加上的,指的是 sqlx 自己源码里 err_protocol!( 那一行,和 SQL 无关。
要在代码里识别它,只能匹配 Error::Protocol(msg) 再看 msg 里有没有 too many arguments for query:没有专门的变体,也没有错误码。
0.8.0 的 run 写的是 num_params: arguments.types.len() as i16,超过范围静默截断,发给服务端的参数个数是错的。#3441(「fix: audit protocol handling」,首次发布于 0.8.1,对应安全公告 RUSTSEC-2024-0363)把这类 as 换成带检查的转换,这里成了 i16::try_from。随后的 #3464 指出:「This will error if arguments.len() > 32767」,而 Postgres「interprets this Int16 as unsigned, meaning the max number of parameters supported is actually 65535」。#3465(「max number of binds is 65535, not 32767 (regression)」,首次发布于 0.8.2)改成 u16::try_from,也就是今天上面那段代码和它的注释。
修法一:按列数分批
每批行数 = 65535 / 列数,向下取整。两列就是 32767 行一批;实验用 30000 行一批插 100 万行:
for chunk in all.chunks(30_000) {
let mut qb: QueryBuilder<Postgres> = QueryBuilder::new("insert into items (id, name) ");
qb.push_values(chunk, |mut b, i| {
b.push_bind(*i).push_bind(format!("item {i}"));
});
total += qb.build().execute(&pool).await?.rows_affected();
}
--- 对照:push_values 每批 30000 行 ---
1000000 行,34 批 => rows_affected = 1000000,1887 ms
代价:行数不同 SQL 文本就不同,最后那一批的 SQL 和前面的不一样,每种都在连接的语句缓存里占一条(见 Query 与 SqlStr);一批 6 万多个 $N 的 SQL 文本本身也很长。给表加一列时别忘了重算批大小。
修法二:每列一个数组,UNNEST 展开
把每一列收集成一个 Vec,绑成数组参数,让 Postgres 用 unnest 展开成行。不管多少行,参数都只有两个:
let ids: Vec<i64> = (0..rows).collect();
let names: Vec<String> = ids.iter().map(|i| format!("item {i}")).collect();
let r = sqlx::query(
"insert into items (id, name) select * from unnest($1::int8[], $2::text[])",
)
.bind(&ids)
.bind(&names)
.execute(&pool)
.await?;
--- UNNEST:两个数组参数 ---
32768 行 => rows_affected = 32768,36 ms
1000000 行 => rows_affected = 1000000,1164 ms
SQL 文本固定,缓存里只有一条;100 万行一条语句插完,比分批快。Vec<i64> 编码成 INT8[]、Vec<String> 编码成 TEXT[],数组类型怎么编码见 Postgres 类型。
有一个要自己保证的前提:各个数组一样长。unnest 收多个数组时按最长的那个展开,短的用 NULL 补齐(src/bin/unnest_len.rs,3 个 id、2 个 name):
[(Some(1), Some("a")), (Some(2), Some("b")), (Some(3), None)]
name 列是 not null 时这会变成一个约束错误,可空的列就静默插进 NULL。从同一个结构体切片里逐字段收集数组,长度自然一致。
WHERE id IN (...) 拼出很多参数是同一个问题,用 = ANY($1) 绑一个数组,见 in-list。
怎么确认没中招
- 搜
push_values、push_bind:拼进去的行数是不是来自外部输入(导入文件、请求体),有没有按列数设上限。 - 用最大批量跑一次测试:行数 × 每行参数个数 ≤ 65535。
- 日志或告警里搜
too many arguments for query;它是Error::Protocol,按错误码统计数据库错误的监控看不到它。
小结
- Postgres 一条语句最多 65535 个参数(含 65535),超过时
PgConnection::run在发任何消息之前返回Error::Protocol,连接不受影响。 - 报错前缀
encountered unexpected or invalid data和末尾的(sqlx_postgres::connection::executor:222)都来自 sqlx 自己,不代表协议出错或连接损坏。 - 0.8.1 加了检查但上限错成 32767(#3441、#3464),0.8.2 改回 65535(#3465)。
- 修法:按
65535 / 列数分批;或者每列绑一个数组、unnest展开,参数个数等于列数,SQL 文本固定。用unnest时各数组必须一样长。