Nplusone hero

How FaunaDB's GraphQL API Solves the n+1 Problem

Dealing with n+1 problems is a common issue when building a GraphQL server. In this post, we’ll learn what the n+1 problem is, how it's solved in most GraphQL servers, and why you don’t need to care about it with FaunaDB’s GraphLQ API.

The n+1 problem

Before diving into the aspects of the n+1 problem, let’s review how GraphQL queries are handled on the server side. When a query hits the server, it is processed by the so-called resolver functions. A resolver is a field-specific function, in charge of fetching the proper data for a given field. This means that for every field present in the query, the server calls its corresponding resolver function in order to fetch the requested data.

In most implementations, the order in which the server calls each resolver function is the same as how the fields appear in the query graph. While resolvers for fields at the same level may be called at the same time, values for child fields are resolved after their parent field. This is known as a breadth-first search in graph theory.

Now, the n+1 problem occurs when processing a query involves performing one initial request to a data source along with n subsequent requests in order to fetch all of the data. In order to understand this more clearly, let’s take a look at an example.

Suppose we have a schema which establishes a relation between a Book and an Author type:

type Book {
  title: String!
  author: Author!
}

type Author {
  firstName: String!
  lastName: String!
}

type Query {
  allBooks: [Book]!
}

And we call the allBooks query to get all of the Books along with their Authors:

query {
  allBooks {     # fetches books (1 request)
    title       
    author {     # fetches authors for each book (n requests for n authors)
      firstName
      lastName
    }
  }
}

As explained above, the server goes through each field by following the order of the graph, and calls the proper resolver functions in order to process the query. It starts by making one request (1) to fetch the data for the books field, and then make multiple subsequent requests (n) for all of the associated author fields. Thus, as a result, there will be n+1 requests in order to fetch all of the data.

If our data source was a SQL-like database, this might result in the following queries:

SELECT * FROM Book
SELECT * FROM Author WHERE authorId = 1
SELECT * FROM Author WHERE authorId = 2
SELECT * FROM Author WHERE authorId = 3
...

At this point, you may have noticed that this is a very inefficient and unpredictable way of retrieving data. When working with large sets, this can easily lead to notorious performance issues. Moreover, you might even end up requesting the same element more than once!

Most data sources already support fetching a selection of elements in a single operation. So, shouldn’t there be a way to rearrange how a query is processed in order to have one request instead of the n subsequent requests?

Batching to the rescue

Fortunately, there’s a solution to this problem through an old well-known technique: batching. Most GraphQL server libraries offer the ability to defer the resolution of a resolver function and batch all of their executions together. In other words, rather than immediately fetching the data for each field one by one, all fields sharing the same batch resolver get their data at once with a single request.

Let’s take a look at the previous query and see how it is processed using batching:

query {
  books {        # fetches books (1 request)
    title       
    author {     # defers the fetching of author (no request)
      firstName
      lastName
    }
  }              # fetches all authors for each book at once (1 request)
}

This time, the server goes through the query, fetches the data for the books field, and defers the resolution of the author fields for later. Once all of the author fields have been traversed, a single request is made in order to fetch the data for all of them.

If we were working with a SQL-like database for storing the data, we might get the following queries:

SELECT * FROM Book
SELECT * FROM Author WHERE authorId IN (1, 2, 3, ...)

As you may have already noticed, we have to perform only two queries now to get all of the data! Thanks to batching, we have reduced the number of queries to a constant number and thus avoid potential performance issues.

Although this a well-known solution for the n+1 problem, we still need to take care and handle each case individually ourselves. Libraries like Dataloader or Apollo's data sources do a great job at easing the implementation of deferred resolvers and batching, and they even provide additional features for handling other cases. However, we cannot escape the fact that we still need to put some code together in order to cover every possible n+1 problem in our server. Since this is such a common issue, you might be wondering if there’s another way to solve all of the potential n+1 problems once and for all...

One query to rule them all

With FaunaDB’s GraphQL API, we’ve taken things a step further. First, we have leveraged one of Fauna’s distinctive features: the Fauna Query Language or FQL. FQL is a powerful and comprehensive language that allows complex and precise manipulation and retrieval of data stored within FaunaDB. While not a general-purpose programming language, it provides much of the functionality expected from one.

With FQL at our disposal, we have used resolvers slightly differently from how they are used normally. Whenever FaunaDB’s GraphQL API server receives a query, all of its fields are first resolved into an FQL expression. That is, instead of immediately fetching the data field-by-field, a built-in resolver automatically generates the equivalent FQL expression for each field. Once an expression has been assigned to all fields, all expressions are combined into one FQL query. As a result, the complete GraphQL query is translated into a single FQL query. Finally, this FQL query is executed and all data for all of the fields is fetched at once.

Let’s take a look at the previous example one last time and see how this works:

query {
  books {        # generates an FQL expression for fetching books (no request)
    title       
    author {     # generates an FQL expression for fetching the author (no request)
      firstName
      lastName
    }
  }              # composes all expressions into one single FQL query
}                # and fetches all of the data at once (1 request)

In this case, the server simply assigns an FQL expression for the books field, and another FQL expression for the author field. Both expressions are then be composed into one query and the data for the books and author fields is fetched together.

As mentioned, the complete GraphQL query is translated into a single FQL query similar to the one below:

Map(
  Paginate(Match(Index("allBooks"))),
  Lambda("bookRef",
    Let(
      {
        "book": Get(Var("bookRef")),
        "author": Get(Select(["data", "author"], Var("book")))
      },
      {
        "title": Select(["data", "title"], Var("book")),
        "author": {
          "firstName": Select(["data", "firstName"], Var("author")),
          "lastName": Select(["data", "lastName"], Var("author"))
        }
      }
    )
  )
);

This means that any given GraphQL query always incurs only one single request to the database. By design, we have resolved the n+1 problem in order to cover any possible use case—regardless of what query has to be processed. And all of this is done without you even having to write a single line of code!

Conclusion

In this post, we have discussed the aspects of the n+1 problem, how batching can help you to keep the number of requests in line, and how this recurrent issue has already been solved for any possible use case within FaunaDB’s GraphQL API.

Throughout this post, we have used a rather simple query to easily illustrate what the n+1 problem is and how it can be solved in different ways. In a real world scenario, GraphQL queries can be vastly complex and even using a traditional optimization technique like batching can require several requests to a data source. In contrast, FaunaDB’s GraphQL API guarantees that for any GraphQL query you make—no matter the complexity—there is always one single request.

If you are interested in learning more about FaunaDB’s GraphQL API, you can start with this simple tutorial.