Chapter 04 · Query Operators, Arrays, Nested Fields, Null/Missing Semantics, and Expressions

Query Anti-Patterns: Unbounded Regex, Huge $in Lists, Client Filtering, and Accidental Collection Scans

Review risky query shapes with execution evidence: regex breadth, membership cardinality, client filtering, selectivity, and accidental scans.

Beginner110–140 minutesQuery anti-pattern/explain capstoneMongoDB Community Server 8.3.8 · mongosh 2.10.0 · PyMongo 4.17.0Last reviewed: September 2026

Learning outcomes

A query can be logically correct and still be operationally dangerous. AtlasMart's incident review found four recurring patterns: an unbounded regex used as pseudo-search, a huge $in list generated from another system, application code that fetched everything and filtered in Python, and a new predicate shipped without an index or explain review. This lesson teaches how to detect those patterns with explain("executionStats"), repair them, and document what the evidence proves.

01

Explain why unanchored/case-insensitive regex patterns can create broad scans and why prefix regexes have different index opportunities.

02

Recognize huge $in lists as a query-shape/cost problem rather than assuming every indexed probe is cheap.

03

Replace client-side filtering with precise server-side predicates and bounded projections.

04

Use executionStats to compare nReturned, totalKeysExamined, totalDocsExamined, and the winning plan.

05

Build a production query-review checklist that treats COLLSCAN, IXSCAN, selectivity, limits, and timeouts as evidence rather than dogma.

Chapter 04 reproducible baseline

Mandatory labs use a disposable loopback-only standalone mongodb/mongodb-community-server:8.3.8-ubuntu2204-slim with dedicated AtlasMart query fixtures and explicit reset commands. Driver examples pin pymongo==4.17.0. The standalone is intentionally unauthenticated only for these short-lived local exercises; do not publish it beyond 127.0.0.1. The labs create only local secondary/multikey indexes needed to expose query plans. Read concern, read preference, replication, and sharding are not varied in this chapter because the goal is query semantics and indexability.

Generation-time execution note

Docker, mongod, mongosh, and PyMongo are not available in this generation environment. Commands were checked against current official MongoDB Server and PyMongo documentation, but product commands were not executed here. Expected-output blocks describe stable fields and relationships to verify; they are not fabricated captured transcripts.

1. Unbounded regex is a poor substitute for search

AtlasMart's support UI once used {name:/.*cam.*/i} for substring search. It works functionally on a tiny collection, but the leading wildcard gives the B-tree no literal prefix to bound, and case-insensitive regex has additional limitations. A case-sensitive prefix such as /^AtlasCam/ is fundamentally different because the engine can derive a prefix range from the index. Neither form provides relevance scoring, analyzers, stemming, fuzzy matching, or the operational model of MongoDB Search.

mongosh · compare bounded prefix and broad regex evidence
db.query_perf.drop()const docs=[];for (let i=0;i<3000;i++) { docs.push({_id:i,sku:`sku-${String(i).padStart(4,"0")}`,  name:i%10===0?`AtlasCam ${i}`:`Product ${i}`,  category:i%3===0?"camera":"accessory",priceCents:1000+(i%200)*25,active:i%7!==0});}db.query_perf.insertMany(docs); db.query_perf.createIndex({name:1});for (const [label,q] of [["prefix",{name:/^AtlasCam/}],["substring",{name:/Cam/i}]]) { const e=db.query_perf.explain("executionStats").find(q,{_id:1}).limit(20); printjson({label,nReturned:e.executionStats.nReturned,keys:e.executionStats.totalKeysExamined,  docs:e.executionStats.totalDocsExamined,winningPlan:e.queryPlanner.winningPlan});}

Do not run deliberately pathological backtracking expressions against production merely to “test” them. Use isolated fixtures, query timeouts, and representative patterns. The repair for a true search workload is often a search index or a different product path, not a cleverer regex.

2. Huge $in lists can turn one query into hundreds of probes

