# The query cost that grows with the business

A threshold in seconds measures the machine that ran the test. Counting statements measures the code, and the finding is the growth curve rather than the absolute number. Four surfaces were measured that way: a customer list at 5,574 statements became 3; two thread lists at 83 and 89 became 11 and 17; a home screen at 133 became 15, and all three are now flat with page size.

- **Status:** Available
- **Audience:** both, developer
- **Last verified:** 2026-09-10
- **Canonical:** https://connectbyjbrh.com/research/n-plus-one-that-grows/

## Why the unit is a statement

Wall-clock thresholds are unstable in the way that matters least and stable in the way that matters most: they move with hardware, cache warmth, network distance and whoever else is on the box, and they stay quiet while the thing you actually care about — a per-row query — grows underneath them. A page that issues one statement per row is a defect at 40 rows and an outage at 4,000, and on a developer machine with a warm cache both feel fine.

- A statement count is deterministic: the same code over the same fixture gives the same number, on any machine, in any timezone, under any load.
- It exposes the *shape* of the cost. Two numbers taken at two page sizes say whether the surface is flat or linear, which is the property that decides whether it survives a growing business.
- It is assertable. A test can require that a screen costs no more statements at 200 rows than at 20, and that assertion fails the moment somebody adds a convenient lookup inside a loop.

## Four measurements

| Surface | What it did | Before | After | With page size |
|---|---|---|---|---|
| The operator's customer list | Fetched the user, workspace and entitlement for every membership, one at a time — 1,857 accounts | 5,574 | 3 | Three inner joins say the same thing once |
| The triage strip | Read a page of up to 200 threads, then per row fetched the last message and the contact — measured at 40 rows | 83 | 11 | Flat |
| The workspace console | The same shape, over its own page — measured at 40 rows | 89 | 17 | Flat |
| Home | Read the newest 200 open threads and ran three more statements per thread — measured at 40 conversations | 133 | 15 | Flat |

The customer list is the clearest case because its multiplier is exact: three statements per membership across 1,857 memberships is 5,571, plus the query that found them. Nothing was slow in an interesting way; it was slow in an arithmetic way, and arithmetic scales.

> **Note** Home's per-row cost also puts a number on its ceiling: three statements per thread against a 200-thread page is roughly six hundred round trips for one screen. That figure is arithmetic from the measured per-row cost rather than an observed run, and it is the reason the ceiling was not a safety net.

## The shape of every fix

None of these needed a cache, a denormalised column or a background job. Each was the same move: stop asking per row, and ask once for the whole page's worth.

1. Fetch the page — the identifiers you are going to show — in one statement.
   - Result: You now hold a bounded set of ids, which is what every following query can be keyed on.
2. Fetch each dependent set in one statement keyed by those ids: one windowed query for the last message per thread, one contacts-by-id query for the people.
   - Result: This is what `workspace_console.page_context` does, and it is why the two thread lists became flat rather than merely cheaper.
3. Let the database do the aggregation. Counts are set-based statements, not a loop with a running total.
   - Result: Home went from 133 statements to 15 by replacing per-thread lookups with three set-based ones.
4. Join where the relationship is one-to-one, rather than fetching the related row separately.
   - Result: The customer list's user, workspace and entitlement are exactly that shape, which is how 5,574 becomes 3.

## What a statement count does not measure

- **It is not a millisecond.** One badly-planned statement over a large table can cost more than fifty keyed lookups. The count finds growth; a plan is still what you read when a single statement is the problem.
- **It says nothing about index quality.** A flat surface with a missing index is flat and slow, and only reading the plan reveals it.
- **It says nothing about locks or contention**, which are load properties rather than shape properties.
- **One measurement is not a curve.** A single number is a fact about one fixture; two numbers at two sizes are the finding. Every figure above was taken at a stated row count for that reason.
- **It does not capture work outside the database** — a model call, a provider request or a render pass are separate budgets with their own units.

## Questions

### Is three statements always better than fifteen?

Not automatically. Three heavy statements can cost more than fifteen light ones. The comparison that matters is between the same surface before and after, at two page sizes: the win in every case above was that the cost stopped growing with the number of rows on the screen.

### Why not just measure the time?

Timing is the right instrument for a slow statement and the wrong one for a growing pattern. A per-row query hides comfortably inside a fast machine's budget until the data grows, at which point the timing test fails everywhere at once and says nothing about why.

### How do you stop the pattern coming back?

By asserting the count rather than remembering the lesson. A check that fixes the statement count for a screen at two page sizes fails on the next convenient lookup inside a loop, which is when it is cheapest to fix.

## Related

- [Headline numbers that stop at the page size](https://connectbyjbrh.com/research/counting-past-the-limit/)
- [Relationships in Connect](https://connectbyjbrh.com/docs/relationships/)
- [Five ways a test suite has passed while proving nothing](https://connectbyjbrh.com/research/false-passing-tests/)
- [Connect by JBRH](https://connectbyjbrh.com/docs/product/)
- [A spreadsheet view that is not a second database](https://connectbyjbrh.com/research/grid-over-services/)
- [Troubleshooting](https://connectbyjbrh.com/docs/troubleshooting/)

## What this page is based on

- `docs-source/sources/GENERAL.md` §11 — the measured statement counts
- `docs-source/sources/CHANNELS.md` §5 — the same measurements with the per-surface detail
- Connect capability registry (docs-source/facts.py) — `MEASURED`
