---
title: "Payload CMS — Queries Overview"
aliases: [payload-queries, payload-where, payload-filtering]
tags: [payloadcms, queries, filtering, local-api, rest-api, graphql]
sources:
  - raw/queries__overview.md
  - https://payloadcms.com/docs/queries/overview
  - https://payloadcms.com/docs/queries/depth
  - https://payloadcms.com/docs/queries/pagination
  - https://payloadcms.com/docs/queries/select
  - https://payloadcms.com/docs/queries/sort
created: 2026-05-15
updated: 2026-05-15
---

# PayloadCMS — Queries

## Key Takeaways

- **One language, three APIs** — same `Where` object works in [[wiki/payloadcms/local-api|Local API]], [[wiki/payloadcms/rest-api|REST API]], and [[wiki/payloadcms/graphql-overview|GraphQL]]; only the wrapper syntax differs
- **15 operators** cover equality, comparison, text search, list membership, existence, and geospatial queries
- **`and`/`or` nesting** can be arbitrarily deep for complex filter logic
- **Dot notation** (`'artists.featured'`) accesses nested and relationship fields without joins
- **REST queries use `qs-esm`** — complex `Where` objects stringify cleanly; avoid hand-crafting query strings
- **`all` operator is MongoDB-only** — do not use with Postgres/SQLite adapters
- **Performance stack:** `index: true` + `depth: 0` + `limit: 1` + `pagination: false` + `select` combined is the fastest pattern for unique-field lookups
- **`depth` is ignored in GraphQL** — shape your GraphQL query fields instead
- Add `index: true` to any field queried or sorted frequently — biggest single perf win

## Overview

Payload's querying language is uniform across all three APIs (Local, REST, GraphQL). You write the same logic; only the syntax wrapper differs.

- Query operators work on any indexed or non-indexed field
- `and`/`or` can be nested arbitrarily deep
- Dot notation accesses nested/related fields
- Add `index: true` to frequently queried fields for significant speed gains

## Operators

| Operator | Use case |
|----------|----------|
| `equals` | Exact match |
| `not_equals` | Exclude exact value |
| `greater_than` / `greater_than_equal` | Numeric or date |
| `less_than` / `less_than_equal` | Numeric or date |
| `like` | Case-insensitive substring; all words must be present |
| `contains` | Case-insensitive substring (single value) |
| `in` | Value in comma-delimited list |
| `not_in` | Value NOT in comma-delimited list |
| `all` | All values in list present (MongoDB only) |
| `exists` | `true` = field present, `false` = field absent |
| `near` | Point field: `lng,lat,maxMeters,minMeters` |
| `within` | Point field: inside GeoJSON area |
| `intersects` | Point field: intersects GeoJSON area |

## Where Syntax

### Local API

```ts
import type { Where } from 'payload'

const posts = await payload.find({
  collection: 'posts',
  where: {
    color: { equals: 'blue' },
  },
})
```

### REST API

```
GET /api/posts?where[color][equals]=blue
```

Use `qs-esm` for complex queries:

```ts
import { stringify } from 'qs-esm'

const query = stringify(
  { where: { color: { equals: 'mint' } } },
  { addQueryPrefix: true },
)
const res = await fetch(`/api/posts${query}`)
```

### GraphQL

```graphql
query {
  Posts(where: { color: { equals: mint } }) {
    docs { color }
    totalDocs
  }
}
```

## And / Or Logic

```ts
const query: Where = {
  or: [
    { color: { equals: 'mint' } },
    {
      and: [
        { color: { equals: 'white' } },
        { featured: { equals: false } },
      ],
    },
  ],
}
```

## Nested / Relational Fields (dot notation)

```ts
const query: Where = {
  'artists.featured': { exists: true },
}
```

## Depth

Controls how many levels of relationships are auto-populated. Default: `2`. Has **no effect in GraphQL** (use query shape instead).

| depth | Behavior |
|-------|----------|
| `0` | Return only IDs for related docs |
| `1` | Populate one level |
| `2` (default) | Populate two levels |

```ts
// Local API
await payload.find({ collection: 'posts', depth: 1 })

// REST
fetch('/api/posts?depth=1')
```

### Global default depth

```ts
export default buildConfig({
  defaultDepth: 1,
})
```

### Per-field max depth (overrides request depth)

```ts
{
  name: 'author',
  type: 'relationship',
  relationTo: 'users',
  maxDepth: 2,
}
```

## Pagination

`find` queries are paginated by default.

| Control | Default | Description |
|---------|---------|-------------|
| `limit` | `10` | Docs per page; `0` disables pagination |
| `page` | `1` | Page number |
| `pagination` | `true` | `false` = return all docs, skip count |

