Rust in Everything
基于 sqlx 0.9.0

一次查询的全过程

主线位置:查询主线从头到尾,外加连接池主线的借和还。本页不深入任何一个 trait,只把一次 fetch_one 经过的函数串起来,并用抓包确认每一步实际发了哪些协议消息;每一段末尾链到详细讲它的那一章。

sequenceDiagram
  participant App as 调用方
  participant Pool as Pool
  participant Conn as PgConnection
  participant PG as Postgres
  App->>Pool: fetch_optional(query)
  Pool->>Conn: acquire 取空闲连接
  Conn->>PG: Sync(ping)
  PG-->>Conn: ReadyForQuery
  Note over Conn: 语句缓存没命中
  Conn->>PG: Parse Describe Sync
  PG-->>Conn: ParseComplete 等四条
  Conn->>PG: Bind Execute Close Sync
  PG-->>Conn: BindComplete DataRow 等
  Conn-->>App: PgRow 经 FromRow
  Pool->>Conn: Drop 后 spawn 归还
  Conn->>PG: Sync(ping)
  PG-->>Conn: ReadyForQuery

实验:抓一次查询的协议消息

要看 sqlx 发了什么,日志不够用(RUST_LOG=sqlx=trace 也只打语句级的事件,见怎么读这一卷),得看线上的字节。实验 crate 里写了一个 tokio TCP 代理:监听 127.0.0.1 的随机端口,每进来一条连接就连到 Postgres,两个方向各起一个任务转发字节,同时按协议格式(一个类型字节加一个 4 字节大端长度;前端的第一条 StartupMessage 没有类型字节)切出完整的消息,每次 read 之后把这次凑齐的消息打成一行。核心是这个循环:

loop {
  let n = match from.read(&mut buf).await {
    Ok(0) | Err(_) => return,
    Ok(n) => n,
  };
  if to.write_all(&buf[..n]).await.is_err() {
    return;
  }
  pending.extend_from_slice(&buf[..n]);
  let mut names = Vec::new();
  loop {
    // ... 第一条 StartupMessage 单独处理
    if pending.len() < 5 {
      break;
    }
    let ty = pending[0];
    let len = u32::from_be_bytes(pending[1..5].try_into().unwrap()) as usize;
    if pending.len() < 1 + len {
      break;
    }
    let body = pending[5..1 + len].to_vec();
    names.push(describe(ty, &body, frontend));
    pending.drain(..1 + len);
  }
  if !names.is_empty() && !quiet.load(Ordering::SeqCst) {
    let arrow = if frontend { "C->S" } else { "S->C" };
    println!("  [conn {conn_no}] {arrow} {}", names.join(", "));
  }
}

describe 把类型字节翻成名字,Parse 和 Bind 顺带解出语句名和 SQL,DataRow 解出每一列的字节长度。一次 read 对应对端的一次写,所以同一行里的几条消息是对端一次 flush 出来的。

主程序先用一条直连建表,再建一个 max_connections(1) 的池,连接串指向代理(sslmode=disable,否则字节是加密的),建池时的握手不打印:

const SQL: &str = "select id, title from posts where id = $1";

#[tokio::main]
async fn main() -> anyhow::Result<()> {
  // setup on a direct connection, not through the proxy
  let direct: PgConnectOptions = std::env::var("DATABASE_URL")?.parse()?;
  let mut setup = sqlx::PgConnection::connect_with(&direct).await?;
  sqlx::raw_sql(
    "create schema if not exists intro;
     drop table if exists intro.posts;
     create table intro.posts (id int8 primary key, title text not null);
     insert into intro.posts values (1, 'hello sqlx');",
  )
  .execute(&mut setup)
  .await?;

  let p = proxy::start("192.168.64.106:5432").await?;
  let opts = direct
    .clone()
    .host("127.0.0.1")
    .port(p.port)
    .ssl_mode(sqlx::postgres::PgSslMode::Disable)
    .options([("search_path", "intro")]);
  p.quiet.store(true, Ordering::SeqCst);
  let pool = PgPoolOptions::new().max_connections(1).connect_with(opts).await?;
  p.quiet.store(false, Ordering::SeqCst);

  for round in ["第一次", "第二次"] {
    println!("--- {round} query_as + bind + fetch_one ---");
    let row: (i64, String) = sqlx::query_as(SQL).bind(1_i64).fetch_one(&pool).await?;
    println!("结果 {row:?}");
    settle().await;
  }
  // ... 后面三段见下文
}

