GraphQL Optimization: It’s More Than N+1 (Part 1)

GraphQL was introduced to ease the access to backend data for frontend developers. It gives frontend developers the paradigm they need to simplify the specification of the data for their applications. In GraphQL, the developer declaratively specifies what data they want, not how to get it. As experts in the database field, having arrived on the scene at the rise of relational databases and the emergence of object relational extensions, we at StepZen are maniacally focused on bringing the lessons learned from our heritage to the world of GraphQL.

Why Optimize?

Besides optimization of data access being core to our DNA, optimization of GraphQL is important to opening up the aperture for rapid frontend development. The obvious optimization opportunity for a GraphQL operation is to minimize the trips to the backend data sources, whether they are databases, REST APIs or other GraphQL APIs.

While GraphQL makes it easier for developers to specify what data they want and gives them autonomy from the owners of the backends to a degree, these backends are likely under the control of another developer, DBA and/or organization who will care about any extraneous load that will be introduced to their backends. Reducing traffic to the backends can also: reduce cost by reducing the number of calls to a cost-per-call backend system avoid rate limits for backends improve the application performance by reducing the latency of the GraphQL endpoint

The spamming of backends is often referred to as the N+1 problem, when the application makes N requests instead of 1 to retrieve an object’s details or its child entities. As we will explain, a GraphQL schema gives a performant GraphQL server the context it needs to avoid such spamming, but it also enables many other opportunities for reducing the number of backend system requests, hence it is more than N+1.

The N+1 Problem

Frontend applications often result in a cascade of independent requests to a single backend that iteratively retrieves the application data. For example, an application may receive a list of authors in a single REST API request, but may independently and iteratively make further requests, sometimes to the same endpoint, sometimes to different endpoints, to retrieve the information required to display the author’s name, address and rating.

This is a variant of the well-known N+1 database performance anti-pattern introduced by object/relational mappers. While simplifying the data access for the developer, O/R mappers also encouraged a pattern of spamming the database with a lot of piecemeal requests. Naive implementations would execute queries against the backends exactly as the programmer invoked, but fortunately O/R mapping engines came up with several strategies for mitigating this pattern.

In the case of web APIs, this problem becomes a bit more obscure because the original endpoint does not return the information the application needs in one call, and different parts of the application may need different slices of data. As performance problems appear, developers may be able to analyze their data access and consolidate backend calls, but they may also need to request different endpoints to do so, and then convince a backend developer to provide it. GraphQL alleviates this tension between frontend and backend developers, allowing the developers to request all and only the data they require from a single endpoint. It is then the job of the GraphQL server to recognize the pattern and avoid backend spamming.

More than N+1

While most N+1 solutions center around reducing multiple requests to filling in detail data for a given entity (author name, rating, etc.), or for retrieving all the child objects for a given entity (such as the book details for all of an author’s books), the general principle of making one request instead of many can be applied much more broadly.

A GraphQL operation (request to a GraphQL endpoint) expresses what the frontend developer needs, not how to get it, expressed as a selection set of fields, typically with sub-selections.

The selection set can be arbitrarily deep and arbitrarily wide, which allows the frontend developer to fetch the data type needed in a single request from the GraphQL endpoint.

Depth is how deeply nested a returned object may be. For example, consider the following schema:


type Author {
  id: ID!
  name: String
  genre: String
  publisher: String
  rating: Float
  books: [Book]
}

type Book {
  id:ID!
  title: String
  auth_id: ID
}

type Query {
  author(id: ID!): Author
}

The following selection set’s depth is three:

{author(id:1) { name books { title}}

Sometimes the depth is limited by the schema, as is the case with our current schema. However, a GraphQL schema can still be very deep and often recursive. Consider that data about an author may include a list of similar authors. This can be achieved by extending our above schema as follows:

extend type Author {
  similar: [Author]
}

Such a schema leads to selection sets that are arbitrarily deep. The optimization opportunity that can be seen from this schema is to recognize when a similar author has been previously retrieved in the traversal of the data. Optimizing in this fashion can also assist in recognizing cycles in the data and avoiding unnecessary deeply nested traversals.

Width is how many fields are selected at a given depth in the selection tree. Width is not limited by the schema, but can occur in selections due to aliases. For example, in this trivial example the width is three at the name level with the same field being selected three times using aliases: { author(id:1) { n1:name n2:name n3:name}}

Width at the top level is a key feature to fulfill the goal of the frontend developer issuing a single request to fetch the required data and leads to arbitrary wide operations. For example, consider the data to display a page with information about a user’s favorite authors in addition to promotions for the current top-selling books and local book signings:

{
   a1: author(id:1) { name birthplace email}
   a2: author(id:2) { name birthplace email}
   topSellingBooks { title }
   authorsOnTour(zip:”94118”) { name birthplace email}
}

A couple of optimization opportunities can be seen in this selection:

  • Will fetching author information be a single request to the backend or multiple?

  • If authorsOnTour returns authors one and/or two can the execution for a1 or a2 reuse the work of authorsOnTour?

With the request typically generated, possibly by independent code modules, an application may issue a request with duplicate, or near-duplicate items. For example, in an operation that selects 20-plus top-level fields, there could be similar items, such as:

{
< other fields >
 author_for_popup: author(id:1) { name genre publisher }
< other fields >
 main_author:  author(id:1) { name books { title } }
< other fields >
}

Can the GraphQL server discover these so that it effectively executes a single backend request corresponding to:

author(id:1) { name genre publisher books {title }}

With the selection set being arbitrarily deep and wide, you can see now that GraphQL optimization opportunities can exist across the entire selection tree in the operation, not just filling in an entity’s detail for its next level, including across top-level fields (or indeed fields at any level).

The frontend developer should not care about these optimizations. They request the data they need, in the shape they need, potentially with duplicates and expect the correct results.

The GraphQL server can execute the query any way it wants as long as it produces the right results. Just like the early days of SQL, a declarative query can still perform poorly if the runtime for fulfilling the query does not take advantage of the context it has to run the query efficiently.

StepZen’s declarative approach provides that context, such as the relationship of fields to backends and their types (such as database or GraphQL) and capabilities.

Optimization Techniques

With a declarative approach, a GraphQL server can use its knowledge of an incoming operation, the operation's variables, the schema and the relationship of fields to backends, to analyze and optimize the request, thereby reducing operation latency for the frontend.

In our next post, we’ll dig in to some specific optimization techniques that you can employ to reduce the number of backend requests for an operation. >> GraphQL Optimization Part 2: Deduplication & Reuse

Meanwhile, if you’d like to brainstorm a use case, discuss a performance challenge or learn more about implementing GraphQL with StepZen, we’d love to connect. Drop us a note via this page and we'll be in touch right away.