An indexed $in predicate can be excellent for a small bounded set of IDs. The cost changes when an API sends hundreds or thousands of values: the server must process the list, perform many index/range operations, allocate command/query structures, and return a union of candidates. MongoDB's documentation specifically warns that hundreds of parameters can hurt performance and recommends limiting the list to tens where practical.

mongosh · compare small and large membership sets
db.query_perf.createIndex({sku:1})const small=["sku-0001","sku-0042","sku-0100","sku-0200"];const large=Array.from({length:600},(_,i)=>`sku-${String(i).padStart(4,"0")}`);for (const [label,values] of [["small",small],["large",large]]) { const q={sku:{$in:values}}; const e=db.query_perf.explain("executionStats").find(q,{_id:1,sku:1}).limit(50); printjson({label,inputValues:values.length,nReturned:e.executionStats.nReturned,  keys:e.executionStats.totalKeysExamined,docs:e.executionStats.totalDocsExamined});}

A large list is not automatically wrong. It may be the correct bounded batch for a background job. The production question is whether the shape is frequent, user-controlled, latency-sensitive, and whether another ownership model, temporary staging collection, join-like pipeline, or batch API would be safer.

3. Client-side filtering wastes network and bypasses server optimization

The anti-pattern list(coll.find({})) followed by a Python list comprehension transfers every document to the application and only then rejects most of them. MongoDB cannot use a selective index to avoid that transfer because the server was asked for everything. Projection suffers the same issue: fetching whole documents and dropping fields in Python wastes bandwidth and can expose fields the endpoint never needed.

Python · wrong client filtering vs precise server query
from pymongo import MongoClientclient = MongoClient("mongodb://127.0.0.1:27036/?directConnection=true", serverSelectionTimeoutMS=3000)coll = client.atlasmart.query_perf# Wrong shape: fetch all, then filter locally.all_docs = list(coll.find({}))local = [d for d in all_docs if d.get("active") and d.get("category") == "camera" and d.get("priceCents", 0) < 2500]print("client_received", len(all_docs), "client_kept", len(local))# Safer shape: predicate + projection + deterministic order + bound on the server.flt = {"active": True, "category": "camera", "priceCents": {"$lt": 2500}}server = list(coll.find(flt, {"_id":1,"sku":1,"priceCents":1}).sort([("priceCents",1),("_id",1)]).limit(50))print("server_returned", len(server))client.close()

The output counts are the simplest evidence: the wrong version receives all 3,000 fixtures while keeping a small subset; the server-side version only transmits matching projected documents. In production, command monitoring/APM, network bytes, latency percentiles, and server query metrics provide richer evidence.

4. A collection scan is evidence to investigate, not an automatic crime

COLLSCAN means the winning plan scans collection records rather than using an index for the main access path. On a tiny collection or a query that returns most records, a collection scan can be rational. On a large latency-sensitive API where docsExamined dwarfs nReturned, it is a red flag. Conversely, an IXSCAN is not automatically efficient if it examines huge numbers of keys for a low-selectivity predicate such as broad $nin.

mongosh · selectivity matters more than stage-name superstition
db.query_perf.createIndex({category:1,priceCents:1})const good={category:"camera",priceCents:{$lt:1800}};const broad={active:{$nin:[false]}};for (const [label,q] of [["selective",good],["broad-nin",broad]]) { const e=db.query_perf.explain("executionStats").find(q,{_id:1}).limit(100); printjson({label,nReturned:e.executionStats.nReturned,keys:e.executionStats.totalKeysExamined,  docs:e.executionStats.totalDocsExamined,winningPlan:e.queryPlanner.winningPlan});}

Use maxTimeMS as a bounded-failure guardrail for appropriate operations, not as a performance fix. A query that repeatedly reaches the deadline still needs a better predicate, index, data model, workload boundary, or search architecture.

5. AtlasMart query-review capstone

