Skip to main content

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.

info

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 the userId from the parent Post. 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's id 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.