// let the spawned return-to-pool task finish, so its traffic is printed before the next header
async fn settle() {
  tokio::time::sleep(std::time::Duration::from_millis(200)).await;
}

settle 睡 200 毫秒,让归还连接的后台任务在打印下一段标题之前跑完。M2 Max、macOS、rustc 1.99.0、Postgres 16.14 上的输出(前两段):

--- 第一次 query_as + bind + fetch_one ---
  [conn 1] C->S Sync
  [conn 1] S->C ReadyForQuery(I)
  [conn 1] C->S Parse(sqlx_s_1, "select id, title from posts where id = $1"), Describe(S, sqlx_s_1), Sync
  [conn 1] S->C ParseComplete, ParameterDescription, RowDescription, ReadyForQuery(I)
  [conn 1] C->S Bind(portal unnamed, stmt sqlx_s_1), Execute, Close(P, unnamed), Sync
  [conn 1] S->C BindComplete, DataRow(列长 8/10), CommandComplete("SELECT 1"), CloseComplete, ReadyForQuery(I)
结果 (1, "hello sqlx")
  [conn 1] C->S Sync
  [conn 1] S->C ReadyForQuery(I)
--- 第二次 query_as + bind + fetch_one ---
  [conn 1] C->S Sync
  [conn 1] S->C ReadyForQuery(I)
  [conn 1] C->S Bind(portal unnamed, stmt sqlx_s_1), Execute, Close(P, unnamed), Sync
  [conn 1] S->C BindComplete, DataRow(列长 8/10), CommandComplete("SELECT 1"), CloseComplete, ReadyForQuery(I)
结果 (1, "hello sqlx")
  [conn 1] C->S Sync
  [conn 1] S->C ReadyForQuery(I)

两次执行的区别只有一处:第二次没有 Parse / Describe 那一来一回,Bind 直接引用第一次建好的 sqlx_s_1。每次查询前后各有一对孤零零的 Sync / ReadyForQuery,那是连接池借出和归还时的 ping。下面按调用顺序把这几行对到源码上。

1. 构造:query_as 和 bind

query_as 包一层 query:

pub fn query<'a, DB>(sql: impl SqlSafeStr) -> Query<'a, DB, <DB as Database>::Arguments>
where
    DB: Database,
{
    Query {
        database: PhantomData,
        arguments: Some(Ok(Default::default())),
        statement: Either::Left(sql.into_sql_str()),
        persistent: true,
    }
}

arguments 一开始就是 Some(Ok(空的 PgArguments)),persistent 默认 true。bind 调 Arguments::add,把 1_i64 编码进参数缓冲区,记下它的类型;编码失败时把错误存进 arguments,等执行时再报。到这里还没有任何 I/O。SqlSafeStr、bind 的细节见 Query、QueryAs 与 SqlSafeStr,Encode 见类型:Type、Encode、Decode。

2. fetch_one 就是 fetch_optional

QueryAs::fetch_one:

        self.fetch_optional(executor)
            .await
            .and_then(|row| row.ok_or(Error::RowNotFound))

fetch_optional 把内层的 Query 交给执行器的 fetch_optional,拿回一个 PgRow 后调 O::from_row(&row)。O 是 (i64, String),元组的 FromRow 按下标逐列 try_get,见 Row 与 FromRow;没有行就是 RowNotFound,见错误。

3. &Pool 借一条连接

执行器是 &pool。&Pool 的 Executor 的 fetch_optional 只有一行:

        Box::pin(async move { pool.acquire().await?.fetch_optional(query).await })

