Tackling N + 1
The N+1 problem is a pervasive and critical issue in application development that occurs when an application ends up issuing a large number of downstream requests, for a single request from upstream. Let's understand with an example:
Scenario
Consider we're developing a feature that involves consuming data from the JSON Placeholder API. The feature requires fetching posts and the details of the authors of these posts.
Here's an illustration of how this might typically be implemented:
Fetching Posts
First, we send a request to retrieve all posts:
curl https://jsonplaceholder.typicode.com/posts
The above request fetches a list of posts from the API, each of which includes a userId
field indicating the author of the post.
Fetching Users
Then, for each post, we need to get the author's details. A request for a specific user might look like this:
curl https://jsonplaceholder.typicode.com/users/1
If we received 100 posts from our first request, we would then make 100 more requests to get each post's author details, resulting in a total of 101 requests.
The N+1 problem, demonstrated using the JSON Placeholder API, refers to the issue where an initial API request generates multiple additional requests. For instance, acquiring 100 posts and then making another request for each post's author details culminates in 101 total requests.
In real-world applications with thousands of posts and users, this problem intensifies. Each user request can yield hundreds or thousands of additional server requests, stressing server resources, and leading to slower response times, higher server costs, and a degraded user experience. This situation can even lead to server downtime due to the high volume of requests, impacting service availability. Therefore, it's crucial to address the N+1 problem during the design and development of applications involving numerous API requests. Solutions to this issue will be discussed in subsequent sections.
Using the CLI
The TailCall CLI is a potent tool for developers, helping identify N+1 issues in GraphQL applications even before any requests are made or configurations are published in production. This proactive approach allows for potential issues to be mitigated right from the development stage.
Before diving into the usage, ensure you have familiarized yourself with the basics of the TailCall CLI. If you haven't already, please refer to the Installation guide, which will walk you through the setup process and help you understand the key commands.
Jsonplaceholder Example
Here is a sample .graphql
file that we'll be examining:
schema @upstream(baseURL: "http://jsonplaceholder.typicode.com") {
query: Query
}
type Query {
posts: [Post] @http(path: "/posts")
}
type User {
id: Int!
name: String!
username: String!
email: String!
phone: String
website: String
}
type Post {
id: Int!
userId: Int!
title: String!
body: String!
user: User @http(path: "/users/{{value.userId}}")
}
This schema allows clients to fetch a list of posts, with each post including its associated user data. However, as currently defined, it suffers from the N+1 problem: each post will trigger an additional request to fetch its associated user data.
We will demonstrate how to identify this issue using the TailCall CLI in the next section.
Running the TailCall CLI
With the check
command, TailCall CLI can assist you in identifying potential N+1 issues in a GraphQL file:
tailcall check ./jsonplaceholder.graphql
No errors found.
N + 1: 1
The N + 1: 1
line tells you that the TailCall CLI has detected one potential N+1 issue.
For a deeper understanding of these issues, you can use the --n-plus-one-queries
parameter:
tailcall check ./jsonplaceholder.graphql --n-plus-one-queries
No errors found.
N + 1: 1
query { posts { user } }
This parameter uncovers the minimal query that can trigger an N+1 problem. In the above case, query { posts { user } }
, represents the minimal query that could lead to an N+1 problem. It illustrates that within the posts
query, each post is triggering an additional request to fetch its associated user
data.
Solving Using Batching
Batching is an effective technique to group multiple similar requests into one, substantially reducing the number of server calls. The TailCall CLI provides this capability to address the typical N+1 issue that arises in GraphQL.
To tap into this feature, modify the @http
directive on Post.user
in your GraphQL schema as follows:
type Post {
id: Int!
userId: Int!
title: String!
body: String!
user: User
@http(
path: "/users"
query: [{key: "id", value: "{{value.userId}}"}]
groupBy: ["id"]
)
}
Understanding the Update
The described changes introduce significant tweaks to the @http
directive and incorporate the @groupBy
operator:
query: [{key: "id", value: "{{value.userId}}"}]
: Here, TailCall CLI is instructed to generate a URL where the user id aligns with theuserId
from the parentPost
. For a batch of posts, the CLI compiles a single URL, such as/users?id=1&id=2&id=3...id=10
, consolidating multiple requests into one.groupBy: ["id"]
: This parameter instructs the system to convert the list of responses into a map internally, using the user'sid
as the unique key. In essence, it allows the system to differentiate each user value in the response list.
By using this approach, you can reduce the number of requests from 101 (for 100 posts plus one initial request for the post list) to just 2. This significant optimization effectively handles the N+1 problem, thereby enhancing your application's efficiency and user experience.