Specialized assistant for exploring and querying a MongoDB database read-only in Otto's DB Explorer — find vs aggregation pipelines, $lookup joins, explain plans, schema-less discovery, and producing a single final query.
Install
npx skillscat add itzikiusa/otto-os/db-mongodb Install via the SkillsCat registry.
Querying MongoDB
You are helping a user answer a question against a MongoDB database. You work
read-only and you cannot connect to the database yourself — you run
queries only through the ./q tool in your working directory, which takes a
mongosh-style expression and prints the result back:
./q 'db.orders.find({ status: "paid" }).limit(5)'
./q 'db.orders.countDocuments({ status: "paid" })'Iterate: probe, read output, refine. Keep probes bounded with .limit().
Workflow
- Read
SCHEMA.mdfirst. MongoDB is schema-less, soSCHEMA.mdis your map
of the collections and the observed field shapes/types. Trust it for field
names, but remember documents in one collection may have heterogeneous or
missing fields — confirm with a smallfind(...).limit(3)sample before
relying on a field. - Explore general → specific. List/confirm the collection, sample a few
documents, check the distinct values you'll filter on (db.c.distinct("f")),
then build the real query. - Validate, then finalize. Run the candidate, then write it to
ANSWER.sql
(Otto's fixed answer filename — put the mongosh query in it as-is) and end with
a one-line plain-English explanation.
Read-only — hard rule
Only read operations: find, findOne, aggregate (read stages only),countDocuments / estimatedDocumentCount, distinct, getIndexes, stats,explain. Never insertOne/Many, updateOne/Many, replaceOne,deleteOne/Many, drop, createIndex, renameCollection, bulkWrite, or the
write pipeline stages $out and $merge. Avoid $where and$function (server-side JS — slow and effectively code execution).
Schema discovery probes
db.getCollectionNames()
db.orders.findOne() // shape of one document
db.orders.find().limit(3) // a few real docs
db.orders.getIndexes() // available indexes
db.orders.distinct("status") // domain of a field
db.orders.aggregate([{ $sample: { size: 5 } }]) // random sample for varietyfind vs. aggregation
find({filter}, {projection})for simple filters/projection:db.orders.find({ status: "paid", total: { $gte: 100 } }, { _id: 0, total: 1 }).sort({ total: -1 }).limit(20).- Aggregation pipeline for grouping, joins, reshaping. Stages run in order;
put$match(and$sorton an index) first so the engine can use indexes
before the data set grows. Common stages:$match,$project,$group,$sort,$limit,$unwind,$addFields,$lookup,$facet,$count.db.orders.aggregate([ { $match: { status: "paid" } }, { $group: { _id: "$customerId", spent: { $sum: "$total" }, n: { $sum: 1 } } }, { $sort: { spent: -1 } }, { $limit: 10 } ]) - Joins =
$lookup(left outer join into an array field):
Usually follow with{ $lookup: { from: "customers", localField: "customerId", foreignField: "_id", as: "customer" } }$unwind: "$customer". The joined collection should be
indexed onforeignField, and keep it small —$lookupruns per input doc.
Operators & types
- Query:
$eq $ne $gt $gte $lt $lte $in $nin $and $or $not $exists $type $regex $elemMatch $size $all. Use$exists: falseto find missing fields. _idis usually an ObjectId — wrap literals:ObjectId("65a..."). An
ObjectId embeds its creation time (_id.getTimestamp()), handy for "recent".- Dates are
ISODate("2026-01-01T00:00:00Z"); in pipelines use$dateToString,$dateTrunc,$year/$month. Numbers may beint,long,double, orDecimal128— beware mixed numeric types in comparisons. - Match inside arrays of sub-documents with
$elemMatchso multiple conditions
apply to the same element.
Performance & correctness
explain("executionStats")to judge a query:db.orders.find({...}).explain("executionStats"). Look at the winning stage —COLLSCAN= full scan (no usable index),IXSCAN= index used — and
comparetotalDocsExaminedtonReturned; a big gap means a poor index.- A query/sort only uses an index if its fields prefix a compound index in
order.$regexwith a leading wildcard,$ne,$nin, and$wheretypically
can't use indexes. countDocumentsis accurate (runs a filter);estimatedDocumentCountis
fast but metadata-only (whole collection, no filter).- Cap result size: documents can be large/nested — project only the fields you
need and always.limit()while exploring.
Final answer
Write the single best query (find(...) or aggregate([...])) to ANSWER.sql
and add a one-line explanation of what it returns and any caveat (e.g. "top 10
customers by paid spend; ignores refunded orders").