CouchDB Mango Indexes: Making _find Fast Without Writing a MapReduce View
Mango JSON selectors are convenient, but unindexed queries scan every document. Declare matching indexes, verify plans with _explain, and use MapReduce views when needed.
24 Aug 2025, 01:30 UTC

You add a status field to your CouchDB documents, write a Mango selector to fetch pending orders, and everything looks fine until the dataset grows. The query that took milliseconds now takes seconds. The selector syntax was never the problem. The problem is that CouchDB could not match your selector to an index, so it scanned every document in the database. Mango is a declarative query layer, but it is not a query optimizer that builds indexes on the fly. You have to declare the index, and then you have to prove the query planner is using it.
Treat Mango as a contract between your query shape and your index definition. When both sides match, _find is fast. When they do not, you get a full scan.
Selectors declare shape, not logic
Mango selectors are JSON documents. A selector like {\"status\": \"pending\", \"priority\": {\"$gt\": 5}} tells CouchDB which fields matter and which operators apply. Supported operators include $eq, $gt, $in, $and, $or, and $regex. What you cannot do is embed arbitrary JavaScript or perform cross-document joins. The query planner compares the fields in your selector against the fields in your indexes. If the leading fields of an index cover the selector's equality conditions and range constraints, the planner can use it. Otherwise, it falls back to scanning.
Match the index to the selector
Create an index with POST /{db}/_index. A composite index on the fields you filter and sort by is usually the right starting point. For the pending-orders query above, declare both fields:
POST /orders/_index
{
"index": {
"fields": ["status", "priority"]
},
"name": "by-status-priority",
"type": "json"
}
Run this as a database admin or a user with write access to the database. The request returns a success status once the index is built; large databases may take time to backfill existing documents. If your queries always exclude archived documents, a partial index can shrink the index and reduce write amplification:
POST /orders/_index
{
"index": {
"fields": ["status", "priority"],
"partial_filter_selector": {
"archived": false
}
},
"name": "active-orders",
"type": "json"
}
The catch is strict: your query selector must imply the partial filter. If you query without including archived: false, the planner will ignore this index. Partial indexes are an optimization for well-understood query patterns, not a general safety net.
Verify with _explain, then know the limits
An index that exists is not an index that gets used. Before you trust _find in production, run POST /{db}/_explain with the same selector and sort options. The response identifies the index chosen for the query. If the plan points to the special _all_docs index, Mango could not find a better match and is performing a full scan. That is your signal to revisit the index fields, their order, or the selector itself.
POST /orders/_explain
{
"selector": {
"status": "pending",
"priority": {"$gt": 5}
}
}
You can also list existing indexes with GET /{db}/_index to confirm the definition matches your intent. After creating an index, compare _find response times on a representative dataset. If latency does not improve, the index is either unused or redundant.
Mango indexes are heuristic. Adding more indexes does not guarantee better plans; it increases write amplification and disk usage because every document write must update each index. Redundant indexes are a tax you pay on every insert. Index types and planner heuristics have also evolved across CouchDB 2.x and 3.x, so confirm the exact _explain response shape and index options against your deployment version.
More importantly, Mango is not an aggregation engine. If you need reduce-style rollups, cross-document joins, or custom scoring, a MapReduce view is still the correct tool.
| Need | Better tool |
|---|---|
| Straightforward lookup by field | Mango with a matching index |
| Range query on a sorted field | Mango with a composite index |
| Cross-document aggregation or rollups | MapReduce view |
| Custom scoring or complex joins | MapReduce view or external index |
Use Mango for straightforward lookups and range queries. Use views when the computation itself is the hard part.
An actionable checklist
- Declare an index whose leading fields match your selector's equality and range conditions.
- Run
POST /{db}/_explainto confirm the planner selected that index instead of falling back to_all_docs. - If an index turns out to be redundant, remove it with
DELETE /{db}/_index/{design-doc}/json/{index-name}, using the design document ID returned when the index was created.
Mango rewards precision. Give it a selector that matches an index, and it will give you predictable performance. Give it a vague selector, and it will scan your entire database to find an answer.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.