Solving the N Plus One Problem
Introduction to the N+1 Problem and Data Loaders
In this lesson, we're going to address a common performance issue in GraphQL known as the N+1 problem and how to solve it using batching and lazy execution.
The N+1 problem occurs when your GraphQL server makes an excessive number of database or API calls to satisfy nested queries. For example, if you fetch a list of books along with their authors, your server might make one query to get the books (1 query) and then one additional query per book to get the author (n queries), leading to a total of n + 1 queries. This can significantly degrade the performance of your application, especially as n grows, resulting in a time complexity of for the number of queries.
In Ruby's graphql-ruby gem, we can solve this problem using the graphql-batch gem, which provides batching and caching capabilities to optimize data fetching.
Benefits of Using Batching:
- Batching: Combines multiple requests into a single batch query.
- Caching: Reduces redundant queries by remembering previously fetched results.
Defining the GraphQL Schema
In GraphQL, the schema defines the shape of the data and the queries you can perform. To illustrate how batching can solve the N+1 problem, we'll create a simple GraphQL schema with authors and books.
First, let's set up our sample data:
Now, let's define our GraphQL types using graphql-ruby's class-based syntax:
A Note on Data Structure
In previous lessons, we stored the author's name directly on each book (e.g., author: "J.R.R. Tolkien"). Here, we've shifted to a more realistic relational structure where books reference authors by author_id, and authors are stored in a separate collection.
This is how data is typically organized in databases—it avoids duplicating author information and keeps data consistent. For example, if an author's name needs to be updated, you only change it in one place rather than on every book.
However, this relational structure is exactly what creates the N+1 problem: fetching books and their authors now requires looking up each author separately. If you have 100 books, a naive implementation would make 1 query for the books plus 100 queries for the authors. This is the problem we'll solve with batching.
In this schema:
- We define
AuthorTypeandBookTypeclasses that inherit fromGraphQL::Schema::Object. - The
BookTypehas a nestedauthorfield that returns anAuthorType. The resolver usesobject[:author_id]to look up the associated author. - The
QueryTypedefines queries to fetch a list of books and a single author by ID.
Implementing Resolvers with Data Loaders
To implement batching and caching, we'll use the graphql-batch gem. This gem provides a GraphQL::Batch::Loader class that we can extend to create custom loaders.
First, let's create an AuthorLoader that batches and caches author requests:
Here's how the AuthorLoader works:
- Batching: The
performmethod receives an array of all author IDs requested during a single GraphQL execution. Instead of making separate lookups for each ID, we process them all at once. - Caching: Once an author is fetched, graphql-batch automatically caches the result. If the same author ID is requested again in the same query, the cached value is returned.
- Fulfillment: The
fulfillmethod associates each ID with its corresponding author data.
The resolvers in our type classes use AuthorLoader.for.load(id) to fetch authors. The .for method creates a loader instance for the current GraphQL execution context, and .load(id) queues the ID for batching.
Initializing and Using Data Loaders
Now, let's integrate our schema with graphql-batch and set up a Sinatra server to handle GraphQL requests:
Here's what's happening:
- We define
AppSchema, which includes ourQueryTypeand enables graphql-batch withuse GraphQL::Batch. - The Sinatra POST route
/graphqlhandles incoming GraphQL requests. - We parse the JSON request body to extract the query and variables.
- We execute the query using
AppSchema.executeand return the result as JSON.
To start the server, add this at the end of your file:
Testing Your Implementation
Finally, let's test our implementation by running some queries in a separate Ruby file:
Expected output:
Everything should work correctly, fetching the required data while only making the necessary requests. The AuthorLoader batches all author lookups together, preventing the N+1 problem.
Summary and Next Steps
In this lesson, you learned about the N+1 problem in GraphQL and how to resolve it efficiently using graphql-batch in Ruby. By defining the schema with graphql-ruby's class-based syntax, implementing custom loaders for batching and caching, integrating graphql-batch into your schema, and testing with Sinatra, you now have the skills to optimize data fetching in GraphQL applications.
You've reached the end of this course! Congratulations on making it this far. Now, dive into the practice exercises to reinforce your new skills and prepare for creating more powerful and efficient GraphQL APIs.
