Every list query, and what it costs #
2026-07-31, after the author-picker work. The question asked was whether other
queries share the two faults found there: no limit, and fetching a whole
document to read one key. Every row count below is from production, not
estimated.
The headline #
Missing limit turned out to be mostly a non-problem, and payload shape the
real one. A list the user must see in full cannot be capped without pagination —
a cap would silently hide children, comments or votes, and on a vote tally it
would silently produce a WRONG NUMBER. So the fixes worth making were about what
each row carries, not how many rows come back.
Three changes came out of it, all measured:
| what | before | after |
|---|---|---|
poll turnout (count_active_members) |
46.0 KB | 0.1 KB |
| drawer expansion, widest folder | 54.0 KB | 11.4 KB |
| home context list | 3.2 KB | 1.9 KB |
The queries that return lists #
limit column: whether the operation sends one. worst is the largest result
this deployment can actually produce today.
| query | limit | worst | verdict |
|---|---|---|---|
| NodesWhereQuery | ✓ | — | |
| NodePickerQuery | ✓ 10 | — | author picker |
| NodesSearchQuery | ✓ 30 | — | search bar |
| MembersExistQuery | ✓ 1 | — | existence probe |
| UsersSearchQuery | ✓ 10 | — | |
| RecentNodesQuery | ✓ | — | |
| ChildrenQuery | — | 48 children | must not cap — a folder shows all its children |
| DrawerChildrenQuery | — | 48 children | must not cap — same list, in the tree |
| ChildIdsQuery | — | 239 per context | correct: a subtree walk that is wrong if truncated; ids only |
| VotesWhereQuery | — | 10 votes | must not cap — this is the tally |
| CommentsQuery | — | 7 comments | must not cap — a truncated discussion is a lie |
| ContextsWhereQuery | — | 8 groups | bounded by the number of contexts |
| PollsWhereQuery | — | 5 per context | bounded |
| PermissionsQuery | — | 45 per context | bounded by mime × role |
| RelationsQuery | — | 2 per node | bounded |
| InvitationsQuery | — | 23 per person | bounded, and per signed-in user |
| DeletedNodesQuery | — | 2 in the bin | bounded today; see below |
| MembersCountQuery | n/a | 1001 → 1 integer | fixed, below |
The one that was genuinely wrong: counting by fetching #
count_active_members selected every member id of a context and took .len().
On the largest context here — 1001 members — that is 46 KB over the wire to
learn one integer, and it runs for every poll, because turnout is drawn on each
one. The comment above it said the schema exposes no members_aggregate. That
was simply false: membersAggregate is there and the user role may call it.
Verified as an ordinary member against production: {"count": 1001}, 0.1 KB.
The two that carried documents to draw an icon #
DrawerChildFields and ContextNodeFields both select data, and both read
exactly one thing out of it: a file's content type, for the glyph. Everything
else in that jsonb — the whole Slate document of every sibling — was travelling
for nothing. Hasura selects inside a jsonb column, so they now ask for
data(path: "type"), as the search bar already does.
Measured on the widest folder in production, 48 children: 54.0 KB → 11.4 KB. The drawer expands a folder every time you open one, so this is not a one-off.
The ones deliberately left alone #
ChildNodeFields.datastays whole. A folder row renders rich content inline (SlateRenderer) and the feed reads a comment's text out of it. It is used, not carried.PollSummaryFields.datastays whole — the options are the point.NodeFields.datais the document itself.
Worth revisiting when the data grows #
Nothing here is urgent at today's sizes, and each would be a real fix later:
- The bin has 2 rows today. At a few thousand it needs
order_by deletedAt descplus a limit and a "show older" step — a cap without an order would hide arbitrary items, which is why it has none now. - A folder with hundreds of children needs pagination, and then
ChildrenQuery/DrawerChildrenQueryget their limit for free. The congress-scale folder to watch is the one at 48. - A poll with thousands of votes should be tallied server-side by an aggregate rather than by shipping ballots to the browser to count. Same shape of fix as the member count above, and the same reason: ask for the number.