acquire 从空闲队列取出建池时打开的那条连接,test_before_acquire 默认为 true,于是先 ping 一次:

    async fn ping(&mut self) -> Result<(), Error> {
        // Users were complaining about this showing up in query statistics on the server.
        // By sending a comment we avoid an error if the connection was in the middle of a rowset
        // self.execute("/* SQLx ping */").map_ok(|_| ()).boxed()

        // The simplest call-and-response that's possible.
        self.write_sync();
        self.wait_until_ready().await
    }

这就是输出里每段开头那一对 Sync / ReadyForQuery:Sync 本身不执行任何东西,服务端只回一个 ReadyForQuery,状态字节 I 表示不在事务里。注释说以前 ping 发的是一条 SQL 注释,后来因为会出现在服务端的语句统计里而改掉了。借出之后,PoolConnection 解引用成 &mut PgConnection,调到 &mut PgConnection 的 Executor。为什么 &Pool、&mut PgConnection 都能当执行器,见 Executor 与 Acquire;acquire 的完整流程见获取连接。

📝NOTE
没有蠢问题

问: 每次借连接都多一个来回,归还时又一个,能不能关掉?

答: 借出这一侧可以。PgPoolOptions::test_before_acquire(false) 之后,同样两次查询的抓包里,查询前的那对 Sync / ReadyForQuery 没有了,查询后的还在:

--- 第一次(test_before_acquire(false))
  [conn 1] C->S Parse(sqlx_s_1, "select $1::int8"), Describe(S, sqlx_s_1), Sync
  [conn 1] S->C ParseComplete, ParameterDescription, RowDescription, ReadyForQuery(I)
  [conn 1] C->S Bind(portal unnamed, stmt sqlx_s_1), Execute, Close(P, unnamed), Sync
  [conn 1] S->C BindComplete, DataRow(列长 8), CommandComplete("SELECT 1"), CloseComplete, ReadyForQuery(I)
结果 7
  [conn 1] C->S Sync
  [conn 1] S->C ReadyForQuery(I)
--- 第二次(test_before_acquire(false))
  [conn 1] C->S Bind(portal unnamed, stmt sqlx_s_1), Execute, Close(P, unnamed), Sync
  [conn 1] S->C BindComplete, DataRow(列长 8), CommandComplete("SELECT 1"), CloseComplete, ReadyForQuery(I)
结果 7
  [conn 1] C->S Sync
  [conn 1] S->C ReadyForQuery(I)

归还那一次是 return_to_pool 里无条件的 self.raw.ping(),没有开关。它不只是探活:如果查询的 future 在中途被 drop,连接上可能还有没读完的消息,Transaction 被 drop 时排进写缓冲区的 ROLLBACK 也要靠这次 wait_until_ready 发出去、等到回应。不先把连接收拾干净就放回空闲队列,下一个借到它的人会读到上一个人的结果。归还是在后台任务里做的,不占调用方的时间,见归还与维护。

4. PgConnection 的 fetch_optional 一直读到最后

fetch_optional 从 Query 里取出 take_arguments()、persistent()、sql(),交给 run,然后读它返回的流:

            let mut s = pin!(self.run(sql, arguments, persistent, metadata).await?);

            // With deferred constraints we need to check all responses as we
            // could get a OK response (with uncommitted data), only to get an
            // error response after (when the deferred constraint is actually
            // checked).
            let mut ret = None;
            while let Some(result) = s.try_next().await? {
                match result {
                    Either::Right(r) if ret.is_none() => ret = Some(r),
                    _ => {}
                }
            }
            Ok(ret)

第一行留下,后面的行读了就扔,直到流结束(ReadyForQuery)。所以 fetch_one 并不会「读到一行就停」:查询能返回很多行时,服务端照样全部发过来,客户端全部读完。fetch_one 的文档也提醒「for best performance, ensure the query returns at most one row」。

📝NOTE
冷知识:fetch_optional 以前读到第一行就返回

