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