Docs
Launch GraphOS Studio

How Federation handles the N+1 query problem

Learn how to handle the N+1 problem for operations that return a list in a federated graph

federationsubgraphs

developers quickly encounter the infamous "N+1 problem" with that return a list:

query TopReviews {
topReviews(first: 10) {
id
rating
product {
name
imageUrl
}
}
}

In a monolithic , the execution engine takes these steps:

  1. Resolve the Query.topReviews , which returns a list of Reviews.
  2. For each Review in the list, resolve the Review.product .

If the Query.topReviews returns 10 reviews, then the executor resolves Review.product 10 times. If the Reviews.product makes a database or REST for a single Product, then we'll see 10 unique calls to the . This is suboptimal for the following reasons:

  • It's more efficient to fetch the 10 products in a single (for example SELECT * FROM products WHERE id IN (<product ids>)).
  • If any reviews refer to the same product, then we're wasting resources fetching something we already have.

The solution for monolithic APIs is the dataloader pattern. All implementations support this pattern. The Apollo Server documentation explains how to use the JavaScript implementation in Node.js servers.

The N+1 problem in a federated graph

Consider the same TopReviews , but we've implemented the Review and Product types in separate :

Fortunately, ning handles N+1 queries for entities like the Product type by default! The for this works like this:

  1. First, we Fetch the list of Reviews from the Reviews using the root Query.topReviews. We also ask for the id of each associated product.
  2. Next, we extract the Product references and Fetch them in a batch to the Products 's Query._entities root .
  3. After we get back the Product entities, we merge them into the list of Reviews, indicated by the Flatten step.

Fetch (reviews)
Fetch (products)
Flatten (topReviews,[],products)

Writing efficient entity resolvers

In most implementations (including @apollo/subgraph), we don't write the Query._entities directly. Instead, we use the reference resolver API for resolving an individual reference:

const resolvers = {
Product: {
__resolveReference(productRepresentation) {
return fetchProductByID(productRepresentation.id);
},
},
};

The motivation for this API relates to a subtle, critical aspect of the subgraph specification: the order of resolved entities must match the order of the given references. If we return entities in the wrong order, those are merged with the wrong entities and we'll have incorrect results. To avoid this issue, most libraries handle entity order for you.

This does reintroduce the N+1 problem: in the example above, we'll call fetchProductByID once for each reference.

Fortunately, the solution is exactly the same in a : dataloaders. In nearly every situation, reference should use a dataloader.

const resolvers = {
Product: {
__resolveReference(product, context) {
return context.dataloaders.products(product.id);
},
},
};

Now, when the calls the Products with a batch of Product entities, we'll make a single batched request to the Products .

Next
Home
Edit on GitHubEditForumsDiscord

© 2024 Apollo Graph Inc.

Privacy Policy

Company