```ts
// Local API
await payload.find({ collection: 'posts', limit: 10, page: 2 })

// REST
fetch('/api/posts?limit=10&page=2')
```

### Response shape

```json
{
  "docs": [...],
  "totalDocs": 6,
  "limit": 10,
  "totalPages": 1,
  "page": 1,
  "pagingCounter": 1,
  "hasPrevPage": false,
  "hasNextPage": false,
  "prevPage": null,
  "nextPage": null
}
```

### Disable pagination (performance)

```ts
await payload.find({
  collection: 'posts',
  where: { slug: { equals: 'my-post' } },
  pagination: false,
  limit: 1,   // combine with limit for best perf
})
```

## Sort

- Ascending: field name (e.g. `title`)
- Descending: prefix with `-` (e.g. `-createdAt`)
- Multiple fields: array (Local) or comma-separated (REST/GraphQL)
- Only stored (non-virtual) fields can be sorted

```ts
// Local API — single field
await payload.find({ collection: 'posts', sort: '-createdAt' })

// Local API — multiple fields
await payload.find({ collection: 'posts', sort: ['priority', '-createdAt'] })

// REST — single
fetch('/api/posts?sort=-createdAt')

// REST — multiple
fetch('/api/posts?sort=priority,-createdAt')

// GraphQL
// Posts(sort: "priority,-createdAt")
```

## Select (Field Projection)

Returns only specified fields. Reduces response size and skips field hooks for unselected fields. `id` is always returned.

### Include mode

```ts
await payload.find({
  collection: 'posts',
  select: {
    text: true,
    group: { number: true },  // nested field
    array: true,              // entire array
  },
})
```

### Exclude mode

```ts
await payload.find({
  collection: 'posts',
  select: {
    array: false,
    group: { number: false },
  },
})
```

### REST

```
GET /api/posts?select[color]=true&select[group][number]=true
```

Or with `qs-esm`:

```ts
const query = stringify({ select: { text: true, group: { number: true } } }, { addQueryPrefix: true })
fetch(`/api/posts${query}`)
```

### Empty select → returns only `id`

```ts
const post = await payload.findByID({ collection: 'posts', id: '1', select: {} })
// { id: '1' }
```

## Populate (override defaultPopulate)

`defaultPopulate` on a collection sets which fields are returned when that collection is populated as a relationship. Override per-request with `populate`:

```ts
// Collection config
export const Pages: CollectionConfig<'pages'> = {
  slug: 'pages',
  defaultPopulate: { slug: true },  // only return slug when populated
  fields: [{ name: 'slug', type: 'text' }],
}

// Override at query time (Local API)
await payload.find({
  collection: 'posts',
  populate: {
    pages: { text: true },  // override pages' defaultPopulate
  },
})

// REST
fetch('/api/posts?populate[pages][text]=true')
```

**Upload collections with `defaultPopulate`:** always include `filename: true` alongside `url: true`, otherwise `url` returns `null`.

## Entity-level Select Config

Force fields to always be selected regardless of caller's `select`:

```ts
export const Posts: CollectionConfig = {
  slug: 'posts',
  select: ({ select }) => (select ? { ...select, title: true } : undefined),
  fields: [],
}
```

Return `undefined` to leave caller's select unchanged. This runs before the read; no per-document data available.

## Performance Checklist

- `index: true` on frequently queried/sorted fields
- `depth: 0` when you only need IDs
- `limit: 1` + `pagination: false` when querying by unique field (e.g. slug)
- `select` to return only needed fields
- `defaultPopulate` to limit relationship payload size

## Gotchas

- `all` operator only works with MongoDB adapter
- `depth` is ignored by GraphQL — control population via query shape
- `beforeRead`/`afterRead` hooks may not receive full doc when `select` is active
- Sorting on virtual fields is not supported unless linked to a relationship field
- `limit: 0` has the same effect as `pagination: false`

## Related

- [[wiki/payloadcms/local-api|Local API]] — direct DB access via `payload.find()`
- [[wiki/payloadcms/rest-api|REST API]] — HTTP endpoints with query string syntax
- [[wiki/payloadcms/graphql-overview|GraphQL API]] — `where` argument in queries/mutations
- [[wiki/payloadcms/queries-depth|Queries — Depth]] — relationship population control
- [[wiki/payloadcms/database-indexes|Database Indexes]] — `index: true` field config
- [[wiki/payloadcms/fields-point|Point Field]] — `near`/`within`/`intersects` operators
- [[wiki/payloadcms/performance-overview|Performance Optimization]] — full perf checklist
