Docs
Launch GraphOS Studio

Query plans

How your router plans operations across subgraphs


⚠️ You definitely don't need to understand the details of query plans to get started with Apollo Federation! This information is provided primarily for advanced debugging purposes.

Whenever your receives an incoming , it needs to figure out how to use your s to populate data for each of that operation's s. To do this, the router generates a query plan:

Parallel
Fetch (reviews)
Fetch (users)
Fetch (hotels)
Flatten (latestReviews,[],hotel)

A plan is a blueprint for dividing a single incoming into one or more s that are each resolvable by a single . Some of these operations depend on the results of other s, so the plan also defines any required ordering for their execution.

Example graph

Let's say our federated includes these s:

Hotels subgraph
type Hotel @key(fields: "id") {
id: ID!
address: String!
}
type Query {
hotels: [Hotel!]!
}
Reviews subgraph
type Hotel @key(fields: "id") {
id: ID! @external
reviews: [Review!]!
}
type Review {
id: ID!
rating: Int!
description: String!
}

Based on these s, clients can execute the following against our :

query GetHotels {
hotels { # Resolved by Hotels subgraph
id
address
reviews { # Resolved by Reviews subgraph
rating
}
}
}

This includes s from both the Hotels and the Reviews subgraph. Therefore, the needs to send at least one query to each subgraph to populate all requested fields.

Take a look at the 's plan for this query:

This syntax probably looks confusing! 🤔 Let's break it down.

Structure of a query plan

A plan is defined as a hierarchy of nodes that looks like a JSON or when serialized.

At the top level of every plan is the QueryPlan node:

QueryPlan {
...
}

Each node defined inside the QueryPlan node is one of the following:

NodeDescription
FetchTells the gateway to execute a particular operation on a particular subgraph.
ParallelTells the gateway that the node's immediate children can be executed in parallel.
SequenceTells the gateway that the node's immediate children must be executed serially in the order listed.
FlattenTells the gateway to merge the data returned by this node's child Fetch node with data previously returned in the current Sequence.

Each of these is described in further detail below.

Fetch node

A Fetch node tells the to execute a particular on a particular . Every plan includes at least one Fetch node.

# Executes the query shown on the "books" subgraph
Fetch(service: "books") {
{
books {
title
author
}
}
},

The node's body is the to execute, and its service indicates which to execute the against.

In our example graph above, the following requires data only from the Hotels :

query GetHotels {
hotels {
id
address
}
}

Because this doesn't require orchestrating operations across multiple s, the entire query plan contains just a single Fetch node:

QueryPlan {
Fetch(service: "hotels") {
{
hotels {
id
address
}
}
},
}

The Fetch node uses a special syntax when it's resolving a reference to an entity across s. For details, see Resolving references with Flatten.

Parallel node

A Parallel node tells the that all of the node's immediate children can be executed in parallel. This node appears in plans whenever the router can execute completely independent s on different s.

Parallel {
Fetch(...) {
...
},
Fetch(...) {
...
},
...
}

For example, let's say our federated has a Books and a Movies subgraph. And let's say a client executes the following to fetch separate lists of books and movies:

query GetBooksAndMovies {
books {
id
title
}
movies {
id
title
}
}

In this case, the data returned by each does not depend on the data returned by any other . Therefore, the can both subgraphs in parallel.

The plan for the looks like this:

Sequence node

A Sequence node tells the that the node's immediate children must be executed serially in the order listed.

Sequence {
Fetch(...) {
...
},
Flatten(...) {
Fetch(...) {
...
}
},
...
}

This node appears in plans whenever one 's response depends on data that first must be returned by another . This occurs most commonly when a requests s of an entity that are defined across multiple s.

As an example, we can return to the GetHotels from our example graph:

query GetHotels {
hotels { # Resolved by Hotels subgraph
id
address
reviews { # Resolved by Reviews subgraph
rating
}
}
}

In our example , the Hotel type is an entity. Hotel.id and Hotel.address are resolved by the Hotels , but Hotel.reviews is resolved by the Reviews . And our Hotels subgraph needs to resolve first, because otherwise the Reviews doesn't know which hotels to return reviews for.

The plan for the looks like this:

