skip to content

An identical list endpoint is fast in one environment and slow in another — how do you prove a per-row emitter is why?

level: seniorimportance: should knowfreq 50%

answer

  1. the cost follows the data
  2. count against rows, not total time
  3. cheap statements, many of them
  4. hold the deployment, vary the data
  5. bisect the output to name the field

basics

~20 s

Show that the statement count moves with the number of rows walked. Capture statements per request alongside the result size in both environments; a count that tracks rows one-for-one while each statement stays fast identifies the emitter.

solid answer

~40 s

The distinguishing property of a per-row emitter is that its cost is set by data volume, not by code, so identical code diverges the moment the two data sets differ. Prove it by correlation rather than by inspection: capture statements per request together with the number of rows the response walked, in both environments. A per-row emitter shows a count that rises roughly one-for-one with rows while individual statement times stay flat and healthy. Then confirm causally — re-run the fast environment against a data shape with realistic collection sizes and watch the count follow the data, not the deployment. Ruling out the usual alternatives matters too: a plan difference or a cold cache changes per-statement time while the count stays constant, which is the opposite signature.

go deeper

for a junior

Remember that this cost is set by how many rows are walked, so a small data set will look fine no matter how carefully the same code path is exercised.

for a middle

Explain the separating measurement: statements per request against rows returned. Many cheap statements point to an emitter; one slow statement with a flat count points somewhere else entirely.

for a senior

Show the full chain — correlate, then hold the deployment fixed and vary the data, then bisect the output shape to name the field. Rule out plan and resource differences explicitly.

for a principal

Push for the count and the rows walked to be recorded per request as a matter of course, so this class of report is answered from data already collected instead of a reproduction exercise per incident.

"Works here, not there, same commit" is the most common way a query-per-row emitter is first reported, and it is a consequence of the defect's defining property: **the statement count is a function of the rows walked, not of the code executed**. Two deployments running byte-identical code will diverge as soon as their data differs in size or shape. ## Why data volume, not the code path, sets the cost The walk visits whatever the read returned. A collection with three items is touched three times; the same field, in the same line, is touched three thousand times against a larger tenant. Nothing in the code changed and nothing needs to. Three practical consequences follow: - A small data set can never expose the defect, however carefully the code path is exercised. - The same endpoint can be healthy for most users and fatal for the largest one, because collection sizes are almost always skewed. - The cost grows with adoption over time, so an endpoint that has been fine for a year degrades without a deploy. ## Proving it by correlation The evidence you want is two numbers per request, captured in both environments: **the number of statements issued** and **the number of rows the response actually walked** (items in the list, plus items in any nested collection rendered). Then look at the relationship. | Observation | Reading | |---|---| | Count rises roughly one-for-one with rows; each statement fast | per-row emitter, effectively confirmed | | Count flat; one statement much slower in one environment | plan, index or statistics difference — not this defect | | Count flat; all statements slightly slower | resource or network difference between hosts | | Count rises faster than rows | nested walk, with more than one term multiplying | The second row of that table is the alternative most often confused with this one, and the two are cleanly separable: a bad plan makes one statement expensive while the count is unchanged, whereas an emitter leaves every statement cheap and multiplies how many there are. Total time can look identical in both cases, which is precisely why a total-time comparison proves nothing on its own. ## Then prove it causally Correlation across two environments still confounds data with everything else that differs between them — configuration, hardware, warmth of caches. Close that gap by making the data the only variable: 1. In the fast environment, construct a data set whose collection sizes match the slow one, for a single representative parent. Re-run the same request. 2. Watch the statement count follow the data. If the count moves with collection size while the deployment is held fixed, the emitter is established. 3. Bisect the output shape to name the field: drop one linked field from what is rendered, re-run, and see which removal makes the burst disappear. Step three is what turns a diagnosis into an actionable line number, and it is the step teams skip — leaving them with a proven pathology and no idea which walk produced it. ## Reading the request-level shape Two further tells help before you have instrumentation in place. A per-row emitter's latency scales with the *result size of that specific request*, so time per request plotted against items returned makes a straight line rather than a cloud; a per-statement problem makes a flat band. And in the log, the extras arrive as a run of near-identical statements differing in one bound value — a shape nothing else produces. One caution: some layers, on touching a link, opportunistically fill the same link for other loaded objects in one grouped statement. Where that happens the count stops rising one-for-one and rises in steps instead, which can mask the correlation at small sizes while the underlying walk is still there. Read the statement text, not only the count: grouped fills carry a list of bound values where a one-at-a-time fill carries a single one. ## Reporting it credibly The finding that persuades is a short chain: here is the statement count against rows in each environment; here is a single statement's timing showing the store is not the problem; here is the same code with the same data shape reproducing it in the fast environment; here is the field whose removal deletes the burst. Each link rules out an alternative, and together they name a line. "It is an N+1" without those links is a guess that happens to be right often enough to be dangerous — it sends people optimising the store, or the environment, when the emitting walk is what needs to change.

  • Total request time is the same in both environments. Why does that not rule out a per-row emitter?
    Because total time cannot distinguish many cheap statements from one expensive one. Both produce the same total. The separating measurement is the count of statements against rows walked, plus per-statement timings — an emitter shows a high count with uniformly fast statements.
  • You cannot copy the large data set into the fast environment. What else establishes causality?
    Vary the volume where you can. Request one item, then ten, then a page, in the same environment, and record statements against rows. A count that scales with the request's own result size proves the relationship without needing the other data set at all.
  • The statement count rises in steps rather than one per row. What might be happening?
    The layer may be filling the same link for several loaded objects at once when one of them is touched, producing grouped statements carrying several bound values. The walk is still per-object; only the round trips are grouped. Read the statement text to tell the two apart.

saying these in an interview costs you the question

  • Compares total request time and calls the question answered
  • Blames the environment or hardware without counting statements
  • Assumes a slow endpoint means a slow statement somewhere
  • Proves the pathology but never names the emitting field
  • Treats a small local data set as evidence the path is fine