shell · start disposable MongoDB on 127.0.0.1:27036
docker rm -f atlasmart-mongo-ch04-l5docker run --name atlasmart-mongo-ch04-l5 -p 127.0.0.1:27036:27017 -d mongodb/mongodb-community-server:8.3.8-ubuntu2204-slimdocker logs atlasmart-mongo-ch04-l5 --tail 25
shell · compare four risky query shapes on one deterministic fixture
mongosh "mongodb://127.0.0.1:27036/atlasmart?directConnection=true" --quiet --eval 'db.query_perf.drop(); const docs=[];for(let i=0;i<3000;i++){docs.push({_id:i,sku:`sku-${String(i).padStart(4,"0")}`,name:i%10===0?`AtlasCam ${i}`:`Product ${i}`,category:i%3===0?"camera":"accessory",priceCents:1000+(i%200)*25,active:i%7!==0});}db.query_perf.insertMany(docs);db.query_perf.createIndex({name:1}); db.query_perf.createIndex({sku:1}); db.query_perf.createIndex({category:1,priceCents:1});const tests=[ ["prefix-regex",{name:/^AtlasCam/}], ["substring-regex",{name:/Cam/i}], ["selective-range",{category:"camera",priceCents:{$lt:1800}}], ["broad-nin",{active:{$nin:[false]}}]];for(const [label,q] of tests){const e=db.query_perf.explain("executionStats").find(q,{_id:1}).limit(100); printjson({label,nReturned:e.executionStats.nReturned,keys:e.executionStats.totalKeysExamined,docs:e.executionStats.totalDocsExamined,plan:e.queryPlanner.winningPlan});}const ids=Array.from({length:600},(_,i)=>`sku-${String(i).padStart(4,"0")}`);const ie=db.query_perf.explain("executionStats").find({sku:{$in:ids}},{_id:1}).limit(50); printjson({hugeIn:ids.length,nReturned:ie.executionStats.nReturned,keys:ie.executionStats.totalKeysExamined,docs:ie.executionStats.totalDocsExamined});' 

Verification checklist

  • The lab creates a deterministic 3,000-document fixture and explicit indexes before measuring.
  • Prefix and substring regex queries are compared through execution statistics instead of assumed latency.
  • The 600-value $in query reports its input cardinality and examined-key/document counts.
  • The selective compound-index query and broad $nin query demonstrate why selectivity matters.
  • The Python example distinguishes documents received by the client from documents actually needed.
  • No pathological regex, production URI, global server setting, or external search service is required.

Check your understanding

  1. Why is /^AtlasCam/ different from /Cam/i for an ordinary name index?
  2. Why can a 600-value $in be expensive even with an index?
  3. What is the main defect in list(coll.find({})) followed by Python filtering?
  4. Is COLLSCAN always wrong?
  5. What should a query-review checklist record before approving a new hot-path filter?
Review the answers

The anchored case-sensitive pattern exposes a literal prefix that can constrain an index range; the unanchored/case-insensitive pattern cannot rely on the same bound.

It creates many membership comparisons/index probes and larger query-processing overhead; index presence does not make input cardinality free.

The server is asked for everything, so it cannot use a selective predicate to avoid scanning/transmitting rejected documents.

No. It can be reasonable for tiny collections or broad queries. Judge the workload through selectivity, nReturned, docs/keys examined, latency, and operational goals.

Exact predicate/projection/sort/limit, dataset and selectivity, relevant indexes, explain statistics, expected result set, failure/timeout behavior, and a production monitoring signal.

bash · cleanup/reset
docker rm -f atlasmart-mongo-ch04-l5

Chapter 05 now applies the same precision to state changes: update operators, array mutations, upserts, find-and-modify operations, and bulk writes.

Authoritative references

Keep knowledge open

Help the academy stay free and grow.

If these tutorials save you time, a small donation supports new lessons, technical review, diagrams, examples, and long-term maintenance.

ETHEthereum / ERC-20 only
0x716c4Ab160C4B66F31a28AE2448BfF68fc3a2ef0

Send only Ethereum or ERC-20 compatible assets to this address.