How to get unique values in a GROQ query?

16 replies
Last updated: Jun 9, 2020
Hi, is it possible to structure a groq query such that I only get unique values. E.g. *`[_type == 'route' && unique(url)]{...}`
AI Update

Yes, you can get unique/distinct values in GROQ, but not quite the way you've written it. The unique() function isn't a filter that you apply inline within the document filter expression.

GROQ provides the array::unique() function that works on arrays. Here's how to use it:

For unique values from a field across documents:

array::unique(*[_type == 'route'].url)

This collects all url values from route documents and returns only the unique ones as an array.

You can also pipe into it:

*[_type == 'route'].url | array::unique()

Both approaches give you the same result - an array of unique URL values.

Important limitation: There's no built-in way to filter documents by unique field values directly in the filter expression like *[_type == 'route' && unique(url)]. The array::unique() function works on arrays, not as a filter constraint.

If you need full documents with unique URLs:

You'll need to handle this differently depending on your use case:

  1. Get unique URLs first, then fetch documents separately based on those URLs
  2. Fetch all documents and deduplicate in your application code based on the URL field
  3. Restructure your data model if you frequently need to query by unique values

The array::unique() function was added in September 2022 as part of GROQ spec updates to help with exactly this kind of scenario - finding distinct values across your content. It removes duplicate values from an array, with the caveat that it only works on values that can be compared for equality (strings, numbers, booleans, and null).

If you share more about what you're trying to achieve - like whether you need just a list of unique URLs, or the first/last document with each unique URL - I can suggest a more specific approach!

Show original thread
16 replies
Hi there!
Hm, what are you trying to achieve exactly? AFAIK there is no built in way to do this with GROQ, but there might be some workarounds.
I think this will work, though
[_type == 'route' && count(*[url == ^.url]) == 1]{...}
(This is probably not a very effective query, and not something I would advise on a large dataset)
Atm I posed this question I was trying to find a way to create a documentList in the deskstructure that would display each first level url i.e. /something, /another, /test in a list.Then for each of these you would have a deeper of urls with that same first-level url.

Something like this:

/something
    /something/test
    /something/test2
/another
    /another/foo
    /another/baar
That seemd promising, but I get an error trying to use this exact code. Iget an error stating that the S.documentList is required to have a filter method.
i.e.

return S.documentList({...}).filter('...')
Still can't get that to work
Could you post the schema for the document type you want to to list that way?
yes, sure
import Document from '../types/Document';

import { MdLabel, MdFolder } from 'react-icons/md';


const product: Document = {
    
name: 'lyseProduct',
    
type: 'document',
    
title: 'Product',
    
icon: MdFolder,
    
fields: [
        
{
            
name: 'title',
            
type: 'string',
            
title: 'Title',
            
readOnly: true,
            
description: 'The name of the product (read only).',
            
validation: Rule => [Rule.required()],
        
},
        
{
            
name: 'category',
            
type: 'string',
            
title: 'Category',
            
readOnly: true,
            
description: 'The product category (read only).',
            
validation: Rule => [Rule.required()],
        
},
        
{
            
name: 'price',
            
type: 'number',
            
title: 'Price',
            
readOnly: true,
            
description: 'The price of the product (read only).',
            
validation: Rule => [Rule.required()],
        
},
        
{
            
name: 'description',
            
type: 'text',
            
title: 'Description',
            
description: 'Further description of the product.',
        
},
    
],
    
preview: {
        
select: {
            
title: 'title',
            
price: 'price',
            
category: 'category',
        
},
        
prepare({ title, price, category }) {
            
return {
                `title: 
${category}: ${title}
,`                
media: MdLabel,
                `subtitle: 
Price: ${price || '<unset>'}
,

            
};`        
},
    
},

};


export default product;
What are the values you want to group by? Categories?
yes, that is correct
We were thinking about creating a new document for category, but that does work that well. Since category is not defined in Sanity, but rather collected from an api.
Ok, without knowing what your category value looks like you can do something like this, where you use an async function to get all used categories and use these to filter the
documentList


// Import already configured client
import client from 'part:@sanity/base/client'

S.listItem()
  .title('Routes')
  .child(async () => {
    // Get all used categories as an array
    const categories = await client
      .fetch("*[_type == 'route'].category")

    // Filter to get only unique categories
    const uniqueCategories = categories
      .filter((val, index, self) => self.indexOf(val) === index)
      .filter(Boolean)

    return S.list()
      .title('Routes by category')
      .id('routes-by-category')
      .items(
        uniqueCategories.map(category =>
          S.listItem()
          .title(category)
          .child(
            S.documentList()
              .title(`Routes in ${category}`)
              .filter("_type == 'route' && category == $category")
              .params({category})
          )
        )
      )
})
This does exactly what I was trying to achieve! Thank you ❤️
👍

Sanity – Build the way you think, not the way your CMS thinks

Sanity is the developer-first content operating system that gives you complete control. Schema-as-code, GROQ queries, and real-time APIs mean no more workarounds or waiting for deployments. Free to start, scale as you grow.

Was this answer helpful?