0.7.4 之前,这里的循环是 if let Either::Right(r) = s { return Ok(Some(r)); },读到第一行立刻返回。问题出在可延迟约束(DEFERRABLE INITIALLY DEFERRED)上:服务端可能先发回数据行,到语句或事务结束检查约束时才发 ErrorResponse,提前返回就看不到这个错误。issue #1370 的标题就是「PostgreSQL deferred FK constraint violations are silently dropped」。PR #2913(「Fix handling of deferred constraints」)改成现在这样:记下第一行,接着读完所有响应,上面那段注释就是这次加的。CHANGELOG 把它记在 0.7.4 的 Fixed 里:「Fix handling of deferred constraints」。

5. run:查缓存,没命中就 prepare

PgConnection::run 先建一个 QueryLogger(drop 时打那条 sqlx::query 日志),再 wait_until_ready:把写缓冲区里剩下的东西 flush 出去,把之前每个 Sync 欠下的 ReadyForQuery 读完。arguments 是 Some 时走扩展查询,先检查参数个数能不能放进 u16(第 221 行,超过 65535 报 too many arguments for query),然后 get_or_prepare:

        if let Some(statement) = self.inner.cache_statement.get_mut(sql) {
            return Ok((*statement).clone());
        }

        let statement = prepare(
            self,
            sql,
            parameters,
            metadata,
            persistent,
            resolve_column_origin,
        )
        .await?;

        if persistent && self.inner.cache_statement.is_enabled() {
            if let Some((id, _)) = self.inner.cache_statement.insert(sql, statement.clone()) {
                self.inner.stream.write_msg(Close::Statement(id))?;
                self.write_sync();

                self.inner.stream.flush().await?;

                self.wait_for_close_complete(1).await?;
                self.recv_ready_for_query().await?;
            }
        }

缓存是每条连接一个 StatementCache,内部是 hashlink 的 LruCache<String, T>,键是 SQL 原文,值是语句 id 和元数据;容量来自 PgConnectOptions 的 statement_cache_capacity,默认 100。插入时挤掉了最久没用的那条,就发一条 Close::Statement 让服务端也释放它。

第一次执行缓存是空的,走 prepare:

    let id = if persistent {
        let id = conn.inner.next_statement_id;
        conn.inner.next_statement_id = id.next();
        id
    } else {
        StatementId::UNNAMED
    };

    // ...
    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: &param_types,
        query: sql,
        statement: id,
    })?;

    if metadata.is_none() {
        // get the statement columns and parameters
        conn.inner
            .stream
            .write_msg(message::Describe::Statement(id))?;
    }

    // we ask for the server to immediately send us the result of the PARSE command
    conn.write_sync();
    conn.inner.stream.flush().await?;

这对应抓包里的第二行:Parse(sqlx_s_1, ...)、Describe(S, sqlx_s_1)、Sync 一次发出。语句名是 sqlx_s_ 加一个每条连接自增的序号;persistent(false) 时用未命名语句,执行完不进缓存。resolve_types 把参数的 PgTypeInfo 换成 OID,int8 这种内置类型 OID 是已知的,不用查库。Parse 后面紧跟 Sync 并且立刻 flush、等回应,所以 prepare 是单独一个来回:sqlx 要先拿到 ParameterDescription 和 RowDescription(参数和列的类型),才能在下一步决定怎么 Bind、怎么解码。服务端回的四条消息就是第三行。元数据整理成一个 Arc<PgStatementMetadata>,和语句 id 一起存进缓存。

第二次执行,get_mut(sql) 命中,直接返回 (sqlx_s_1, 元数据),抓包里那一来一回整个消失。这一节的协议细节见扩展查询协议和协议消息。

6. Bind、Execute、Close、Sync,一次 flush

拿到语句之后,run 一口气写四条消息:

            self.inner.stream.write_msg(Bind {
                portal: PortalId::UNNAMED,
                statement,
                formats: &[PgValueFormat::Binary],
                num_params,
                params: &arguments.buffer,
                result_formats: &[PgValueFormat::Binary],
            })?;

            // ...
            self.inner.stream.write_msg(message::Execute {
                portal: PortalId::UNNAMED,
                // Non-zero limits cause query plan pessimization by disabling parallel workers:
                // https://github.com/launchbadge/sqlx/issues/3673
                limit: 0,
            })?;
            // ...
            self.inner
                .stream
                .write_msg(Close::Portal(PortalId::UNNAMED))?;

            // ...
            self.write_sync();

