Documents, collections and queries

The document model, dot notation into nested data, and the query and update operators you will use every day.

The document model

A MongoDB database holds collections, and a collection holds BSON documents — ordered key/value maps that may nest other documents and arrays. There is no fixed column list: two documents in one collection can have different fields, which is why the schema lives in your application code as much as in the database.

use shop;

db.orders.insertOne({
  _id: ObjectId("66e0a1f2c3b4d5e6f7a8b9c0"),
  customer: { id: 42, name: "Ada", country: "GB" },
  items: [
    { sku: "LAMP-01", qty: 2, unitPrice: NumberDecimal("19.90") },
    { sku: "CABLE-3", qty: 1, unitPrice: NumberDecimal("4.50") }
  ],
  status: "paid",
  tags: ["priority", "wholesale"],
  placedAt: ISODate("2026-09-14T09:12:00Z")
});
  • _id is mandatory and unique per collection; an ObjectId is generated client-side if you do not supply one.
  • Field order inside a document is preserved, but the order of documents in a collection is not — always sort explicitly.
  • BSON types are distinct from JavaScript types. NumberDecimal preserves exact decimals; a plain number becomes a double when it has a fraction.
  • Storing a date as a string breaks range queries and sorting, because string comparison is lexicographic.
💡
Schema flexibility is a choice, not a licence. Add a $jsonSchema validator to a collection when the shape matters — it turns a class of corrupt writes into rejected writes.

Querying

// equality on a nested field and an array element
db.orders.find({ "customer.country": "GB", tags: "priority" })

// operators
db.orders.find({
  status: { $in: ["paid", "shipped"] },
  "items.qty": { $gte: 2 },
  placedAt: { $gte: ISODate("2026-01-01") }
})

// elements of an array must match together
db.orders.find({ items: { $elemMatch: { sku: "LAMP-01", qty: { $gt: 1 } } } })

// projection plus sorting and paging
db.orders.find({ status: "paid" }, { "customer.name": 1, total: 1, _id: 0 })
         .sort({ placedAt: -1 })
         .limit(20);
OperatorMeaning
$eq $ne $gt $gte $lt $lteComparison; $gt/$lt only compare within one BSON type
$in $ninMatch any / none of a list
$existsField present or absent — distinct from being null
$all $sizeArray contains all values / has this length
$elemMatchSeveral conditions must hold on the same array element
$and $or $nor $notLogical composition
$regexPattern match; anchored patterns can use an index

Without $elemMatch, conditions on array fields can each be satisfied by different elements, so { "items.sku": "LAMP-01", "items.qty": 5 } matches a document where no single item is both.

Updating without clobbering

db.orders.updateOne(
  { _id: id },
  {
    $set: { status: "shipped", "customer.country": "US" },
    $inc: { retries: 1 },
    $unset: { holdReason: "" },
    $push: { history: { at: new Date(), by: "worker" } },
    $addToSet: { tags: "export" }
  },
  { upsert: false }
);

db.orders.updateMany({ status: "new" }, { $set: { status: "queued" } });

const before = db.orders.findOneAndUpdate(
  { _id: id, status: "queued" },
  { $set: { status: "running" } },
  { returnDocument: "before" }     // fetch-and-claim in one round trip
);
  • Update operators change named fields; a replacement document overwrites everything else it does not mention.
  • updateMany with a broad filter is the document equivalent of a missing WHERE — run it as find first.
  • findOneAndUpdate is atomic, which makes it the right primitive for claiming a job without a separate transaction.
  • $push on an unbounded array eventually hits the 16 MB document limit; use the bucket pattern for time-series style growth.

FAQ

When should I embed and when should I reference?
Embed when the child is always read with the parent and is bounded in size — order line items, an address. Reference when the child is large, unbounded, or shared between parents, so it has its own collection and its own indexes.
Does MongoDB have joins?
$lookup performs a left outer join inside the aggregation pipeline. It works, but each one costs a lookup per input document, so a design that avoids them stays faster.

The aggregation pipeline Indexes and schema design patterns

Last refreshed 2026-09-18.