As shown, this plan defines a Sequence that executes a Fetch on the Hotels before executing one on the Reviews . (We'll cover the Flatten node and the second Fetch's special syntax next.)

Flatten node

A Flatten node always appears inside a Sequence node, and it always contains a Fetch node. It tells the to merge the data returned by its Fetch node with data that was previously Fetched during the current Sequence:

Flatten(path: "hotels.@") {
Fetch(service: "reviews") {
...
}
}

The Flatten node's path tells the at what position to merge the newly returned data with the existing data. An @ element in a path indicates that the immediately preceding path element returns a list.

In the snippet above, the data returned by the Flatten's Fetch is added to the Sequence's existing data within the objects contained in the hotels list .

Expanded example

Once again, let's return to the GetHotels on our example graph:

query GetHotels {
hotels { # Resolved by Hotels subgraph
id
address
reviews { # Resolved by Reviews subgraph
rating
}
}
}

The plan for this first instructs the to execute this query on the Hotels :

{
hotels {
id
address
__typename # The router requests this to resolve references (see below)
}
}

At this point, we still need review-related information for each hotel. The plan next instructs the to query the Reviews for a list of Hotel objects that each have this structure:

{
reviews {
rating
}
}

Now, the needs to know how to merge these Hotel objects with the data it already fetched from the Hotels . The Flatten node's path tells it exactly that:

Flatten(path: "hotels.@") {
...
}

In other words, "Take the Hotel objects returned by the Reviews and merge them with the Hotel objects in the top-level hotels returned by the first ."

When the completes this merge, the resulting data exactly matches the structure of the client's original :

{
hotels {
id
address
reviews {
rating
}
}
}

Resolving references with Flatten

Like Sequence nodes, Flatten nodes appear whenever one 's response depends on data that first must be returned by another . This almost always involves resolving entity s that are defined across multiple subgraphs.

In these situations, the Flatten node's Fetch needs to resolve a reference to an entity before fetching that entity's s. When this is the case, the Fetch node uses a special syntax:

Flatten(path: "hotels.@") {
Fetch(service: "reviews") {
{
... on Hotel {
__typename
id
}
} =>
{
... on Hotel {
reviews {
rating
}
}
}
},
}

Instead of containing a , this Fetch node contains two GraphQL fragments, separated by =>.

  • The first is a representation of the entity being resolved (in this case, Hotel). Learn more about entity representations.
  • The second contains the entity s and subfields that the needs the to resolve (in this case, Hotel.reviews and Review.rating).

When the sees this special Fetch syntax, it knows to a 's Query._entities field. This is what enables a to provide direct access to any available fields of an entity.

Now that you've learned about each plan node, take another look at the example query plan in Example graph to see how these nodes work together in a complete plan.

Viewing query plans

You can view the plan for a particular in any of the following ways:

Outputting query plans with headers

With the Apollo Router v0.16.0+ and @apollo/gateway v2.5.4+, you can pass the following headers to return the plans in the response :

  • Including the Apollo-Query-Plan-Experimental header returns the plan in the response
  • Additionally including the Apollo-Query-Plan-Experimental-Format header with one of the supported options changes the output format:
    • A value of prettified returns a human-readable string of the plan
    • A value of internal returns a JSON representation of the plan

Outputting query plans with @apollo/gateway

Your gateway can output the plan for each incoming as it's calculated. To do so, add the following to the file where you initalize your ApolloGateway instance:

  1. Import the serializeQueryPlan function from the @apollo/query-planner library:

    const {serializeQueryPlan} = require('@apollo/query-planner');
  2. Add the experimental_didResolveQueryPlan option to the object you pass to your ApolloGateway constructor:

    const gateway = new ApolloGateway({
    experimental_didResolveQueryPlan: function(options) {
    if (options.requestContext.operationName !== 'IntrospectionQuery') {
    console.log(serializeQueryPlan(options.queryPlan));
    }
    }
    });

    The value you provide for this option is a function that's called every time the gateway generates a plan. The example function above logs the generated query plan for every except for queries (such as those sent periodically by tools like the Apollo Studio Explorer). You can define any logic you want to log plans or otherwise interact with them.

    For all available options passed to your function, see the source.

Previous
Monitoring
Next
Error codes
Edit on GitHubEditForumsDiscord