FaunaDB Query Optimization: Index vs Filter Decision Guide
Use FaunaDB Indexes to restrict candidate sets before applying Filters, avoiding full collection scans and reducing read costs.
07 Apr 2026, 23:02 UTC

The Decision: Restrict First, Then Filter
When retrieving documents in FaunaDB, choose between a predefined Index and a runtime Filter expression. A Filter without a supporting Index forces a linear scan: the engine reads every document in the collection and evaluates the predicate against each one. Read cost and latency grow in direct proportion to collection size. The practical takeaway is to use an Index to restrict the candidate set, then apply a Filter only to the small remaining subset.
Supported Options and Constraints
FaunaDB indexes are materialized views built at write time. You define the terms (attributes used for lookup) when you create the index. Index terms are immutable: changing a term requires creating a new index and updating references to it. Filters require no schema change and support arbitrary Lambda logic, but they execute after documents are retrieved.
Comparison of Retrieval Methods
| Factor | Index-Based Lookup | Filter Expression |
|---|---|---|
| Lookup complexity | O(log n) via index terms | O(n) linear scan of source set |
| When evaluated | Write time (materialized) | Read time (per query) |
| Schema flexibility | Rigid; terms fixed at creation | High; ad-hoc Lambda conditions |
| Read cost | Proportional to matched documents | Proportional to total scanned documents |
| Write impact | Index updates on create/write | None |
Trade-offs and Decision Logic
Use an Index when
- The query attribute is predictable (for example,
status,email, ororganization_id). - You need sorting or range matching on an indexed field.
- The collection is large enough that a full scan would exceed read budgets or time out.
Use a Filter when
- Criteria are rare, dynamic, or too varied to justify a dedicated index.
- You have already restricted the result set to a small number of documents using an Index.
The recommended pattern is Restrict → Filter: Match on selective index terms first, then run the Filter over this small candidate set rather than over Documents(Collection("subscriptions")).
Implementation: Active Subscriptions Expiring After a Date
Assume a collection subscriptions where each document stores data.status and data.expiry_date. You want active subscriptions expiring after January 1, 2026.
Step 1: Create the index
Run this in the Fauna Shell or through an SDK authenticated with a server or admin key.
CreateIndex({
name: "subscriptions_by_status_and_expiry",
source: Collection("subscriptions"),
terms: [
{ field: ["data", "status"] },
{ field: ["data", "expiry_date"] }
]
})
Permissions: server key or admin key. Risk: each index adds storage and increases write latency because FaunaDB must update the index on every document create or update.
Step 2: Query using the index, then filter
Let(
{
candidates: Paginate(
Match(
Index("subscriptions_by_status_and_expiry"),
"active",
Time("2026-01-01T00:00:00Z")
)
)
},
Map(
Var("candidates"),
Lambda("ref", Get(Var("ref")))
)
)
This retrieves only documents whose status and expiry fall within the indexed range. If you need additional ad-hoc logic, wrap the result in a Filter over this small candidate set rather than over Documents(Collection("subscriptions")).
Verification
- Run a Filter-only query against a collection of at least 1,000 documents and note the read count in the response.
- Run the indexed query above against the same data and compare the read count. The indexed query should read roughly the number of matching documents, not the full collection.
- Confirm term order: place the attribute that eliminates the most candidates first in the
termsarray.
Limitations
- Over-indexing increases storage cost and write latency.
- Index definitions cannot be altered; to change terms, create a new index and migrate queries.
- A Filter is unsafe as the first step on large collections because it scans every document before applying the predicate.
Rollback
Creating an index changes cluster state. To remove it, run:
Delete(Index("subscriptions_by_status_and_expiry"))
This frees storage but removes the optimized access path; update any queries that depend on it before deletion.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.