I had a front end developer comment that one of API's was slow. The developer was trying download all of the products and their related image. After a quick investigation it turned out we had hit the dreaded N+1 problem.
Using AppSync's new context info object we've been able to apply a simple optimization that has significantly reduced the number of DynamoDB queries our API was making.
A simplified version of the schema would look something like this:
type Image {
id: ID!
url: String
}
type Product {
id: ID!
name: String
image: Image
}
type Query {
listImages: [Image]
listProducts: [Products]
}
The AppSync resolvers for Query.listProducts
and Query.listImages
both performed a DynamoDB Query
allowing one request to return multiple records. The Product.image
resolver used a GetItem
to fetch the image for each product.
In my head the developer would use a query like this
query ListProductsWithImage {
listProducts {
id
name
image {
id
url
}
}
}
You might notice a problem here. While listProducts
can be performed with a single DynamoDB operation I'm performing another DynamoDB operation for every record that it returns. The N+1 problem.
It turned out that the developer was actually using two queries and joining the data in the application. The first downloaded all of the images
query ListImages {
listImages {
id
url
}
}
The second all of the products
query ListProducts {
listProducts {
id
name
image {
id
}
}
}
While this could have been efficient it wasn't because the Product.image
resolver was still performing a GetItem
for each product returned by Query.listProducts
.
This was crazy because we store the Image
id
in the imageId
attribute as part of the Product
record in DynamoDB.
One solution would be to expose the imageId
in Product
but this is discouraged in GraphQL schema design because it exposes the internal implementation details to an external client making it harder to change the implementation later.
With the new $ctx.info
object and early returning I can now optimize my resolvers for this use case by adding the following code at the top of the Product.image
request resolver.
#if ($ctx.info.selectionSetList.size() == 1 && $ctx.info.selectionSetList[0] == "id")
#return({ "id": "$ctx.source.imageId" })
#end
If id
is the only image
field requested it will now return early, before the GetItem
operation, using $ctx.source.imageId
as the id
.
This small optimization improves API performance and saves money by removing unnecessary GetItem
requests without exposing the internal implementation but still allows the client to fetch additional fields if required.