Solving the N+1 Problem in GraphQL: A Performance Breakthrough

The N+1 Problem: A Performance Killer

The N+1 problem arises when GraphQL resolvers only handle one object at a time, leading to an exponential increase in database queries as the graph grows deeper. For example, retrieving a list of directors and their films would require 1 + 10 = 11 queries, and adding each film’s list of actors would result in 1 + 10 + 100 = 111 queries. This problem can quickly spiral out of control, making GraphQL inefficient and slow.

Dealing with the N+1 Problem

Facebook’s DataLoader utility, implemented in Node.js, introduced the batching strategy to solve the N+1 problem. This approach defers resolving segments of the query until a later stage, allowing all objects of the same kind to be resolved together in a single query. While effective, this solution is often an afterthought, adding complexity to the development process.

A Better Approach: Baking in Deferred Resolution

Instead of offering deferred resolution as an add-on, GraphQL servers should incorporate it as the default strategy. By transferring the responsibility of resolving object types from resolvers to the server’s data loading engine, we can eliminate the N+1 problem. Here’s how:

  • Resolvers return IDs, not objects: When resolving relationships, resolvers should return IDs, not objects.
  • DataLoader retrieves objects: A DataLoader entity retrieves objects for the IDs, efficiently including all IDs in a single query.
  • GraphQL server’s data loading engine: The engine glues the resolvers and DataLoader together, obtaining object IDs from resolvers and retrieving objects through the DataLoader.

Implementing the Adapted Approach

In PHP, the GraphQL by PoP implementation demonstrates this adapted strategy. We split resolvers into two entities: FieldResolvers and TypeDataLoaders. FieldResolvers resolve fields, while TypeDataLoaders load objects of a specific type. The code becomes more concise, with a clear separation of concerns.


// FieldResolvers resolve fields
class FieldResolver {
  public function resolve($parent, $args) {
    // Return IDs, not objects
    return [$id1, $id2,...];
  }
}

// TypeDataLoaders load objects of a specific type
class TypeDataLoader {
  public function load($ids) {
    // Retrieve objects for the IDs in a single query
    return [$object1, $object2,...];
  }
}

Testing the New Approach

To demonstrate the effectiveness of this solution, we can execute a complex query involving a graph 10 levels deep. The results show that the N+1 problem is avoided, and the query is resolved efficiently.


query {
  directors {
    films {
      actors {
        name
      }
    }
  }
}

Leave a Reply