Why does my search get slower as the collection grows, even with a narrow filter?
Some step of the query is still reading the whole field instead of the rows the filter kept. A filter that matches 500 rows should cost about the same in a collection of 1 million rows as in one of 10 million. When the time grows with the collection while the number of matches stays flat, the usual cause is a facet count, a sort or a keyword clause that walks an index built over every row of a field and checks each entry against the filter. The fix is to start from the filter's matches whenever they are the smaller set, and count, sort and test words over those rows only.
Which parts of a query grow with the collection?
| Step | Slow path: cost grows with the collection | Fast path: cost grows with the matches |
|---|---|---|
| Facet count (values and their counts) | Visit every value's list of rows for the field and intersect each with the filter | Read each match's value, or, for a field with few values, look each value up among the matches |
| Sorted page | Walk the sort field's index in order until enough rows pass the filter | Read the sort key of each match, sort those, return the page |
| Keyword search inside a filter | Build the set of every row containing the words, then intersect it with the filter | Test the words on each match |
| Total count on every page | Resolve the whole filter again for each page | Resolve it once and reuse it |
The slow path is the right choice when the filter is broad. If a filter keeps 60% of a collection, walking the index once is cheaper than reading millions of matches one at a time. So a query planner has to estimate how many rows the filter keeps and pick a path, and the slowdown appears when that estimate is missing or wrong.
Why does the database pick the slow path?
ORDER BY created_at LIMIT 50 a planner may walk the created_atEXPLAIN ANALYZE](https://www.postgresql.org/docs/current/using-explain.html) this shows as an
index scan on the sort column with a Filter line and a large "Rows Removed by Filter" count.
asset_id with hundreds of thousandsexecution_hint](https://www.elastic.co/docs/reference/aggregations/search-aggregations-bucket-terms-aggregation):
the default global ordinals path suits broad queries, and Elastic's documentation says map, which
buckets the matching documents' own values, should only be considered when very few documents match.
How do I find the slow step?
1. Time the query with each part removed in turn: no facet, no sort, no keyword clause. 2. Run the same filter against collections of different sizes. If the time grows with the collection while the match count stays the same, one step is walking the field. 3. Read the plan:
EXPLAIN ANALYZE in PostgreSQL, or "profile": true in Elasticsearch and
OpenSearch (search profile API).
4. Move the narrow clause to the top level of the filter and time it again. If that fixes it, the planner
could not see the clause.How do I fix it?
(tenant_id, created_at) returns one tenant's rows already in date order, so the sort reads only
matching rows.
execution_hint: map on the terms aggregation for those queries.
What did this look like in our own vector store?
MVS, the vector store behind Mixpeek, had this problem on filtered facet counts, sorted pages and keyword search inside a filter: each walked the field's index, so they slowed down as a collection grew even when the filter matched a few hundred rows. It now reads the filter's matches first, including clauses nested inside AND groups, and counts, sorts and tests words over those rows when a cost rule says that is cheaper. Filters that keep most of a collection still take the old path.
| Benchmark at 8.17 million rows (median) | Before | After |
|---|---|---|
| Filtered distinct count | 8.9 s | 22 ms |
| Sorted page of 50 | 456 ms | 13 ms |
| Keyword search scoped to 96 folders | 7.5 s | 266 ms |
Every answer matched the old path exactly. The change is in the changelog for 7 November 2026. MVS keeps vectors and payloads on object storage, S3 or GCS, and serves filtered vector search, keyword search and facets from the same index.
Related: why vector search returns fewer results when you add a filter and filtered vector search.
Frequently Asked Questions
Why is my facet count slow when the filter matches only a few rows?
The count is probably walking every value of the field and checking each against the filter, which costs the same however few rows matched. Count from the matches instead, or in Elasticsearch set
execution_hint to map for narrow queries.Why is ORDER BY with LIMIT slow on a filtered query?
The planner is likely walking the sort column's index and discarding rows that fail the filter until it has enough. With a narrow filter that means reading most of the index. An index that starts with the filter column and ends with the sort column fixes it in PostgreSQL.
How do I know whether my search will slow down as data grows?
Run the same filtered query on a copy of your data at two or three sizes. If the time rises with the collection while the number of matches stays flat, it will keep rising.
Does the faster path change the results?
It should not, and you should check. Compare rows, counts and order for a sample of real queries on both paths before you switch. In our benchmark every answer matched.