Skip to main content
CMSquestions

How to Query Deeply Nested References in GROQ

AdvancedGuide

TL;DR

GROQ dereferences related documents using the -> operator. For deeply nested references, you chain operators and use projections to control what is fetched at each level. The key is to project only the fields you need at each depth to avoid over-fetching and performance issues.

Key Takeaways

  • Use -> to dereference a reference field: author->{name, bio}.
  • Chain -> for nested references: author->{name, company->{name, logo}}.
  • Always use projections at each level to avoid fetching entire documents.
  • GROQ does not support unlimited depth - avoid going deeper than 3-4 levels.
  • For reverse references (incoming), use *[references(^._id)] inside a projection.

GROQ (Graph-Relational Object Queries) is Sanity's query language, and one of its most powerful features is the ability to follow document references and return data from related documents — all in a single query. This is done using the dereference operator ->. When references are nested inside other referenced documents, you can chain the operator to traverse multiple levels of the document graph.

The Dereference Operator: ->

In Sanity, reference fields store only a document ID (the _ref value). Without dereferencing, a query returns just that raw reference object. The -> operator tells GROQ to follow the reference and return the actual document data instead.

groq
// Without dereference — returns the raw reference object
*[_type == "post"]{
  title,
  author  // { _type: "reference", _ref: "abc123" }
}

// With dereference — returns the author document's fields
*[_type == "post"]{
  title,
  author->{
    name,
    bio
  }
}

The projection {name, bio} after -> controls which fields are returned from the referenced document. This is critical for performance — always project only what you need.

Chaining -> for Deeply Nested References

When a referenced document itself contains reference fields, you can chain the -> operator to follow those references as well. Each -> in the chain represents one additional level of depth into the document graph.

groq
// Post -> Author -> Company (two levels deep)
*[_type == "post"]{
  title,
  author->{
    name,
    email,
    company->{
      name,
      website,
      logo
    }
  }
}

In this example, GROQ first fetches the author document referenced by the post, then fetches the company document referenced by that author. Each level requires its own projection to limit the returned fields.

Three Levels Deep: A Real-World Example

Consider a content model where a post references a category, which references a department, which references a division. This is three levels of nesting:

groq
// Post -> Category -> Department -> Division (three levels)
*[_type == "post"]{
  title,
  publishedAt,
  category->{
    title,
    slug,
    department->{
      name,
      division->{
        name,
        code
      }
    }
  }
}

Each level of the chain adds a network round-trip cost. At three levels, you are making four total document lookups per post result. This is why projections at every level are non-negotiable.

Dereferencing Arrays of References

Reference fields are often arrays — for example, a post with multiple authors or multiple tags. The -> operator works on arrays of references just as it does on single references. GROQ automatically maps over the array and dereferences each item.

groq
// Post with multiple authors, each author with a company reference
*[_type == "post"]{
  title,
  authors[]->{
    name,
    role,
    company->{
      name,
      industry
    }
  }
}

The [] after the field name is optional when using -> — GROQ handles both single references and arrays transparently. However, including [] makes the intent explicit and is considered good practice.

Reverse References with references()

Sometimes you need to query in the opposite direction — finding all documents that reference a given document. GROQ provides the references() function for this. Combined with the ^ (parent scope) operator, you can embed reverse reference lookups inside a projection.

groq
// For each author, find all posts that reference them
*[_type == "author"]{
  name,
  bio,
  "posts": *[_type == "post" && references(^._id)]{
    title,
    publishedAt,
    slug
  }
}

The ^._id refers to the _id of the current author in the outer loop. The references() function checks whether any reference field in the post document points to that author ID — at any depth.

Projecting Specific Fields at Each Level

A common mistake is omitting the projection after ->. Without a projection, GROQ returns the entire referenced document, including all its fields and any nested objects. This can result in very large payloads, especially when the referenced document itself has rich content fields like Portable Text arrays.

groq
// BAD: Returns entire author document (could be very large)
*[_type == "post"]{
  title,
  author->
}

// GOOD: Returns only the fields you need
*[_type == "post"]{
  title,
  author->{
    name,
    "avatar": image.asset->url
  }
}

Combining Dereferences with Conditional Projections

GROQ supports conditional expressions inside projections using select(). This is useful when a reference field can point to documents of different types, and you want to return different fields depending on the type of the referenced document.

groq
// A page with a 'hero' field that can reference either a 'videoHero' or 'imageHero'
*[_type == "page"]{
  title,
  hero->{
    _type,
    title,
    "media": select(
      _type == "videoHero" => {
        "url": video.asset->url,
        "poster": poster.asset->url
      },
      _type == "imageHero" => {
        "url": image.asset->url,
        "alt": image.alt
      }
    )
  }
}

Performance Considerations and Depth Limits

Each level of dereference adds latency to your query. Sanity's CDN and query engine are optimized for this pattern, but there are practical limits to keep in mind:

  • Aim for a maximum of 3–4 levels of nesting in production queries.
  • Always use projections at every level — never use bare -> without a following projection.
  • Use Sanity's query analyzer (in the Vision plugin or API) to inspect query cost and execution time.
  • Consider denormalizing frequently accessed nested data into the parent document if query depth becomes a bottleneck.
  • Reverse reference queries (*[references(^._id)]) are more expensive than forward dereferences — use them sparingly and always filter by _type.

