Module: MongoDB
MongoDB·136·4 MIN READ

136: MongoDB CRUD and Query Language — Filters, Operators, Arrays, Projection, Cursors, Bulk Writes, and Safe Updates

TOPICS COVERED: MongoDB CRUD and Query Language — Filters, Operators, Arrays, Projection, Cursors, Bulk Writes, and Safe Updates

Learning objectives

You will learn to:

  • insert one/many documents;
  • query with comparison, logical, element, array, and regex operators;
  • understand missing versus null;
  • use projection;
  • sort/limit/skip;
  • work with cursors;
  • count documents;
  • update with atomic operators;
  • use array update operators;
  • replace documents;
  • delete safely;
  • use upsert and bulkWrite;
  • understand query injection risks;
  • build safe pagination/query filters.

Insert

javascript
db.tasks.insertOne({
  tenantId: ObjectId("..."),
  title: "Learn CRUD",
  completed: false,
  priority: "normal",
  tags: ["mongodb"],
  createdAt: new Date()
})

Many:

javascript
db.tasks.insertMany([
  {...},
  {...}
])

Decide ordered/unordered bulk semantics when failures may occur.

Find one

javascript
db.tasks.findOne({
  _id: ObjectId("...")
})

Find cursor

javascript
db.tasks.find({
  completed: false
})

find returns cursor in shell/driver.

Do not assume database sends entire collection instantly.

Equality

javascript
{
  priority: "high"
}

Comparison operators

javascript
{
  createdAt: {
    $gte: ISODate("2026-08-01"),
    $lt: ISODate("2026-09-01")
  }
}

Common:

text
$eq
$ne
$gt
$gte
$lt
$lte
$in
$nin

$ne/$nin can match broad sets and often be less index-selective.

Do not assume every operator query is efficient.

Logical

javascript
{
  $or: [
    { priority: "high" },
    { overdue: true }
  ]
}

Common:

text
$and
$or
$nor
$not

Mongo implicitly ANDs top-level fields:

javascript
{
  tenantId: ObjectId(...),
  completed: false
}

Element

javascript
{
  dueDate: {
    $exists: true,
    $type: "date"
  }
}

Use $exists to distinguish absent field.

Null versus missing

Query:

javascript
{ field: null }

has special behavior and can match null/missing depending query form.

If you need explicit existence/type:

javascript
{
  field: null,
  other...
}

Use documented patterns such as $type/$exists.

Test null semantics; do not assume SQL NULL behavior.

Dot notation

javascript
{
  "profile.city": "Madurai"
}

Nested fields query without loading entire document.

Arrays

Contains element:

javascript
{
  tags: "mongodb"
}

Matches array containing value.

All:

javascript
{
  tags: {
    $all: [
      "node",
      "mongodb"
    ]
  }
}

Size:

javascript
{
  tags: {
    $size: 2
  }
}

$elemMatch

For array of documents:

javascript
{
  items: {
    $elemMatch: {
      sku: "A",
      quantity: {
        $gte: 2
      }
    }
  }
}

This requires predicates to match same array element.

Without $elemMatch, separate elements may satisfy separate dot predicates.

Understand carefully.

Regex

javascript
{
  title: {
    $regex: "^Mongo",
    $options: "i"
  }
}

Regex can be expensive and index-unfriendly depending pattern.

User-provided regex is dangerous:

  • regex DoS;
  • broad scan;
  • unexpected metacharacters.

Do not pass raw search string as regex without escaping/limits.

For full text/search use appropriate text/Atlas Search design.

Projection

Include:

javascript
db.tasks.find(
  { completed: false },
  {
    title: 1,
    priority: 1,
    createdAt: 1
  }
)

_id included by default unless excluded.

Exclusion:

javascript
{
  largeField: 0
}

Generally do not mix inclusion/exclusion except _id rules.

Projection reduces data transfer but does not automatically guarantee covered query.

Sort

javascript
.sort({
  createdAt: -1,
  _id: -1
})

Add deterministic tiebreaker for pagination.

Sort without supporting index can consume memory/CPU.

Limit

javascript
.limit(25)

Always limit public list APIs.

Skip

javascript
.skip(1000)

Useful for small offset pagination.

Deep skip requires walking past many results and can degrade.

Cursor/range pagination preferred for large changing lists.

Count

javascript
db.tasks.countDocuments({
  completed: false
})

Exact count can be expensive on huge filters.

estimatedDocumentCount() uses metadata for collection estimate and different semantics.

Choose based on need.

Distinct

javascript
db.tasks.distinct("priority", {
  tenantId: ObjectId(...)
})

Do not use distinct as replacement for proper aggregation if you need counts/sorts.

Cursor iteration

Node driver later:

js
const cursor = collection.find(filter);

for await (const doc of cursor) {
  ...
}

This streams batches rather than converting all to array.

Avoid:

js
await cursor.toArray()

for millions of documents.

Cursor batch size

Drivers fetch batches.

Batch size tuning can affect memory/network.

Defaults usually fine.

Update one

