Combining queries with subqueries in AQL
Subqueries let you form complex requests and allow you to process more data in with a single query
How to use subqueries
Wherever an expression is allowed in AQL, a subquery can be placed. A subquery is a query part that can introduce its own local variables without affecting variables and values in its outer scope(s).
It is required that subqueries be put inside parentheses (
and )
to
explicitly mark their start and end points:
FOR p IN persons
LET recommendations = ( // subquery start
FOR r IN recommendations
FILTER p.id == r.personId
SORT p.rank DESC
LIMIT 10
RETURN r
) // subquery end
RETURN { person : p, recommendations : recommendations }
A subquery’s result can be assigned to a variable with
LET
as shown above, so that it can be referenced
multiple times or just to improve the query readability.
Function calls also use parentheses and AQL allows you to omit an extra pair if
you want to use a subquery as sole argument for a function, e.g.
MAX(<subquery>)
instead of MAX((<subquery>))
:
FOR p IN persons
COLLECT city = p.city INTO g
RETURN {
city : city,
numPersons : LENGTH(g),
maxRating: MAX( // subquery start
FOR r IN g
RETURN r.p.rating
) // subquery end
}
The extra wrapping is required if there is more than one function argument,
however, e.g. NOT_NULL((RETURN "ok"), "fallback")
.
Subqueries may also include other subqueries.
Subquery results and unwinding
Subqueries always return a result array, even if there is only a single return value:
RETURN ( RETURN 1 )
[ [ 1 ] ]
To avoid such a nested data structure, FIRST()
can be used for example:
RETURN FIRST( RETURN 1 )
[ 1 ]
To unwind the result array of a subquery so that each element is returned as
top-level element in the overall query result, you can use a FOR
loop:
FOR elem IN (RETURN 1..3) // [1,2,3]
RETURN elem
[
1,
2,
3
]
Without unwinding, the query would be RETURN (RETURN 1..3)
and the result
a nested array [ [ 1, 2, 3 ] ]
with a single top-level element.
Evaluation of subqueries
Subqueries that are used inside expressions are pulled out of these expressions and conditionally executed beforehand. The effective behavior is short-circuiting evaluation. An example is the ternary operator.
Consider the following query:
RETURN RAND() > 0.5 ? (RETURN 1) : 0
It get transformed into something more like this, with the calculation of the subquery happening before the evaluation of the condition:
LET temp1 = RAND() > 0.5
LET temp2 = (FILTER temp1 RETURN 1)
RETURN temp1 ? temp2 : 0
The condition is evaluated separately and stored in a variable, then the
subquery is executed conditionally using a FILTER
operation. Finally, the
ternary operator reuses the result of the evaluated condition to determine which
value to return.
The short-circuiting behavior allows you to write queries like the following:
LET maybe = DOCUMENT("coll/does_not_exist")
LET dependent = maybe ? (
FOR attr IN ATTRIBUTES(maybe)
RETURN attr
) : "document not found"
RETURN dependent
The maybe
variable can be null
, which cannot be iterated over with FOR
and
calling ATTRIBUTES()
with null
as argument would raise a query warning.
However, the subquery is only executed if DOCUMENT()
found a document, so this
query works without issues.
Similarly, when you use subqueries as sub-expressions that are combined with
logical AND
or OR
, the subqueries are only executed if the outcome is yet
to be determined:
RETURN false AND (RETURN ASSERT(false, "executed"))
RETURN true OR (RETURN ASSERT(false, "executed"))
If the first operand of a logical AND
is false
, the overall result is
false
regardless of the second operand. If the first operand of a logical OR
is true
, the overall result is true
regardless of the second operand.
As the outcome is already determined, there is no need to execute the subqueries
in these cases.
Subqueries that are used inside expressions are pulled out of these expressions and executed beforehand. That means that subqueries do not participate in lazy evaluation of operands, for example in the ternary operator.
Consider the following query:
RETURN RAND() > 0.5 ? (RETURN 1) : 0
It get transformed into something more like this, with the calculation of the subquery happening before the evaluation of the condition:
LET temp1 = (RETURN 1)
LET temp2 = RAND() > 0.5 ? temp1 : 0
RETURN temp2
The subquery is executed regardless of the condition. In other words, there is
no short-circuiting that would avoid the subquery from running in the case that
the condition evaluates to false
. You may need to take this into account to
avoid query errors like
Query: AQL: collection or array expected as operand to FOR loop; you provided a value of type ’null’ (while executing)
LET maybe = DOCUMENT("coll/does_not_exist")
LET dependent = maybe ? (
FOR attr IN ATTRIBUTES(maybe)
RETURN attr
) : "document not found"
RETURN dependent
The problem is that the subquery is executed under all circumstances, despite
the check whether DOCUMENT()
found a document or not. It does not take into
account that maybe
can be null
, which cannot be iterated over with FOR
.
A possible solution is to fall back to an empty array in the subquery to
effectively prevent the loop body from being run:
LET maybe = DOCUMENT("coll/does_not_exist")
LET dependent = maybe ? (
FOR attr IN NOT_NULL(ATTRIBUTES(maybe || {}), [])
RETURN attr
) : "document not found"
RETURN dependent
The additional fallback maybe || {}
prevents a query warning
invalid argument type in call to function ‘ATTRIBUTES()’
that originates from a null
value getting passed to the ATTRIBUTES()
function that expects an object.
Similarly, when you use subqueries as sub-expressions that are combined with
logical AND
or OR
, the subqueries are always executed:
RETURN false AND (RETURN ASSERT(false, "executed"))
RETURN true OR (RETURN ASSERT(false, "executed"))
If the first operand of a logical AND
is false
, the overall result is
false
regardless of the second operand. If the first operand of a logical OR
is true
, the overall result is true
regardless of the second operand.
However, the subqueries are run nonetheless, causing both example queries to fail.
You can prevent the subqueries from executing by prepending a FILTER
operation
with the value of the logical operator’s first operand and negating it in case
of an OR
:
LET cond = false
RETURN cond AND (FILTER cond RETURN ASSERT(false, "executed"))
LET cond = true
RETURN cond OR (FILTER !cond RETURN ASSERT(false, "executed"))