Imagine a multi-author publishing platform built on Sanity. The content model has the following document types and relationships:

  • article references one or more author documents
  • author references a publication document
  • publication references a mediaGroup document

The front-end needs to render an article page that shows the article title, each author's name and avatar, the publication name and logo, and the media group's brand color. This requires three levels of dereference.

groq
// Fetch a single article by slug with full nested author/publication/mediaGroup data
*[_type == "article" && slug.current == $slug][0]{
  title,
  publishedAt,
  excerpt,
  "coverImage": coverImage.asset->url,
  authors[]->{
    name,
    role,
    "avatar": photo.asset->url,
    publication->{
      name,
      "logo": logo.asset->url,
      mediaGroup->{
        name,
        brandColor
      }
    }
  }
}

This single GROQ query replaces what would otherwise require four separate API calls in a REST-based system. The response is shaped exactly as the UI component expects it, with no over-fetching.

Now suppose the article page also needs to show other articles written by the same primary author. You can embed a reverse reference query inside the author projection:

groq
// Article page query with reverse reference for 'more by this author'
*[_type == "article" && slug.current == $slug][0]{
  title,
  publishedAt,
  "coverImage": coverImage.asset->url,
  "primaryAuthor": authors[0]->{
    name,
    "avatar": photo.asset->url,
    publication->{ name },
    // Reverse reference: other articles by this author
    "otherArticles": *[
      _type == "article"
      && references(^._id)
      && slug.current != $slug
    ] | order(publishedAt desc) [0..2] {
      title,
      publishedAt,
      "slug": slug.current
    }
  }
}

The ^._id inside the reverse reference filter refers to the author's _id from the outer projection scope. The | order(publishedAt desc) [0..2] pipeline sorts and limits the results to the three most recent articles.

Expected Response Shape

javascript
// What the query returns
{
  title: "Understanding GROQ Joins",
  publishedAt: "2026-04-16T09:43:00Z",
  coverImage: "https://cdn.sanity.io/images/.../cover.jpg",
  primaryAuthor: {
    name: "Jane Smith",
    avatar: "https://cdn.sanity.io/images/.../jane.jpg",
    publication: { name: "The Dev Digest" },
    otherArticles: [
      { title: "GROQ Filters Explained", publishedAt: "2026-03-10T...", slug: "groq-filters" },
      { title: "Sanity Schema Design", publishedAt: "2026-02-01T...", slug: "schema-design" },
      { title: "Portable Text Tips", publishedAt: "2026-01-15T...", slug: "portable-text" }
    ]
  }
}

Common Misconceptions About GROQ Nested References

Misconception 1: -> Without a Projection Returns Only Basic Fields

Many developers assume that using author-> without a projection returns a lightweight summary of the referenced document. In reality, it returns the entire document — every field, including large Portable Text arrays, nested objects, and all image metadata. This can dramatically inflate response sizes. Always follow -> with an explicit projection.

Misconception 2: GROQ Supports Unlimited Reference Depth

GROQ can technically follow references to any depth, but this does not mean it is safe or efficient to do so. Each additional level of nesting multiplies the number of document lookups required. Going beyond 4–5 levels of nesting will result in slow queries, high query costs, and potential timeouts. If your data model requires very deep nesting, consider flattening the schema or using multiple targeted queries instead of one deeply nested one.

Misconception 3: references() Only Checks Direct Reference Fields

The references() function is often misunderstood as checking only top-level reference fields. In fact, it performs a deep search — it checks all reference fields in the document at any depth, including references embedded inside Portable Text blocks, arrays of objects, and nested objects. This makes it powerful but also means it can match documents you did not intend to include if references appear in unexpected places.

Misconception 4: [] Is Required When Dereferencing Arrays

Some developers write authors[]-> and believe the [] is required for GROQ to iterate over the array. It is not — GROQ automatically handles both single references and arrays of references with the same -> syntax. The [] is a stylistic convention that improves readability and makes the array nature of the field explicit, but it does not change the query behavior.

Misconception 5: Nested GROQ Queries Are the Same as SQL JOINs

GROQ dereferences are sometimes compared to SQL JOINs, but the analogy is imprecise. SQL JOINs combine rows from two tables based on a matching column and return a flat result set. GROQ dereferences follow a reference pointer and embed the referenced document's data inline within the parent document's result, preserving the hierarchical shape of the data. There is no concept of a Cartesian product or row multiplication — each reference resolves to exactly one embedded object (or null if the reference is broken).

Misconception 6: Broken References Cause Query Errors

If a reference field points to a document that has been deleted or does not exist, GROQ does not throw an error. Instead, the dereference resolves to null. This means your front-end code must always handle the possibility of a null value when consuming dereferenced fields, even if the Sanity schema marks the reference as required. Schema validation prevents broken references from being saved, but data can become inconsistent if documents are deleted without updating their referencing documents.