Batch page tree node lookups to avoid N+1 queries - #6211
Draft
VPS-thodax wants to merge 1 commit into
Draft
Conversation
Blocks are transformed concurrently, so a page containing many internal links issued one query per link plus one per path segment. Concurrent lookups of the same node each ran their own query, because the cache was only filled once a query had resolved. Batch node lookups happening in the same tick into a single query and deduplicate them while in flight. The number of queries is now bound by the depth of the page tree instead of the number of links. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PrvQarKxE7b5obfJnS3kRJ
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
Resolving the content of a page runs one database query per internal link, plus one more per segment of each link's path. A page with 200 internal links pointing at nodes four levels deep issues 801
PageTreeNodequeries:The same node is queried repeatedly within a single request, and the query count grows with the number of links on the page.
Reason
PageTreeReadApi.getNode()writes a node into its cache only once the query has resolved. Blocks are transformed concurrently (Promise.allintransformToPlain), so every lookup issued before the first one returns misses the cache and runs its own query.nodePath()then walks up the tree with onegetNode()per level, multiplying the effect.Fix
Node lookups go through a
DataLoader: loads happening in the same tick collapse into a singleid in (…)query, and duplicate lookups of the same id are deduplicated while in flight. The number of queries is now bound by the depth of the page tree instead of the number of links — the page above drops from 801 queries to 5.getNodesByIds()uses the same loader, so it shares the batch and the cache.The loader and its cache are per request. They live on the read API instance, and
PageTreeReadApiServiceis request-scoped, so nothing is shared between requests — or between API instances. There is no cache to invalidate and no coherency to manage when running several replicas: a node published or unpublished elsewhere is visible on the very next request.One behavior change within that request: lookups that find nothing are now cached too. That is what you want for a request; for a read API deliberately kept alive longer —
createReadApi()in a console command, for example — a node that becomes visible mid-run is not picked up. The cache for found nodes already behaved this way.Decisions
queryNodes().getChildNodes(),getNodeByPath()andpageTreeRootNodeList()filter by slug, parent and scope instead of by id, so they need a different batching strategy. Left out to keep this change small. -> done in Batch child node lookups to avoid N+1 queries #6212Verification
Measured on the demo API against a page whose internal links point at nodes four levels deep, counting statements in the PostgreSQL log. This is a local environment, so the ratios are the meaningful part, not the absolute timings:
With 10 concurrent requests against a single API instance, throughput goes from 1.6 req/s to 10.6 req/s and median latency from 4.5 s to 0.84 s.
That the cache does not outlive the request was checked against the running API: renaming a node, and unpublishing and republishing one, each took effect on the very next request.
GraphQL responses are byte-identical before and after. Nine further page tree queries — among them
pageTreeNodeList,mainMenu,topMenu,parentNodes,pageTreeFullTextSearch,paginatedPageTreeNodesandpaginatedRedirects— also return identical responses.New unit tests cover concurrent lookups of different ids collapsing into one query, repeated lookups of the same id, already-loaded nodes, nodes that do not exist, and resolving several paths with one query per tree level.
Further information