skip to content

In database/sql, what happens when a *sql.Stmt is executed on a connection it was not prepared on?

level: middleimportance: should knowfreq 40%

answer

  1. one Go handle, many connections
  2. the pool picks where it runs
  3. it prepares itself again, quietly
  4. statements times live connections
  5. on a Tx or Conn it is bound for life

basics

~20 s

database/sql prepares it again on that connection. A *sql.Stmt from a *sql.DB is not bound to one connection: it tracks which connections it has been prepared on and quietly adds each new one it is handed.

solid answer

~40 s

A `*sql.Stmt` created by `db.PrepareContext` stays usable for the lifetime of the `*sql.DB`, and it does that by re-preparing itself. Internally it keeps the set of connections it already has a server-side statement on; when an execution lands on a connection that is not in that set — because the original one is busy, or was retired, or the pool simply grew — `database/sql` prepares the statement there too and remembers it. The cost is one extra round trip the first time the handle meets each connection, and the consequence is that a hot statement over a ten-connection pool can end up as ten server-side statements. The contrast matters: a statement prepared with `tx.PrepareContext` on a `*sql.Tx`, or on a `*sql.Conn`, is bound to that single connection forever and becomes unusable once it closes.

go deeper

for a junior

Know that you do not choose which connection a query runs on: database/sql takes a free one from its pool for each call, and a statement prepared on the *sql.DB follows it there.

for a middle

Explain the re-preparation mechanic precisely — a DB-level handle keeps the set of connections it is already prepared on and adds new ones silently, costing one extra round trip the first time it meets each connection.

for a senior

Reason about the multiplier in production: what the server holds is statements times live connections, and connections turning over means that arithmetic is continuous rather than a one-off warm-up.

for a principal

Decide when the reuse is worth the bookkeeping at all. For statements a process runs a handful of times, a plain parameterised call is simpler, has a bounded cost, and removes a whole class of lifetime bugs from review.

## One Go handle, many server-side statements A prepared statement is not a portable object. It lives inside one session on the database server, and a session in Go terms is one pooled connection. But `*sql.Stmt` is documented as usable for the lifetime of the `*sql.DB` it came from, and a `*sql.DB` hands out whatever connection is free. Those two facts can only be reconciled one way, and that way is re-preparation. When you call `stmt.ExecContext` or `stmt.QueryContext`, `database/sql` borrows a connection. Then: 1. If the handle already has a server-side statement on that connection, it executes it. One round trip. 2. If it does not, `database/sql` prepares the statement on that connection first, records the pairing, and then executes. Two round trips. Step 2 is invisible from your code. There is no error, no callback, no flag. The handle simply grows a new attachment. ## Why the connection changes underneath you Several ordinary things move a statement onto a new connection: - **Concurrency.** A `*sql.Stmt` is safe for concurrent use; if two goroutines execute it at the same time, they are on two different connections by definition. - **The first connection is busy** with someone else's query when your execution arrives. - **Connections come and go.** A long-running process does not keep the same physical connections all day; as old ones are retired and new ones opened, the handle attaches to the new ones on first use. So the steady-state server-side footprint of a service is roughly *number of long-lived statements* multiplied by *number of live connections*, not the number of `*sql.Stmt` values in the Go heap. For a background job holding four statement handles against a pool of ten connections, that is up to forty parsed statements on the server, all legitimate. ## What Close does about it `stmt.Close()` is the counterpart: it drops the statement on every connection the handle attached to, not just the one it started on. That is why a single deferred `Close` is enough no matter how far the handle spread. ## The bound case: Tx and Conn The re-preparation behaviour belongs to statements prepared on a `*sql.DB`. Two other origins behave the opposite way: - `tx.PrepareContext` on a `*sql.Tx` - `conn.PrepareContext` on a `*sql.Conn` A statement from either is bound to that one underlying connection forever. It cannot migrate, because the whole point of a `*sql.Tx` or a `*sql.Conn` is that it pins one session. When the transaction commits or rolls back, or the `*sql.Conn` is closed, the statement stops working and further calls return an error rather than silently re-preparing somewhere else. That asymmetry is worth stating explicitly in an interview, because it is where the two halves of prepared-statement lifetime meet: `*sql.DB` gives you a durable handle over a shifting set of connections, `*sql.Tx` and `*sql.Conn` give you a handle that is exactly as long-lived as the thing it was prepared on. ## Performance consequences The re-preparation is cheap in the steady state and awkward at the edges. **Steady state.** A handful of statements against a stable set of connections converges quickly: after a warm-up, nearly every execution is one round trip and you get the reuse you prepared for. **Churn.** If connections are short-lived relative to how often the statement runs, the handle spends a large fraction of its executions on the two-round-trip path — the same prepare-then-execute you were trying to avoid. At that point the explicit preparation is paying for itself less and less, and a plain parameterised `ExecContext` is competitive and simpler. **Server memory.** Each attachment is a parsed statement and its plan, held per session. Multiply by statements and by connections. This is normally small and bounded; it becomes a problem only when the count is unbounded, which means handles that were never closed rather than the re-preparation itself. ## What it is not A `*sql.Stmt` does not hold a connection out of the pool between calls. It holds statement attachments *on* connections. Each execution borrows a connection, runs, and returns it. That is precisely why one handle can serve many goroutines at once — and why its footprint on the server is a function of how many connections it has touched rather than how often it has run.

  • How is a statement from tx.PrepareContext different in this respect?
    It is bound to that transaction's single connection forever. It cannot migrate, and once the `*sql.Tx` commits or rolls back the statement is unusable and further calls return an error. The same holds for a statement prepared on a `*sql.Conn`. Only a statement prepared on the `*sql.DB` re-prepares itself elsewhere.
  • What does the first execution on a fresh connection cost?
    One extra round trip: `database/sql` sends the prepare, then the execute. It is invisible in the code, so a call that usually takes one round trip occasionally takes two. If connections turn over quickly relative to how often the statement runs, that occasionally becomes most calls, and the reuse you prepared for largely evaporates.
  • Does a *sql.Stmt hold a connection out of the pool between executions?
    No. It holds prepared-statement attachments on connections, not the connections themselves. Every `ExecContext` or `QueryContext` borrows a connection, runs, and gives it straight back. That is why one handle serves many goroutines concurrently, and why its server-side footprint tracks how many connections it has touched rather than how many calls it has served.

saying these in an interview costs you the question

  • Says a *sql.Stmt pins one connection for its whole life
  • Assumes the statement is prepared exactly once on the server
  • Thinks a Tx-prepared statement survives the commit
  • Believes re-preparation surfaces as an error or a warning
  • Counts one server-side statement per handle regardless of pool size