Skip to content

Navigation Menu

Sign in
Sign up

perf(entgql): avoid COUNT(*) for pageInfo-only connection queries - #625

Open
tutranngoc wants to merge 1 commit into
ent:master from
tutranngoc:master
Open

perf(entgql): avoid COUNT(*) for pageInfo-only connection queries #625
tutranngoc wants to merge 1 commit into
ent:master from
tutranngoc:master

Conversation

@tutranngoc

@tutranngoc tutranngoc commented Jan 29, 2026
edited
Loading

Copy link
Copy Markdown

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 dat

The build() call in:

conn.build(nodes, pager, after, first, before, last)

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 a COUNT(*):

conn.PageInfo.HasNextPage = first != nil && conn.TotalCount > 0
conn.PageInfo.HasPreviousPage = last != nil && conn.TotalCount > 0

This makes every request that includes pageInfo pay for a COUNT(*), even when callers don’t request totalCount and we don’t actually need it.

What this PR changes:

  • Stops triggering SELECT COUNT(*) when it’s not needed for pageInfo.
  • Relies on the existing build() logic for pageInfo instead of TotalCount.

Why it matters

  • Reduces database load by removing redundant COUNT(*) queries.
  • Helps prevent query amplification in nested resolvers (a common source of N+1-like behavior).
  • Improves latency for list queries that request pageInfo but not totalCount.

Example query affected:

variants(first: $firstVariant, after: $afterVariant) {
 edges {
 cursor
 node {
 id
 title
 barcode
 baseID
 costPrice
 height
 }
 }
 pageInfo {
 hasNextPage
 endCursor
 }
 }
image

macr reacted with thumbs up emoji
@tutranngoc tutranngoc changed the title (削除) entgql: update pagination logic to check only totalCountField for edges handling (削除ここまで) (追記) perf(entgql): avoid COUNT(*) for pageInfo-only connection queries (追記ここまで) Jan 29, 2026

Copy link
Copy Markdown
Author

@a8m @giautm Please check this pull request for me 🙇‍♂️🙇‍♂️🙇‍♂️

giautm commented Jan 29, 2026
edited
Loading

Copy link
Copy Markdown
Collaborator

@tutranngoc fyi, you can use entcache package to cache query result of count(). So it wont hit database twice.

https://github.com/ariga/entcache

Copy link
Copy Markdown
Author

@tutranngoc fyi, you can use entcache package to cache query result of count(). So it wont hit database twice.

https://github.com/ariga/entcache

@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.

Copy link
Copy Markdown

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
}
giautm and tutranngoc reacted with thumbs up emoji

Copy link
Copy Markdown
Author

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Reviewers

No reviews

Assignees

No one assigned

Labels

None yet

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

AltStyle によって変換されたページ (->オリジナル) /