Should an N+1 fix change the shared mapping default or only the read that triggered it?
answer
- who does the change apply to
- one read versus every read
- defer globally, decide locally
- global default needs a rule for all reads
- budgets keep per-read plans honest
basics
~20 sDefault to fixing the read. A mapping change applies to every read of that type, so it trades one screen's extra statements for over-fetching everywhere else. Change the mapping only when every read genuinely needs the link.
solid answer
~40 sA mapping default is a claim about **all** reads of a type; a fetch plan on a query is a claim about **one**. Most per-row statement problems are a property of a single read — one screen walks a link, twenty others do not — so the fix belongs to that read, where the person changing it can see what it costs. Loading the link eagerly in the mapping fixes that screen and silently widens every other read of the type, including the ones that needed only an identifier. The mapping is the right place only when the link is part of how the type is always used: a narrow to-one link nearly every read displays. The per-read approach costs drift, so pair it with a statement-count budget on the paths that matter.
go deeper
Take away the rule of thumb: fix the read that has the problem, not the mapping every read shares.
Explain what a mapping default actually applies to, and give a concrete example of a read that would start over-fetching because of it.
Show how you would keep per-read plans from drifting — one home per use case, statement-count assertions on the paths that matter.
Argue the policy: deferred defaults, loading owned by the use case, the narrow conditions under which a global default is defensible, and the review attention a system-wide one-line change deserves.
## Two places a loading decision can live Every data-access layer offers both, under different names: - **In the mapping.** A property of the type: this link is always loaded with its parent, or never is. It applies to every read of that type, everywhere in the system, including reads written next year by people who never saw the discussion. - **In the read.** A fetch plan attached to one query, or a purpose-built projection. It applies to that call site only. The recurring interview question is which of the two a discovered N+1 should change, and the recurring wrong answer is the mapping, because it is a one-line change that makes the symptom disappear. ## Why the mapping is usually the wrong lever 1. **It is a global answer to a local question.** One screen walked the link. The mapping change also affects the export, the admin lookup, the health check and the write path that only needed the identifier. 2. **It converts one problem into a quieter, broader one.** Extra statements are visible in a statement count. Over-fetching is a slow, uniform tax that no single endpoint owns and no test flags. 3. **It compounds.** An eager link whose target has its own eager link pulls a subtree. Two or three such decisions and a single-row read touches half the schema. 4. **It cannot be opted out of cleanly.** Deferred-by-default can always be upgraded per read; loaded-by-default is hard to downgrade, and the layers that allow it make it an awkward exception. 5. **It hides intent.** A reader of the query cannot see what it will load. The loading behaviour lives in a file they are not reading. The reverse direction is safe and is the reason the default posture exists: **defer in the mapping, decide in the read.** Deferring costs nothing to a read that specifies its plan, and it makes the cost of touching a link visible to whoever touches it. ## When the mapping change is right It is not never. The mapping is the honest place when the link is part of how the type is *always* consumed: | Signal | Mapping change defensible? | |---|---| | To-one link, small target row, displayed by nearly every read | Yes — the exception is the rare read, not the rule | | The type is meaningless without it (a value split across two tables) | Yes — arguably it should not be a separate link at all | | A collection of any size | No — every read pays row multiplication or an extra statement | | One screen needs it, others do not | No — that is a per-read decision wearing a global disguise | | Nobody can enumerate the reads of this type | No — that is the reason to stay conservative, not to guess | Note the asymmetry: the defensible cases are narrow to-one links, and the indefensible ones are exactly the cases that produce the worst N+1 symptoms. The remedy that most tempts a global change is the one that least tolerates it. ## The cost of the per-read discipline, and how to pay it Per-read fetch plans are not free. - The same plan gets written in several places and they drift; one gets updated and the others do not. - New reads forget the plan entirely and reintroduce the pattern. - Nothing in the type's definition tells a newcomer that a plan is expected. Practical answers, in the order they pay off: 1. **Name the read.** Put the query and its plan behind one function per use case, so the plan has one home and one place to change. 2. **Budget the important paths.** Assert the statement count for the handful of endpoints that matter, so a regression fails a build instead of appearing in a graph. 3. **Prefer a read model where the read is truly different.** If a screen wants five fields from three tables, a projection is clearer than a fetch plan over objects, and it cannot be broken by someone else's mapping change. 4. **Review mapping-default changes harder than query changes.** A one-line change with system-wide reach deserves more scrutiny than a ten-line change with local reach, which is the opposite of how review attention usually flows. ## The position to defend in an interview Say that loading is a property of the use case, not of the type; that the mapping should express the cheapest safe default, which is deferring; that a global default is justified only when you can state the rule for *all* reads and the link is a narrow to-one; and that the per-read discipline is kept honest by counting statements on the paths you care about rather than by hoping nobody regresses it.
- Is there a case where deferring by default is the wrong global choice?Yes, for a link the type is effectively incomplete without — a one-to-one split of the same conceptual record across two tables. Deferring it buys nothing and guarantees a second statement on every use. That is less a loading decision than a sign the split should not exist.
- How do you stop per-read plans from silently rotting?Give each use case one function that owns its query and plan, and assert a statement count for the paths that matter so a regression fails the build. Reviews cannot catch a missing fetch plan reliably; a count assertion catches it every time the code path runs.
- A team argues the global change is fine because the extra columns are small. How do you respond?Ask them to enumerate the reads it affects, and what happens when the eagerly loaded type later gains its own eager link. The objection is not the current byte count; it is that the cost is unbounded in reads and in depth, and invisible at every call site.
saying these in an interview costs you the question
- Makes links load eagerly in the mapping to stop repeated statements
- Treats over-fetching as free because it is one statement
- Claims a global default is simpler so it must be better
- Cannot say which other reads a mapping change affects
- Adds per-read plans with nothing preventing the next regression