# Query optimization
A GraphQL query may request multiple fields that are using the same resolver. It's not a problem if the resolver is a simple value, but it can be less than optimal when the resolver runs an effect (such as reading from a database).
We might want to:
- cache identical queries (deduplication)
- batch queries to the same source
This is possible in Caliban using the ZQuery
(opens new window) data type.
Additionally, one may want to perform optimizations based on the fields selected by the client. -This optimization can be achieved by field metadata from Caliban that can be referenced in your query classes.
# Introducing ZQuery
A ZQuery[R, E, A]
is a purely functional description of an effectual query that may contain requests to one or more data sources. Similarly to ZIO[R, E, A]
, it requires an environment R
, may fail with an E
or succeed with an A
. All requests that do not need to be performed sequentially will automatically be batched, allowing for aggressive data source specific optimizations. Requests will also automatically be deduplicated and cached.
This - allows for writing queries in a high level, compositional style, with confidence that they will automatically be - optimized. For example, consider the following query from a user services.
val getAllUserIds: ZQuery[Any, Nothing, List[Int]] = ???
+This optimization can be achieved by field metadata from Caliban that can be referenced in your query classes. # Introducing ZQuery
A ZQuery[R, E, A]
is a purely functional description of an effectual query that may contain requests to one or more data sources. Similarly to ZIO[R, E, A]
, it requires an environment R
, may fail with an E
or succeed with an A
. All requests that do not need to be performed sequentially will automatically be batched, allowing for aggressive data source specific optimizations. Requests will also automatically be deduplicated and cached.
This allows for writing queries in a high level, compositional style, with confidence that they will automatically be optimized. For example, consider the following query from a user service.
val getAllUserIds: ZQuery[Any, Nothing, List[Int]] = ???
def getUserNameById(id: Int): ZQuery[Any, Nothing, String] = ???
for {
@@ -73,7 +71,7 @@
ZQuery.fromRequest(GetUserName(id))(UserDataSource)
To run a ZQuery
, simply use ZQuery#run
which will return a ZIO[R, E, A]
.
# ZQuery constructors and operators
There are several ways to create a ZQuery
. We've seen ZQuery.fromRequest
, but you can also:
- create from a pure value with
ZQuery.succeed
- create from an effect value with
ZQuery.fromZIO
- create from multiple queries with
ZQuery.collectAllPar
and ZQuery.foreachPar
and their sequential equivalents ZQuery.collectAll
and ZQuery.foreach
If you have a ZQuery
object, you can use:
map
and mapError
to modify the returned result or error flatMap
or zip
to combine it with other ZQuery
objects provide
and provideSome
to eliminate some of the R
requirements
There are several ways to run a ZQuery
:
runCache
runs the query using a given pre-populated cache. This can be useful for deterministically "replaying" a query without executing any new requests. runLog
runs the query and returns its result along with the cache containing a complete log of all requests executed and their results. This can be useful for logging or analysis of query execution. run
runs the query and returns its result.
# Using ZQuery with Caliban
To use ZQuery
with Caliban, you can simply include fields of type ZQuery
in your API definition.
case class Queries(
users: ZQuery[Any, Nothing, List[User]],
- user: models.UserArgs => ZQuery[Any, Nothing, User])
+ user: UserArgs => ZQuery[Any, Nothing, User])
During the query execution, Caliban will merge all the requested fields that return a ZQuery
into a single ZQuery
and run it, so that all the possible optimizations are applied.
The examples (opens new window) project provides 2 versions of the problem described in this article about GraphQL query optimization (opens new window):
- a naive (opens new window) version where fields are just returning
IO
, resulting in 47 requests - an optimized (opens new window) version where fields are returning
ZQuery
, resulting in 8 requests only
TIP
ZQuery
has a lot of operators that are similar to ZIO
, such as .optional
, etc.
Note that just like ZIO
, a field returning a ZQuery
will be executed only when it is requested by the client.
TIP
When all your effects are wrapped with ZQuery.fromRequest
, it is recommended to use queryExecution = QueryExecution.Batched
instead of the default QueryExecution.Parallel
.
Doing so will provide better performance as it will avoid forking unnecessary fibers.
diff --git a/vuepress/docs/docs/optimization.md b/vuepress/docs/docs/optimization.md
index e404af8bd..73dae015f 100644
--- a/vuepress/docs/docs/optimization.md
+++ b/vuepress/docs/docs/optimization.md
@@ -116,7 +116,7 @@ To use `ZQuery` with Caliban, you can simply include fields of type `ZQuery` in
```scala
case class Queries(
users: ZQuery[Any, Nothing, List[User]],
- user: models.UserArgs => ZQuery[Any, Nothing, User])
+ user: UserArgs => ZQuery[Any, Nothing, User])
```
During the query execution, Caliban will merge all the requested fields that return a `ZQuery` into a single `ZQuery` and run it, so that all the possible optimizations are applied.