What is GROQ?
GROQ (Graph-Relational Object Queries) is Sanity's purpose-built query language for JSON data. Unlike GraphQL's schema-driven approach, GROQ gives you fine-grained control over exactly what fields return, how references expand, and how data transforms before reaching your frontend. This guide covers everything from basic syntax to advanced optimization.
Why GROQ over GraphQL? GROQ was built for content management, making common CMS operations--filtering by type, expanding references, shaping responses--far more intuitive. Its projection system eliminates client-side data transformation, and joins work naturally with the parent operator.
In this guide: Query syntax, projections, filtering, ordering, joins, and performance optimization. For a complete Sanity setup, see our guide on Sanity + Next.js Integration.
GROQ Query Syntax Fundamentals
Every GROQ query begins with the asterisk (*), representing all documents in your dataset. From there, you apply filters, transformations, and projections to narrow and shape results.
The Asterisk and Filters
*[_type == 'movie' && releaseYear >= 1979]
The asterisk followed by a filter creates a data flow: all documents flow through the filter, and only matching documents continue downstream. Filters use brackets [] and can combine conditions with && (and) and || (or).
The Data Flow Model
*[_type == 'movie' && releaseYear >= 1979] | order(releaseYear) {
_id, title, releaseYear
}[0...100]
Data flows left-to-right: * (all documents) → filter → order() → projection → slice. This pipeline metaphor helps debug queries by tracing each transformation.
Query Parameters
*[_type == 'movie' && releaseYear >= $minYear] | order(releaseYear desc) {
_id, title
}[$start...$end]
Parameters using $ prefix enable dynamic queries. Pass { minYear: 1990, start: 0, end: 20 } at runtime. Parameters prevent injection, improve caching, and allow reusable query templates. For best practices on structuring your schema to maximize query efficiency, see our Sanity Schema Design guide.
Projections: Shaping Your Response
Projections use curly braces {} to specify exactly which fields appear in the response. Without projections, GROQ returns entire documents--even unused fields.
Basic Projections
*[_type == 'movie'] {
_id, title, releaseYear
}
Returns only the specified fields for each movie, reducing bandwidth and simplifying frontend code.
Nested Fields and Renaming
*[_type == 'movie'] {
title,
"directorName": director->name
}
Access nested fields with dot notation and rename output fields using quoted strings.
Naked Projections
*[_type == 'movie'].title
// Returns: ["Alien", "Blade Runner", ...]
Place field names outside braces for arrays of primitive values instead of wrapped objects.
Spread Operator
*[_type == 'movie'] {
...,
"directorName": director->name,
"isClassic": releaseYear < 1985
}
The ... includes all original fields while adding computed ones.
Default Values with Coalesce
*[_type == 'movie'] {
title,
"rating": coalesce(rating, "Not yet rated")
}
coalesce() provides fallback values when fields are missing or null.
Filtering: Precise Document Selection
Filters constrain which documents are returned using comparison operators, array membership, and existence checks.
Type Filtering
*[_type == "movie"]
*[_type in ["movie", "tvShow"]]
Filter by _type first--this dramatically reduces the result set.
Comparison Operators
*[_type == "movie" && releaseYear >= 1990] // greater than or equal
*[_type == "movie" && releaseYear < 2000] // less than
*[_type == "movie" && title == "Alien"] // exact match
*[_type == "movie" && title != "Alien"] // not equal
DateTime Filtering
*[_type == "movie" && dateTime(publishedAt) > dateTime("2020-01-01")]
*[_type == "movie" && dateTime(_updatedAt) > dateTime(now()) - 86400*7]
Wrap date strings in dateTime() and use now() for current time.
Array Membership
*[_type == "movie" && "sci-fi" in genres]
*[_type == "movie" && genres[]._ref in *[_type == "genre"]._id]
Use in for array membership and subqueries for dynamic lookups.
Existence and Text Matching
*[_type == "movie" && defined(rating)] // has rating field
*[_type == "movie" && title match "Alien*"] // wildcard search
defined() checks field existence; match performs text search with tokenization.
Path-Based Filtering
*[_id in path("movies.*.directors.*")]
*[_id in path("drafts.**")] // nested path
Filter by document ID patterns for hierarchical data.
Ordering: Controlling Result Sequence
The order() function sorts results by one or more fields using the pipe operator (|).
Basic and Multi-Field Ordering
*[_type == "movie"] | order(releaseYear)
*[_type == "movie"] | order(releaseYear desc, title asc)
Add desc for descending, leave empty or use asc for ascending. Multi-field ordering sorts sequentially.
Case-Insensitive and Computed Ordering
*[_type == "movie"] | order(lower(title))
*[_type == "movie"] | order(rating desc, _createdAt desc)
Use functions like lower() for case-insensitive sorting. Multiple criteria create stable sort orders.
Ordering Best Practices
- Apply ordering before slicing for consistent pagination
- Prefer indexed fields for ordering
- Avoid ordering on deeply nested computed values when possible
Joins: Working with References
References create relationships between documents. The arrow operator (->) expands references, and the parent operator (^) enables true joins.
Expanding Single References
*[_type == "movie"] {
title,
director->,
"directorName": director->name
}
director-> replaces the reference with the full director document; director->name projects only the name.
Expanding Arrays of References
*[_type == "movie"] {
title,
producers[]->
}
Use square brackets before the arrow to traverse and expand arrays of references.
The Parent Operator for Joins
*[_type == "person"] {
name,
"movies": *[_type == "movie" && references(^._id)].title
}
^._id references the outer person's _id, enabling queries that find all movies referencing that person.
The References Function
*[_type == "movie" && references("ridley-scott")]
*[_type == "movie" && references(*[_type == "person" && name == "Ridley Scott"]._id)]
references() finds documents that link to a given ID, supporting both direct IDs and subquery lookups.
Optimization: Writing Efficient Queries
GROQ performance depends on query structure. These patterns minimize response size and processing time.
Always Limit Results
*[_type == "movie"][0...20] // first 20 movies
*[_type == "movie"][0] // single movie (returns object)
Without limits, queries return all matching documents. Range slices ensure consistent pagination.
Project Only What You Need
// Good: only needed fields
*[_type == "movie"]{title, poster{asset->{url}}}
// Bad: full documents
*[_type == "movie"]
The most impactful optimization--every excluded field reduces response size.
Filter Early and Specifically
*[_type == "movie" && rating > 8]{title} // type filter first
Apply _type filters first to reduce datasets early in the pipeline.
Use Vision Plugin for Development
Sanity Studio's Vision plugin enables real-time query testing, performance profiling, and response size analysis. Use it to validate and optimize queries before deployment.
Query Caching
The Sanity API CDN caches query responses. Consistent queries benefit from caching; randomized ordering or time-based filters may have lower cache hit rates.
Common Query Patterns
Paginated Listing
*[_type == "movie"] | order(_createdAt desc) [$offset...$offset + $limit] {
_id, title, releaseYear, poster{asset->{url}}
}
Supports infinite scroll with $offset and $limit parameters.
Single Document with Relations
*[_type == "movie" && slug.current == $slug][0] {
title,
releaseYear,
"director": director->{name, photo},
"actors": cast[].person->{name, photo},
"similarMovies": *[_type == "movie" && _id != ^._id &&
references(^.director._ref)] | order(popularity desc)[0...4]{title}
}
Fetches movie with all related data in one request.
Search with Boosted Results
*[_type == "movie" && (title match $query || synopsis match $query)] |
order(boost(title match $query, 5), boost(synopsis match $query, 1), popularity desc)
[0...10]
boost() weights certain matches higher in results.
Count and Aggregate
*[_type == "movie" && count(awards[]) > 0] {
title,
"awardCount": count(awards)
}
count() works on arrays to find documents with related content and compute aggregates.
Conclusion
GROQ's expressive power lies in its readable, pipeline-based syntax for describing precise data requirements. Master projections, filters, ordering, joins, and optimization patterns to write queries that fetch exactly what your application needs.
Start with basic queries, add complexity incrementally, and use the Vision plugin to validate each step. The investment in learning GROQ pays dividends in reduced boilerplate, smaller response sizes, and simpler frontend code.