Query Plans
Learn how your router orchestrates operations across subgraphs
Learn more about query plans to help you optimize and debug advanced use cases of Apollo Federation.
Example graph
Let's say our federated supergraph includes these subgraphs:
1type Hotel @key(fields: "id") {
2 id: ID!
3 address: String!
4}
5
6type Query {
7 hotels: [Hotel!]!
8}
1type Hotel @key(fields: "id") {
2 id: ID! @external
3 reviews: [Review!]!
4}
5
6type Review {
7 id: ID!
8 rating: Int!
9 description: String!
10}
Based on these subgraphs, clients can execute the following query against our router:
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 router needs to send at least one query to each subgraph to populate all requested fields.
Take a look at the router's query plan for this query:
Click to expand
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:
1QueryPlan {
2 ...
3}
Each node defined inside the QueryPlan
node is one of the following:
Node | Description |
---|---|
Fetch | Tells the router to execute a particular operation on a particular subgraph. |
Parallel | Tells the router that the node's immediate children can be executed in parallel. |
Sequence | Tells the router that the node's immediate children must be executed serially in the order listed. |
Flatten | Tells the router to merge the data returned by this node's child Fetch node with data previously returned in the current Sequence . |
Defer | Tells the router about one or more blocks of @defer ed fields at the same level of nesting. The node contains a primary block and an array of deferred blocks. |
Skip /Include | Tells the router to split a query plan into two possible paths that can change at runtime. |
Each of these is described in further detail below.
Fetch
node
A Fetch
node tells the router to execute a particular GraphQL operation on a particular subgraph. Every query plan includes at least one Fetch
node.
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:
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:
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 router that all of the node's immediate children can be executed in parallel. This node appears in query plans whenever the router can execute completely independent operations on different subgraphs.
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:
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 router can query both subgraphs in parallel.
The query plan for the operation looks like this:
Click to expand
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 router that the node's immediate children must be executed serially in the order listed.
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:
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
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 router to merge the data returned by its Fetch
node with data that was previously Fetch
ed during the current Sequence
:
1Flatten(path: "hotels.@") {
2 Fetch(service: "reviews") {
3 ...
4 }
5}
The Flatten
node's path
argument tells the router 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:
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 router to execute this query on the Hotels subgraph:
1{
2 hotels {
3 id
4 address
5 __typename # The router 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 router to query the Reviews subgraph for a list of Hotel
objects that each have this structure:
1{
2 reviews {
3 rating
4 }
5}
Now, the router 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:
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 router completes this merge, the resulting data exactly matches the structure of the client's original query:
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:
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 router needs the subgraph to resolve (in this case,
Hotel.reviews
andReview.rating
).
When the router 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.
Defer node
A Defer
node corresponds to one or more @defer
s at the same level of nesting in the query plan.
The node contains a primary block and an array of deferred blocks. The primary block represents the part of the query that isn't deferred. Each deferred block corresponds to the one deferred part of the query.
Read more about how @defer
works in the router support for @defer article.
1QueryPlan {
2 Defer {
3 Primary {
4 Fetch(...) {}
5 }, [
6 Deferred(...) {
7 Flatten(...) {
8 Fetch(...) {}
9 }
10 }
11 ]
12 }
13}
Condition nodes
A Skip
or Include
node splits a query plan into an if-else branch. Condition nodes are used when an operation contains a @skip
or @include
directive so the query plan can select different nodes based on the provided runtime variables.
1QueryPlan {
2 Sequence {
3 Fetch(...) {}
4 Include(...) {
5 Flatten(...) {
6 Fetch(...) { }
7 }
8 }
9 }
10}
1QueryPlan {
2 Sequence {
3 Fetch(...) {}
4 Skip(...) {
5 Flatten(...) {
6 Fetch(...) { }
7 }
8 }
9 }
10}
Viewing query plans
You can view the query plan for a particular operation in any of the following ways:
In the GraphOS Studio Explorer
Note that you must publish your graph to GraphOS to view query plans in the Explorer.
As direct output from the
@apollo/gateway
library (see below)
Outputting query plans with headers
With the Apollo Router Core v0.16.0+ and @apollo/gateway
v2.5.4+, you can pass the following headers to return the query plans in the GraphQL response extensions:
Including the
Apollo-Query-Plan-Experimental
header returns the query plan in the response extensionsAdditionally 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 query planA value of
internal
returns a JSON representation of the query plan
Outputting query plans with @apollo/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:
Import the
serializeQueryPlan
function from the@apollo/query-planner
library:JavaScript1const {serializeQueryPlan} = require('@apollo/query-planner');
Add the
experimental_didResolveQueryPlan
option to the object you pass to yourApolloGateway
constructor:JavaScript1const 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 GraphOS 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.