javascript
db.tasks.updateOne(
  {
    _id: ObjectId("..."),
    tenantId: ObjectId("...")
  },
  {
    $set: {
      completed: true,
      updatedAt: new Date()
    },
    $inc: {
      version: 1
    }
  }
)

Atomic on one document.

Update operators

Common:

text
$set
$unset
$inc
$mul
$min
$max
$currentDate
$rename

Array:

text
$push
$addToSet
$pull
$pop

Use update operators instead of read-modify-write where possible.

Lost update

Bad:

text
read document
modify in app
replace

Two clients can overwrite.

Use atomic operator:

javascript
$inc

or version predicate:

javascript
{
  _id,
  version: expectedVersion
}

Update:

javascript
{
  $set: patch,
  $inc: { version: 1 }
}

If matched count 0, conflict.

$push

javascript
{
  $push: {
    tags: "node"
  }
}

Duplicates allowed.

$addToSet avoids duplicate exact values.

Do not use either on unbounded arrays without model bound.

Push modifiers

Mongo supports modifiers such as:

text
$each
$slice
$sort
$position

Can maintain bounded recent-items array.

Example:

javascript
{
  $push: {
    recentEvents: {
      $each: [newEvent],
      $position: 0,
      $slice: 20
    }
  }
}

Useful subset pattern.

Array filters

Update selected array elements:

javascript
db.orders.updateOne(
  { _id: orderId },
  {
    $set: {
      "items.$[item].status": "ready"
    }
  },
  {
    arrayFilters: [
      {
        "item.sku": "A"
      }
    ]
  }
)

Validate identifiers/conditions.

Complex array updates can signal overly large embedded model.

Replace

javascript
replaceOne(filter, replacement)

Replaces document content except immutable _id.

Easy to accidentally drop fields.

Use PATCH-style update operators for partial update.

Delete

javascript
db.tasks.deleteOne({
  _id,
  tenantId
})

Scope tenant/authorization.

Delete many:

javascript
deleteMany({
  archived: true,
  archivedAt: {
    $lt: cutoff
  }
})

Before destructive bulk:

  1. run find same filter;
  2. count;
  3. inspect sample;
  4. backup/transaction/change process;
  5. execute.

Upsert

javascript
updateOne(
  { externalId },
  {
    $set: {...},
    $setOnInsert: {
      createdAt: new Date()
    }
  },
  {
    upsert: true
  }
)

Use unique index to enforce uniqueness under concurrency.

Filter alone without unique index can race.

findOneAndUpdate

Returns document according to options.

Useful for atomic claim/update workflows.

Example job claiming needs robust predicate/index and return-after semantics.

Bulk write

javascript
db.tasks.bulkWrite([
  {
    updateOne: {
      filter: { _id: id1 },
      update: { $set: { completed: true } }
    }
  },
  {
    deleteOne: {
      filter: { _id: id2 }
    }
  }
])

Useful for many operations in fewer round trips.

Ordered default stops after first error; unordered can continue independent operations.

Understand partial success.

Retryable writes

MongoDB supports retryable writes for selected operations/configurations.

Driver may retry transient failures safely when operation has retryable semantics.

Do not build manual retries around non-idempotent operations without understanding driver/server behavior.

Consistency lesson goes deeper.

Query injection

Danger:

js
const filter = req.body.filter;

collection.find(filter);

Attacker can supply:

json
{
  "$where": "...",
  "$ne": ...
}

depending server/API capabilities.

Build filter from validated values:

js
const filter = {
  tenantId: auth.tenantId,
};

if (input.status) {
  filter.status = input.status;
}

Do not allow arbitrary Mongo operators from public client unless endpoint intentionally exposes a safe query DSL.

Cursor pagination

Sort:

javascript
{
  createdAt: -1,
  _id: -1
}

Next page filter after cursor (createdAt, _id):

javascript
{
  tenantId,
  $or: [
    {
      createdAt: {
        $lt: cursor.createdAt
      }
    },
    {
      createdAt: cursor.createdAt,
      _id: {
        $lt: cursor.id
      }
    }
  ]
}

Requires supporting compound index.

Do not use _id timestamp alone when actual business order is another field.

Common mistakes

  • raw client filter;
  • regex from user;
  • deep skip at scale;
  • unbounded toArray;
  • replace loses fields;
  • upsert without unique index;
  • update by _id without tenant scope;
  • read-modify-write lost update;
  • unbounded push;
  • bulk delete without preview;
  • inconsistent field types;
  • $ne scan surprises.

Exercises

  1. Insert/find/update/delete tasks.
  2. Query nested and array fields.
  3. Demonstrate $elemMatch.
  4. Project small shape.
  5. Build cursor iteration.
  6. Compare skip and range pagination.
  7. Implement version update conflict.
  8. Maintain recent-events bounded array.
  9. Build upsert with unique index plan.
  10. Sanitize a public filter builder.
  11. Use bulkWrite and inspect partial errors.

Mastery checklist

Explain:

  • CRUD;
  • operators;
  • null/missing;
  • arrays/elemMatch;
  • projection;
  • cursor;
  • skip/limit;
  • atomic updates;
  • upsert;
  • bulkWrite;
  • retryable writes concept;
  • query injection;
  • cursor pagination.

Official references