-
Notifications
You must be signed in to change notification settings - Fork 211
perf(entgql): avoid COUNT(*) for pageInfo-only connection queries - #625
perf(entgql): avoid COUNT(*) for pageInfo-only connection queries #625tutranngoc wants to merge 1 commit into
Conversation
tutranngoc
commented
Jan 29, 2026
@tutranngoc fyi, you can use entcache package to cache query result of count(). So it wont hit database twice.
tutranngoc
commented
Jan 30, 2026
@tutranngoc fyi, you can use entcache package to cache query result of count(). So it wont hit database twice.
@giautm Thank you for sharing, however, my preference is to eliminate the need for a select count when using pageInfo, even during the first query. Querying for count when using pageInfo is unnecessary.
isakstenstrom
commented
Mar 26, 2026
I don't think this PR handles all edge cases for the pagination. If you create a query that looks like this:
variants(first: $firstVariant, after: $afterVariant) { pageInfo { hasNextPage endCursor } }
then we will not populate the pageInfo object correctly. If we request pagination info without any edges, we still need to do the count.
I solved the same problem using an external template, where I used this logic instead:
ignoredEdges := !hasCollectedField(ctx, edgesField) needTotalCount := hasCollectedField(ctx, totalCountField) needPageInfo := hasCollectedField(ctx, pageInfoField) hasPagination := after != nil || first != nil || before != nil || last != nil if (needTotalCount && hasPagination) || (ignoredEdges && (needTotalCount || needPageInfo)) { c := {{ $r }}.Clone() {{- /* Clear the selection fields before counting to avoid generating invalid queries. */}} c.ctx.Fields = nil if conn.TotalCount, err = c.Count(ctx); err != nil { return nil, err } conn.PageInfo.HasNextPage = first != nil && conn.TotalCount > 0 conn.PageInfo.HasPreviousPage = last != nil && conn.TotalCount > 0 }
tutranngoc
commented
Mar 27, 2026
I don't think this PR handles all edge cases for the pagination. If you create a query that looks like this:
variants(first: $firstVariant, after: $afterVariant) { pageInfo { hasNextPage endCursor } }then we will not populate the pageInfo object correctly. If we request pagination info without any edges, we still need to do the count.
I solved the same problem using an external template, where I used this logic instead:
ignoredEdges := !hasCollectedField(ctx, edgesField) needTotalCount := hasCollectedField(ctx, totalCountField) needPageInfo := hasCollectedField(ctx, pageInfoField) hasPagination := after != nil || first != nil || before != nil || last != nil if (needTotalCount && hasPagination) || (ignoredEdges && (needTotalCount || needPageInfo)) { c := {{ $r }}.Clone() {{- /* Clear the selection fields before counting to avoid generating invalid queries. */}} c.ctx.Fields = nil if conn.TotalCount, err = c.Count(ctx); err != nil { return nil, err } conn.PageInfo.HasNextPage = first != nil && conn.TotalCount > 0 conn.PageInfo.HasPreviousPage = last != nil && conn.TotalCount > 0 }
Thanks for your post, I’ll give it a try.
Uh oh!
There was an error while loading. Please reload this page.
PR Description: Avoid unnecessary COUNT(*) query when resolving pageInfo
When querying a connection with
pageInfo, we always execute an extra SQL query (SELECT COUNT(*)). This is not required for computing pageInfo and adds avoidable datThe
build()call in:already computes cursor fields (
startCursor,endCursor) and correctly determines pagination flags (hasNextPage,hasPreviousPage) based on the paging arguments and returned nodes.Currently we also derive flags using
TotalCount, which forces aCOUNT(*):This makes every request that includes
pageInfopay for aCOUNT(*), even when callers don’t requesttotalCountand we don’t actually need it.What this PR changes:
Why it matters
Example query affected: