Where do you attach a statement counter so the number it reports belongs to exactly one request or unit of work?
answer
- lowest boundary all statements cross
- per-request state, not global
- executions, not prepared statements
- propagate the context across workers
- read after rendering, not at handler exit
basics
~10 sCount at the connection or driver, the one boundary every statement crosses, and hold the counter in per-request state read where the work closes - after rendering, so late fetches are included.
solid answer
~50 sTwo decisions: the counting point and the scope. Count at the lowest shared boundary - a wrapper around the connection or driver - because it sees statements from every path that uses that connection, including extra fetches triggered by a lazily loaded link and statements issued outside the mapper; a counter inside the mapper only sees what the mapper originated. Scope it by creating a fresh counter in per-request state at the entry boundary, incrementing from the wrapper, and reading it where the work closes. A process-wide counter is meaningless under concurrency. Two traps: work handed to another worker leaves the ambient context behind unless it is propagated, and a count read when the handler returns misses fetches that fire while the response is being written. Also pin the definition - count executions, not prepared statements and not round trips.
go deeper
Remember that a count only means something when you can say what it covered: one request, from entry to the point where the work closes, counted where every statement passes.
Explain the counting point and the scoping mechanism together, and name the definition you are counting - executions rather than prepared statements or grouped sends.
Show the failure modes you have actually hit: context lost across workers, several connection leases in one request, and a count read before the late fetches during rendering.
Weigh instrumentation depth against overhead and blast radius: a wrapper on every connection is on the hot path for every query, so decide what it records, at what sampling rate, and who owns it.
A statement count is only meaningful if you can say what it counted. "The service executed 4,812 statements" is a load figure; "this request executed 41 statements" is a diagnosis. Getting from the first number to the second is a question of **where the counter sits** and **what boundary starts and stops it**. ## Choose the lowest boundary the statements all pass through Every statement that reaches the database goes over a connection. That makes the connection — or the driver underneath it — the one place where nothing escapes the count. A thin wrapper around the connection that increments a counter each time a statement is executed sees the mapper's reads, the extra fetches a lazily loaded link triggers when something touches it, hand-written statements issued alongside the mapper, and work done by libraries you did not write. A counter placed higher up sees less. | Counting point | Sees | Misses | |---|---|---| | Inside the mapper's own statistics | statements the mapper originated, often split by kind | anything issued outside the mapper on the same connection | | Wrapper around the connection or driver | every statement any code path sends over that connection | statements sent over a connection obtained some other way | | Connection-pool listener | leases, and statements if it wraps what it hands out | nothing extra, but attribution needs the lease-to-request link | | The database's own statement log | everything the server executed, from every client | the request that caused it, unless a request identifier is carried along | The practical answer in most systems is a wrapper at the connection or driver level, with the mapper's own statistics used as a secondary breakdown when you want reads separated from writes. ## Decide what one "statement" is Before the number can be asserted on, the definition has to be pinned down, because three plausible definitions give three different numbers: - **Statements executed** — each execution counts, including each execution of a reused prepared statement. - **Statements prepared** — a reused prepared statement counts once, which understates a per-row read badly and is the wrong metric for this purpose. - **Round trips** — when a layer groups several statements into one send, many statements cost one trip. This is the number that correlates with latency, and it is not the number that reveals a per-row read. For catching multiplication, count **executions**. Keep round trips as a second number if you have it, and never mix the two in one threshold. ## Bind the counter to a scope, not to the process A single global counter is useless under concurrency: two requests in flight increment the same number. The counter has to live in something that is per request or per unit of work: 1. **Start** it at the boundary that opens the work — the entry point of the request, the job, or the message handler — by creating a fresh counter in the ambient context that the rest of the call path can reach. 2. **Increment** it from the connection wrapper, which reads the counter out of that same ambient context. 3. **Read and clear** it at the boundary that closes the work. The mechanism for the ambient context varies: a value attached to the current execution context, an object handed down explicitly, or a counter keyed on the connection lease, since a pooled connection is leased to one worker at a time. Whichever it is, the two failure modes are the same and both are common: - **Work that moves to another worker** — anything handed to a background thread or an asynchronous continuation leaves the context behind unless the context is explicitly propagated, so its statements are either dropped from the count or attributed to whatever else is running there. - **Several connections per request** — a request that borrows, returns and borrows again, or that fans out across connections, must sum across leases; a counter keyed on one lease reports a fraction. ## Put the read point after the last statement, not after the handler The most common undercount has nothing to do with concurrency. If a lazily-loaded link is touched while the response is being written, its extra fetches happen **after** the handler returns. A counter read when the handler returns reports a clean number for a request that is still multiplying. The read has to happen at the outermost boundary that also closes the unit of work and the connection — after rendering, not before it. The same applies to a job that flushes accumulated changes on the way out: statements the flush emits belong to the job's count. ## What to do with the number once you have it A scoped count is only worth collecting because it becomes comparable. Attach it to the request's own record — alongside the route or operation name and the duration — so the count can be read next to the work that produced it, asserted on in a test, or exported as a distribution keyed by operation. That is also why the scope matters more than the precision: a number that reliably means "this operation, end to end" is far more useful than a more granular number nobody can attribute.
- Why is a counter inside the mapper a weaker instrument than one at the connection?It only sees statements the mapper originated. Hand-written statements on the same connection, work from libraries, and anything issued around the mapper are invisible, so the count under-reports exactly the mixed code paths where a per-row read is most likely to hide. The mapper's statistics are still useful as a breakdown of reads versus writes, but not as the total.
- A request borrows one connection, returns it, then borrows another. What does that do to a lease-scoped counter?It splits the request into two counts, each looking innocent. Either sum across every lease taken within the request scope, or key the counter on the request rather than on the lease. The same applies to a path that fans out across connections deliberately - the per-request total is the number that means anything.
- Should the count include statements the layer groups into a single send?For catching multiplication, yes - count executions, because the multiplication is what you are hunting and grouping does not remove it. Keep grouped sends as a separate round-trip number if the layer exposes it, since that is the figure that tracks latency. What breaks is mixing the two in one threshold, where a change in grouping silently moves the number.
saying these in an interview costs you the question
- Uses one process-wide counter and reports it as a per-request number.
- Counts inside the mapper and assumes nothing else touches the connection.
- Reads the count when the handler returns, missing fetches during rendering.
- Forgets that background or asynchronous work leaves the request context behind.
- Counts prepared statements rather than executions, so a reused statement counts once.