GROQ support matrix
What is supported (all of it, by evaluation), what is pushed into SQL (a growing subset), and where behaviour differs.
The honest summary: evaluation is complete, narrowing is partial, and only narrowing affects speed.
Language support
| Construct | Status |
|---|---|
| Filters, projections, nested projections | Supported |
| Dereferencing with arrow syntax, including in projections | Supported, batched per request |
Ordering, slices, count(), defined(), coalesce() | Supported |
| Conditionals and the spread operator in projections | Supported |
| Functions: string, math, array, date | Supported by the evaluator |
text::match | Supported; ranking is Postgres' rather than Sanity's |
Parent scope (^), including inside a nested subquery | Supported |
A second *[...] nested inside a query | Supported |
score() and ordering by score | Partial |
geo:: functions | Not supported |
Pushed into SQL today
_typeequality and membership_idequality and membershipdefined(field)- plain field equality, and
slug.currentequality (which has its own index) - the perspective's draft or published discrimination
order(...)and a trailing slice, when lifting them cannot change the result
Everything else is evaluated after the rows are fetched. A query that cannot be narrowed is slower; it is never wrong.
A note on nested queries
A query may contain more than one *[...], and the parent scope works inside them, so the natural parent-with-children shape is one query:
*[_type == "docCategory"] | order(order asc){
title,
"articles": *[_type == "docArticle" && category._ref == ^._id] | order(order asc)
}That is the query the documentation sidebar on this site is built from.
It is worth knowing what the planner does with it, because it is the one place where narrowing is visible in the shape of a query rather than only in its speed. Every *[...] in a query is evaluated against the same candidate set, so the predicate pushed into Postgres is the union of every filter's — here type in ('docCategory', 'docArticle') — and no row cap is pushed down, since a cap chosen for the outer query would withhold rows the inner one needs. The practical consequence: a nested query whose filter the planner cannot narrow widens the whole candidate set, so keep a _type on the inner filter.
Behavioural differences worth knowing
- Sync tags widen for dereferencing queries. The documents reached through an arrow are not known before the query runs, so the read is tagged with the dataset rather than with those documents.
resultSourceMapis partial. Emitted, but not complete for every projection shape.- Text ranking differs. The result set matches; the order for a broad search can differ.
