itzikiusa

db-mongodb

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.

itzikiusa 0 Updated 2w ago
GitHub

Install

npx skillscat add itzikiusa/otto-os/db-mongodb

Install via the SkillsCat registry.

SKILL.md

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

  1. Read SCHEMA.md first. MongoDB is schema-less, so SCHEMA.md is 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 small find(...).limit(3) sample before
    relying on a field.
  2. 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.
  3. 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 variety

find 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 $sort on 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):
    { $lookup: { from: "customers", localField: "customerId",
                 foreignField: "_id", as: "customer" } }
    Usually follow with $unwind: "$customer". The joined collection should be
    indexed on foreignField, and keep it small — $lookup runs 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: false to find missing fields.
  • _id is 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 be int, long, double, or
    Decimal128 — beware mixed numeric types in comparisons.
  • Match inside arrays of sub-documents with $elemMatch so 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
    compare totalDocsExamined to nReturned; a big gap means a poor index.
  • A query/sort only uses an index if its fields prefix a compound index in
    order. $regex with a leading wildcard, $ne, $nin, and $where typically
    can't use indexes.
  • countDocuments is accurate (runs a filter); estimatedDocumentCount is
    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").

Categories