Bind 把参数绑到语句上,生成一个未命名 portal;参数格式和结果格式都只写一个 Binary,按协议的规定表示「全部用二进制」。抓包里 DataRow(列长 8/10) 的 8 就是 int8 的二进制形式,10 是 hello sqlx 的 UTF-8 字节。Execute 的 limit: 0 表示不限行数。Close::Portal 让服务端在执行完后马上释放这个 portal,而不是等到下一次 Bind 覆盖它。最后一个 Sync 结束这一组,服务端处理完回 ReadyForQuery。四条消息只在写缓冲区里攒着,run 末尾一次 flush,对应抓包里的一行。

📝NOTE
冷知识:fetch_one 以前发的是 limit: 1

0.8.4 之前,run 有一个 limit: u8 参数:fetch_many 传 0,fetch_optional(也就是 fetch_one)传 1。issue #3673(「Parallel workers not used on Postgres」)报告一条查询在 sqlx 里没有用上并行 worker,「taking double the time to execute query than other tools」。维护者在讨论里引了 Postgres 文档:客户端发送 fetch count 非零的 Execute 消息时,并行计划不能并行执行;并解释了原来的用意:「We currently send an Execute with a limit of 1 for fetch_optional and fetch_one to ensure the server does not waste resources sending additional rows if the user forgot to ensure that the query only returns one row.」0.8.4 的发布 PR #3819 去掉了这个参数,CHANGELOG 记为「fix(postgres): send limit: 0 for all Execute messages」,代码里留下了上面那行注释。代价就是上一节说的:多出来的行服务端照发、客户端照读,所以文档才提醒只返回一行。0.8.4 发布后被撤回,0.8.5 是第一个带这项改动、仍可用的版本。

7. 读结果:DataRow 变成 PgRow

run 返回一个 try_stream!,循环里逐条 recv:BindComplete、ParseComplete、CloseComplete 之类直接忽略;CommandComplete 解出影响的行数("SELECT 1");ReadyForQuery 结束循环。数据行这一支:

                    BackendMessageFormat::DataRow => {
                        logger.increment_rows_returned();

                        // one of the set of rows returned by a SELECT, FETCH, etc query
                        let data: DataRow = message.decode()?;
                        let row = PgRow {
                            data,
                            format,
                            metadata: Arc::clone(&metadata),
                        };

                        r#yield!(Either::Right(row));
                    }

PgRow 只有三个字段:这一行的原始字节、格式(扩展查询是 Binary)、和语句共用的 Arc<PgStatementMetadata>。每一行只多一次引用计数,列名和类型不复制。解码在 FromRow 里发生:try_get::<i64, _>(0) 先用元数据里的类型检查兼容性,再对这一列的 8 个字节调 Decode。见 Row 与 FromRow 和 Postgres 的类型。

8. 归还连接

fetch_optional 返回后,pool.acquire() 得到的 PoolConnection 被 drop。它的 Drop 不做 I/O,只 spawn 一个 return_to_pool 任务:检查池有没有关、连接是否超过 max_lifetime,跑 after_release,ping 一次,再推回空闲队列、释放许可。这就是「结果」那行之后的 Sync / ReadyForQuery;它出现在 println! 之后,说明调用方拿到结果时,归还还没做完。

不 bind 会怎样

接着跑后三段:同一条查询把参数直接写进 SQL、不调 bind;把 &str 直接交给 Executor;以及 raw_sql。

  println!("--- query 不 bind ---");
  let row: (i64, String) =
    sqlx::query_as("select id, title from posts where id = 1").fetch_one(&pool).await?;
  println!("结果 {row:?}");
  settle().await;

  println!("--- 直接把 &str 交给 Executor ---");
  let r = (&pool).fetch_one("select id, title from posts where id = 1").await?;
  println!("结果 {:?}", (sqlx::Row::get::<i64, _>(&r, 0), sqlx::Row::get::<String, _>(&r, 1)));
  settle().await;

  println!("--- raw_sql ---");
  let r = sqlx::raw_sql("select id, title from posts where id = 1").fetch_one(&pool).await?;
  println!("结果 {:?}", (sqlx::Row::get::<i64, _>(&r, 0), sqlx::Row::get::<String, _>(&r, 1)));
  settle().await;

  p.quiet.store(true, Ordering::SeqCst);
  let conn = pool.acquire().await?;
  println!("缓存的语句数 {}", Connection::cached_statements_size(&*conn));
