In a paged parent list, why must the total-count statement not reuse the collection fetch joins?
answer
- the count must count parents
- a fetch join makes it count pairs
- fetch joins out, filtering joins preserved
- distinct keys when a child filter stays
- existence test instead of a filtering join
basics
~20 sCounting over a fanned-out grid counts parent-child pairs, so the total is inflated and the page count is wrong. Count parents: drop the collection joins, or count distinct parent keys when a filter forces the join to stay.
solid answer
~50 sA paged response needs two numbers from the same filter: the window of parents and how many there are in total. If the count statement is derived from the fetch statement by keeping its collection joins, it counts rows — parent-child pairs — so the total is the sum of child counts, not the number of parents. The page footer then promises pages that do not exist. The clean repair is to build the count from the filters alone, with no collection join, so one row is one parent. Where a filter genuinely lives on the child, the join must stay and you count **distinct parent keys**, or better, express the child condition as an existence test so the count can drop the join again. Whatever you choose, the count's filters must match the page's exactly.
go deeper
Learn the symptom: if the total next to a paged list is bigger than the number of items you can actually reach, the count is counting joined rows rather than parents.
Explain the difference between a join that fetches and a join that filters, and why only the first can be removed from the count statement without changing the answer.
Show that you rewrite a child predicate as an existence test so both statements shed the join, and that you question whether an exact total is needed at all before paying for it each request.
Decide the product contract: exact totals, a has-more flag, or an estimate. Each has a different cost curve as data grows, and the choice belongs in the API design rather than in each endpoint.
## Why a derived count comes out inflated A paged list needs the page and the total. It is tempting to build the count by taking the fetch statement and swapping the select list for a count, because then the filters are guaranteed to match. But the fetch statement joins the collection, so its grid holds one row per parent-child pair. Counting that grid answers *how many pairs match*, which is the sum of the matched parents' child counts. The consequences are visible to users: - **The page count is too high**, so the last pages are empty. - **"Load more" never terminates**, because the client compares a page of parents against a total of pairs. - **The number shown next to a filter is simply wrong**, and it is wrong by a data-dependent factor, so it looks plausible in a test fixture with one child per parent. ## Three ways to get the right number 1. **Drop the collection joins and count parents.** Build the count from the parent table plus whatever the filters actually require. One row is one parent, so a plain count is the answer. This is the cheapest option and the default. 2. **Keep the join and count distinct parent keys.** Necessary when a filter refers to a child column and you cannot restructure it. Correct, but it makes the engine deduplicate keys across the multiplied grid, so it is measurably more expensive than counting parents. 3. **Convert the child condition into an existence test.** A predicate of the form "the parent has at least one child satisfying X" is a semi-join: it selects parents without multiplying them. The count can then drop the collection join entirely and go back to option 1 — and the page statement usually benefits from the same rewrite. | Approach | Correct total? | Cost | When to use | |---|---|---|---| | Count over the fetch grid | No — counts pairs | Same as the fetch | Never | | Count parents, no collection join | Yes | Cheapest | Filters live on the parent | | Count distinct parent keys over the join | Yes | Deduplication over a multiplied grid | A child filter you cannot restructure | | Existence test, then count parents | Yes | Cheap | A child filter you can restructure | ## The trap that survives the obvious fix Dropping the joins is correct only if the joins were not **filtering**. An inner join to a child table removes parents that have no children; remove that join from the count and you count parents the page will never show. So the rule is not "always drop the joins" but: - **A join used only to fetch data** (typically an outer join, adding no restriction) can be dropped from the count freely. - **A join that restricts which parents qualify** must be preserved in meaning — as an existence test, or as a distinct count over the same join. The test to apply: would removing this join change *which parents* match? If yes, it is a filter and its effect has to be kept. ## The cost of the count itself A total is not free — it is a second evaluation of the same filter, often over the same rows, and on a large filtered set it can dominate the request. Before paying for it every request, consider: - **Fetch one extra parent key** in the page statement to answer "is there a next page", which is all many interfaces actually need. - **Count only on the first page** and carry the total in the client's state while the filter is unchanged. - **Show an approximate total** for very large sets, where an exact number has no product value. - **Cache the count per filter** where filters repeat heavily and staleness is tolerable. ## Keeping the two statements honest The count and the page must agree, or the interface contradicts itself: 1. Apply **identical filters** to both, from one shared definition rather than two hand-written statements that drift. 2. Keep the **same join semantics** for anything that restricts — if the page uses an inner join to a child, the count must not silently become an outer one. 3. Remember that **ordering and limits belong only to the page statement**; a count with an ordering is wasted work and a count with a limit is a bug. The summary worth keeping: **the page statement multiplies rows on purpose and the count statement must not inherit that.** Count parents, keep the filters identical, and treat a filtering join as something to preserve in meaning even when you remove it in form.
- When can the count statement not simply drop the collection join?When the join is filtering rather than fetching. An inner join to a child, or any predicate on child columns, decides which parents qualify; removing it counts parents the page will never show. Preserve the restriction — as an existence test, or by counting distinct parent keys over the same join.
- How do you avoid paying for a total count on every request?Ask whether the product needs an exact total. Fetching one key beyond the page size answers "is there more" for the common case. Otherwise count on the first page only and carry it while the filter is unchanged, or show an approximate figure for very large sets.
- Why is counting distinct parent keys more expensive than counting parents directly?The engine must still produce the multiplied grid and then deduplicate keys across it, typically with a sort or hash. Counting parents with no collection join never creates the extra rows in the first place, so it does strictly less work for the same answer.
saying these in an interview costs you the question
- Derives the count by reusing the fetch statement's collection joins
- Counts child rows and calls it the number of parents
- Drops a filtering inner join from the count as if it were a fetch join
- Lets the count and the page statement carry different filters
- Assumes an exact total is required by every paged interface
- Adds an ordering to the count statement out of symmetry