Keeping a query fast
Which predicates the planner can push into SQL, which it cannot, and how to shape a query so the expensive half never runs.
The performance model is simple to hold in your head: whatever the planner can push into SQL costs a Postgres index lookup, and whatever it cannot costs a row streamed into JavaScript.
Pushed down today
_type == "x"and_type in [...]_id == "x"and_id in [...]defined(field)- plain field equality, including
slug.current == $slug— which has its own expression index, being the most common frontend lookup there is - the draft or published discrimination the perspective implies
order(...)and a trailing[a...b]slice, when lifting them cannot change the result
Not pushed down
- Anything inside a projection.
- Comparisons involving functions the planner does not model.
- Filters that depend on a dereferenced document's fields.
The last one is the one that bites: *[_type == "post" && author->name == $name] streams every post into the evaluator. Invert it instead — find the author, then find posts referencing that id.
Four habits
- Always filter by
_typefirst. It is the cheapest narrowing there is. - Project only what you render. Bodies are large, and a listing that fetches every field pays for every field.
- Slice at the end of the query, not in the client. A trailing slice can be pushed down; slicing an array you already fetched cannot.
- Split a page's reads. Two cached reads with precise tags beat one giant read whose tag set is the whole dataset.
Measuring
The response envelope carries ms. For anything unclear, run the query in the playground and compare shapes: the difference between a narrowed and an unnarrowed query is usually an order of magnitude, and it is visible immediately.
