Query plans

How your gateway 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 gateway receives an incoming GraphQL operation, it needs to figure out how to use your subgraphs to populate data for each of that operation's fields. To do this, the gateway generates a query plan:

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

Example graph

Let's say our federated graph includes these subgraphs:

GraphQL
1# Hotels subgraph
2type Hotel @key(fields: "id") {
3  id: ID!
4  address: String!
5}
6
7type Query {
8  hotels: [Hotel!]!
9}
GraphQL
1# Reviews subgraph
2extend Type Hotel @key(fields: "id") {
3  id: ID! @external
4  reviews: [Review!]!
5}
6
7type Review {
8  id: ID!
9  rating: Int!
10  description: String!
11}

Based on these subgraphs, clients can execute the following query against our gateway:

GraphQL
1query GetHotels {
2  hotels { # Resolved by Hotels subgraph
3    id
4    address
5    reviews { # Resolved by Reviews subgraph
6      rating
7    }
8  }
9}

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

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

Click to expand
GraphQL
1# Top-level definition
2QueryPlan {
3  # Indicates child nodes must be executed serially in order
4  Sequence {
5    # Execute the contained operation on the `hotels` subgraph
6    Fetch(service: "hotels") {
7      {
8        hotels {
9          id
10          address
11          __typename
12        }
13      }
14    },
15    # Merge the data from this contained Fetch with earlier data
16    # from this Sequence, at the position indicated by `path`
17    # (The @ path element indicates the previous element returns a list)
18    Flatten(path: "hotels.@") {
19      # Execute this operation on the `reviews` subgraph
20      Fetch(service: "reviews") {
21        # Use these fields as the representation of a Hotel entity
22        {
23          ... on Hotel {
24            __typename
25            id
26          }
27        } => # Populate these additional fields for the corresponding Hotel
28        {
29          ... on Hotel {
30            reviews {
31              rating
32            }
33          }
34        }
35      },
36    },
37  },
38}

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

Structure of a query plan

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

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

GraphQL
1QueryPlan {
2  ...
3}

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 gateway to execute a particular GraphQL operation on a particular subgraph. Every query plan includes at least one Fetch node.

GraphQL
1# Executes the query shown on the "books" subgraph
2Fetch(service: "books") {
3  {
4    books {
5      title
6      author
7    }
8  }
9},

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

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

GraphQL
1query GetHotels {
2  hotels {
3    id
4    address
5  }
6}

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

GraphQL
1QueryPlan {
2  Fetch(service: "hotels") {
3    {
4      hotels {
5        id
6        address
7      }
8    }
9  },
10}

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

Parallel node

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

GraphQL
1Parallel {
2  Fetch(...) {
3    ...
4  },
5  Fetch(...) {
6    ...
7  },
8  ...
9}

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

GraphQL
1query GetBooksAndMovies {
2  books {
3    id
4    title
5  }
6  movies {
7    id
8    title
9  }
10}

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

The query plan for the operation looks like this:

Click to expand
GraphQL
1QueryPlan {
2  Parallel {
3    Fetch(service: "books") {
4      {
5        books {
6          id
7          title
8        }
9      }
10    },
11    Fetch(service: "movies") {
12      {
13        movies {
14          id
15          title
16        }
17      }
18    },
19  },
20}

Sequence node

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

GraphQL
1Sequence {
2  Fetch(...) {
3    ...
4  },
5  Flatten(...) {
6    Fetch(...) {
7      ...
8    }
9  },
10  ...
11}

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

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

GraphQL
1query GetHotels {
2  hotels { # Resolved by Hotels subgraph
3    id
4    address
5    reviews { # Resolved by Reviews subgraph
6      rating
7    }
8  }
9}

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

The query plan for the operation looks like this:

Click to expand
GraphQL
1QueryPlan {
2  Sequence {
3    Fetch(service: "hotels") {
4      {
5        hotels {
6          id
7          address
8          __typename
9        }
10      }
11    },
12    Flatten(path: "hotels.@") {
13      Fetch(service: "reviews") {
14        {
15          ... on Hotel {
16            __typename
17            id
18          }
19        } =>
20        {
21          ... on Hotel {
22            reviews {
23              rating
24            }
25          }
26        }
27      },
28    },
29  },
30}

As shown, this query plan defines a Sequence that executes a Fetch on the Hotels subgraph before executing one on the Reviews subgraph. (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 gateway to merge the data returned by its Fetch node with data that was previously Fetched during the current Sequence:

GraphQL
1Flatten(path: "hotels.@") {
2  Fetch(service: "reviews") {
3    ...
4  }
5}

The Flatten node's path argument tells the gateway 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 field.

Expanded example

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

GraphQL
1query GetHotels {
2  hotels { # Resolved by Hotels subgraph
3    id
4    address
5    reviews { # Resolved by Reviews subgraph
6      rating
7    }
8  }
9}

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

GraphQL
1{
2  hotels {
3    id
4    address
5    __typename # The gateway requests this to resolve references (see below)
6  }
7}

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

GraphQL
1{
2  reviews {
3    rating
4  }
5}

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

GraphQL
1Flatten(path: "hotels.@") {
2  ...
3}

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

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

GraphQL
1{
2  hotels {
3    id
4    address
5    reviews {
6      rating
7    }
8  }
9}

Resolving references with Flatten

Like Sequence nodes, Flatten nodes appear whenever one subgraph's response depends on data that first must be returned by another subgraph. This almost always involves resolving entity fields 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 fields. When this is the case, the Fetch node uses a special syntax:

GraphQL
1Flatten(path: "hotels.@") {
2  Fetch(service: "reviews") {
3    {
4      ... on Hotel {
5        __typename
6        id
7      }
8    } =>
9    {
10      ... on Hotel {
11        reviews {
12          rating
13        }
14      }
15    }
16  },
17}

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

  • The first fragment is a representation of the entity being resolved (in this case, Hotel). Learn more about entity representations.

  • The second fragment contains the entity fields and subfields that the gateway needs the subgraph to resolve (in this case, Hotel.reviews and Review.rating).

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

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

Viewing query plans

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

Outputting query plans from the gateway

Your gateway can output the query plan for each incoming operation 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:

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

    JavaScript
    1const gateway = new ApolloGateway({
    2  experimental_didResolveQueryPlan: function(options) {
    3    if (options.requestContext.operationName !== 'IntrospectionQuery') {
    4      console.log(serializeQueryPlan(options.queryPlan));
    5    }
    6  }
    7});

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

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

Feedback

Edit on GitHub

Forums