2026/10/12
When GraphQL resolvers cause the N+1 problem's thumbnail

When GraphQL resolvers cause the N+1 problem

  • Go
  • GraphQL

I learned which situation causes the N+1 problem while implementing a GraphQL server with gqlgen. Here are the key points.

Splitting resolvers

To avoid over-fetching from the DB, we can set resolver: true on a model’s field in gqlgen.yml. This configuration tells gqlgen to generate a separate resolver in schema.resolvers.go, so that the field is resolved only when it’s requested.

# in gqlgen.ymlmodels:  Repository:    fields:      owner:        resolver: true      issues:        resolver: true  Issue:    fields:      author:        resolver: true
// In schema.resolvers.gofunc (r *queryResolver) Repository(ctx context.Context, name string, owner string) (*model.Repository, error) {	return r.Services.GetRepositoryByParams(ctx, name, owner)}func (r *repositoryResolver) Issues(ctx context.Context, obj *model.Repository, after *string, before *string, first *int32, last *int32) (*model.IssueConnection, error) {	// ...}func (r *issueResolver) Author(ctx context.Context, obj *model.Issue) (*model.User, error) {	return r.Services.GetUserByID(ctx, obj.Author.ID)}

Generated code

After running the command, gqlgen generates code in generated.go that calls the Author() method for each issue.

go tool gqlgen generate
// generated.gofunc (ec *executionContext) _Issue_author(ctx context.Context, field graphql.CollectedField, obj *model.Issue) (ret graphql.Marshaler) {	return graphql.ResolveField(		ctx,		ec.OperationContext,		field,		func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {			return ec.fieldContext_Issue_author(ctx, field)		},		func(ctx context.Context) (any, error) {			return ec.Resolvers.Issue().Author(ctx, obj)		},		nil,		func(ctx context.Context, selections ast.SelectionSet, v *model.User) graphql.Marshaler {			return ec.marshalNUser2ᚖgithubᚗcomᚋyukiᚑyamamuraᚋgoᚋgraphᚋmodelᚐUser(ctx, selections, v)		},		true,		true,	)}

N+1 problem

Since GetUserByID() method is called for each issue and runs a SQL query every time, a single GraphQL query for a repository results in causing N+1 queries.

// In services/users.gofunc (u *userService) GetUserByID(ctx context.Context, id string) (*model.User, error) {	dbUser, err := u.q.GetUserByID(ctx, id)	if err != nil {		return nil, err	}	user := convertUser(dbUser)	return &user, nil}
// In graph/db/users.sql.go, generated by sqlcconst getUserByID = `-- name: GetUserByID :oneSELECT id, name, project_v2 FROM users WHERE id = ?`func (q *Queries) GetUserByID(ctx context.Context, id string) (User, error) {	row := q.db.QueryRowContext(ctx, getUserByID, id)	var i User	err := row.Scan(&i.ID, &i.Name, &i.ProjectV2)	return i, err}
$ go run .
2026/10/12 17:56:03 connect to http://localhost:8080/ for GraphQL playground
2026/10/12 17:56:05 [SQL] -- name: GetRepositoryByParams :one SELECT repositories.id, owner, repositories.name, created_at, users.id, users.name, project_v2 FROM repositories JOIN users ON repositories.owner = users.id WHERE repositories.name = ?1 AND users.name = ?2 [repo1 foo]
2026/10/12 17:56:05 [SQL] -- name: GetUserByID :one SELECT id, name, project_v2 FROM users WHERE id = ? [U_1]
2026/10/12 17:56:05 [SQL] -- name: ListIssuesByRepository :many SELECT id, url, title, closed, number, author, repository FROM issues WHERE repository = ? ORDER BY number [REPO_1]
2026/10/12 17:56:05 [SQL] -- name: GetUserByID :one SELECT id, name, project_v2 FROM users WHERE id = ? [U_2]
2026/10/12 17:56:05 [SQL] -- name: GetUserByID :one SELECT id, name, project_v2 FROM users WHERE id = ? [U_1]
2026/10/12 17:56:05 [SQL] -- name: GetUserByID :one SELECT id, name, project_v2 FROM users WHERE id = ? [U_1]

References

gqlgen

graphql servers the easy way

gqlgen.com

リゾルバの実装 - 応用編|Goで学ぶGraphQLサーバーサイド入門

zenn.dev