--- query 不 bind ---
  [conn 1] C->S Sync
  [conn 1] S->C ReadyForQuery(I)
  [conn 1] C->S Parse(sqlx_s_2, "select id, title from posts where id = 1"), Describe(S, sqlx_s_2), Sync
  [conn 1] S->C ParseComplete, ParameterDescription, RowDescription, ReadyForQuery(I)
  [conn 1] C->S Bind(portal unnamed, stmt sqlx_s_2), Execute, Close(P, unnamed), Sync
  [conn 1] S->C BindComplete, DataRow(列长 8/10), CommandComplete("SELECT 1"), CloseComplete, ReadyForQuery(I)
结果 (1, "hello sqlx")
  [conn 1] C->S Sync
  [conn 1] S->C ReadyForQuery(I)
--- 直接把 &str 交给 Executor ---
  [conn 1] C->S Sync
  [conn 1] S->C ReadyForQuery(I)
  [conn 1] C->S Query("select id, title from posts where id = 1")
  [conn 1] S->C RowDescription, DataRow(列长 1/10), CommandComplete("SELECT 1"), ReadyForQuery(I)
结果 (1, "hello sqlx")
  [conn 1] C->S Sync
  [conn 1] S->C ReadyForQuery(I)
--- raw_sql ---
  [conn 1] C->S Sync
  [conn 1] S->C ReadyForQuery(I)
  [conn 1] C->S Query("select id, title from posts where id = 1")
  [conn 1] S->C RowDescription, DataRow(列长 1/10), CommandComplete("SELECT 1"), ReadyForQuery(I)
结果 (1, "hello sqlx")
  [conn 1] C->S Sync
  [conn 1] S->C ReadyForQuery(I)
缓存的语句数 2

不 bind 的 query_as 照样走预备语句:Parse(sqlx_s_2, ...),缓存里多了一条,最后缓存的语句数是 2。原因在第 1 节:query() 一开始就把 arguments 设成 Some(Ok(空参数)),一个参数都没有也是 Some。决定走哪种协议的不是参数个数,而是 Execute::take_arguments 返回什么,它的文档写得很清楚:

    /// Returning `Ok(None)` for `Arguments` indicates to use a "simple" query protocol and to not
    /// prepare the query. Returning `Ok(Some(Default::default()))` is an empty arguments object that
    /// will be prepared (and cached) before execution.

返回 Ok(None) 的只有两种:任何 SqlSafeStr(&'static str、AssertSqlSafe<String> 等)直接当查询用时的 blanket impl,以及 raw_sql 返回的 RawSql。run 在 arguments 为 None 时调 queue_simple_query,发一条 Query 消息,结果格式设为 PgValueFormat::Text:

  • 一个来回,没有 Parse、Bind,也不进语句缓存(最后缓存里只有 sqlx_s_1 和 sqlx_s_2)。
  • 结果是文本:DataRow(列长 1/10) 里 id 只有 1 个字节,就是字符 1;同一个值在扩展查询里是 8 字节的二进制。Decode 两种格式都能读,所以 Row::get::<i64, _> 照样得到 1。
  • 简单查询可以一次发多条用分号隔开的语句,也能执行不能预备的 DDL,这是 raw_sql 存在的理由,见 Query、QueryAs 与 SqlSafeStr。代价是参数只能拼进 SQL 里。

往返次数

把抓包按来回数一遍(一个来回是「客户端 flush 一次,等到 ReadyForQuery」):

场景借出 pingprepare执行归还 ping(后台)调用方等待的来回
第一次 query_as + bind11113
第二次(缓存命中)10112
query_as 不 bind(新 SQL)11113
&str / raw_sql101(Query)12
test_before_acquire(false),缓存命中00111

直接在一条 PgConnection 上执行(不经池)没有两次 ping,缓存命中时只有一个来回,见下面的动脑筋。

❗IMPORTANT
动脑筋:SQL 里多一个空格,缓存还认不认?

同一条 PgConnection(不经池,没有 ping),依次执行下面三条 SQL,每条都 bind(41_i64)、fetch_one。第二条只是在 + 前面多了一个空格,第三条和第一条一字不差:

let sqls = ["select $1::int8 + 1", "select $1::int8  + 1", "select $1::int8 + 1"];
for sql in sqls {
  println!("--- {sql:?}");
  let n: i64 = sqlx::query_scalar(sql).bind(41_i64).fetch_one(&mut conn).await?;
  println!("结果 {n},缓存的语句数 {}", conn.cached_statements_size());
}

三次执行一共发几条 Parse?最后缓存里有几条语句?平台同上。

动脑筋答案

--- "select $1::int8 + 1"
  [conn 1] C->S Parse(sqlx_s_1, "select $1::int8 + 1"), Describe(S, sqlx_s_1), Sync
  [conn 1] S->C ParseComplete, ParameterDescription, RowDescription, ReadyForQuery(I)
  [conn 1] C->S Bind(portal unnamed, stmt sqlx_s_1), Execute, Close(P, unnamed), Sync
  [conn 1] S->C BindComplete, DataRow(列长 8), CommandComplete("SELECT 1"), CloseComplete, ReadyForQuery(I)
结果 42,缓存的语句数 1
--- "select $1::int8  + 1"
  [conn 1] C->S Parse(sqlx_s_2, "select $1::int8  + 1"), Describe(S, sqlx_s_2), Sync
  [conn 1] S->C ParseComplete, ParameterDescription, RowDescription, ReadyForQuery(I)
  [conn 1] C->S Bind(portal unnamed, stmt sqlx_s_2), Execute, Close(P, unnamed), Sync
  [conn 1] S->C BindComplete, DataRow(列长 8), CommandComplete("SELECT 1"), CloseComplete, ReadyForQuery(I)
结果 42,缓存的语句数 2
--- "select $1::int8 + 1"
  [conn 1] C->S Bind(portal unnamed, stmt sqlx_s_1), Execute, Close(P, unnamed), Sync
  [conn 1] S->C BindComplete, DataRow(列长 8), CommandComplete("SELECT 1"), CloseComplete, ReadyForQuery(I)
结果 42,缓存的语句数 2

两条 Parse,缓存里两条语句。缓存的键是 SQL 原文(LruCache<String, _>,get_mut(sql) 按字符串比较),sqlx 不解析 SQL,也就不知道两个空格和一个空格是一回事,sqlx_s_2 是一条和 sqlx_s_1 语义相同的新语句。第三条和第一条完全相同,命中 sqlx_s_1,只剩一个来回。用 format! 拼出来的 SQL 每次都不一样,就会一直 Parse、一直挤占这 100 个缓存位置,这也是参数应该 bind 而不是拼接的一个原因。另外,不经池时每次查询前后都没有 Sync 的 ping。

小结

  • fetch_one = fetch_optional + RowNotFound;&Pool 先 acquire(默认 ping 一次)再交给 &mut PgConnection,用完 drop 时在后台再 ping 一次、放回池里。
  • PgConnection::run 按 SQL 原文查每连接一个、容量 100 的 LRU 语句缓存;没命中就单独一个来回 Parse + Describe + Sync,语句名 sqlx_s_N;命中就跳过,抓包里第二次执行没有 Parse。
  • 执行是 Bind(参数、结果都要二进制)+ Execute(limit: 0)+ Close portal + Sync,一次 flush;每个 DataRow 包成共用元数据的 PgRow。
  • fetch_optional 读完所有响应才返回(为了不漏掉延迟约束的错误),多出来的行照样传输。
  • 不 bind 的 query() 仍然走预备语句;只有 &str 直接交给 Executor 和 raw_sql 才走简单查询协议,一条 Query 消息,文本格式,不进缓存。

💬 关联讨论

发起讨论
加载中...