Filter concepts by levelShowing all levels.

MongoDB · Section 1

MongoDB Fundamentals

Level
beginner
Read
24 min
Concepts
7

What a document is, how it differs from a relational row, the terms for MongoDB's nested containers, the five CRUD verbs at a conceptual level, and why "schema-flexible" is not the same as "no schema at all."

What is true here

  1. A document is a BSON record; a collection groups documents that need not share one fixed shape.
  2. server → database → collection → document is the full nesting, biggest to smallest.
  3. Nesting related data inside a document trades a join for a single document read.
  4. update changes named fields and leaves the rest; replace swaps the whole document except _id.
  5. Schema-flexible means structure is chosen — app code or a validator can still enforce it.

What you will be able to do

  • Explain what a document, collection and database are, and how they nest
  • Say what BSON adds over JSON, and name a handful of its types
  • Compare the document model to the relational row/table model on a concrete example
  • Name the five conceptual CRUD verbs and tell update apart from replace
  • Explain why MongoDB is schema-flexible without being schema-free

The document model

What a document is, the terms for MongoDB's nested containers, and how the model compares to a relational schema.

Document-oriented databases

corebeginner

MongoDB stores data as documents — JSON-like records that can nest objects and arrays — instead of rows in a table. Each document lives in a collection, and documents in the same collection do not have to share one fixed set of fields.

Think of it as

A relational table is a spreadsheet: every row has the same columns, and a value that belongs together with another table lives in a second sheet, joined by a key. A MongoDB collection is a folder of self-contained forms: each form (document) can carry everything about one thing — including the parts that would need a second sheet in a spreadsheet — nested right inside it.

json
{
  "field": "value",
  "nested": { "field": "value" },
  "list": ["value", "value"]
}

What we're doing: Show one document holding data that a relational schema would split across two tables.

book.jsonjson
{
  "_id": "65f1...",
  "title": "Dune",
  "author": { "name": "Frank Herbert", "country": "USA" },
  "tags": ["sci-fi", "classic"]
}
1
The whole value is one document — one self-contained record.
4
author is an embedded document: a relational schema would put this in a separate authors table and join on a foreign key.

Why this works: A relational design normalizes author into its own table so the name is not repeated per book, then joins the two at query time. A document design instead asks whether a book is ever read without its author — if the two are always fetched together, nesting author removes the join and the query becomes a single document read.

Assuming a document collection needs one shared schema, like a table

Wrong

text
Insisting every document in a collection define the exact same fields, in the exact same shape, before anything is written.

Better

text
Letting documents differ where the domain genuinely differs, then adding validation (see schema validation) for the fields that must stay consistent.

What you see: Treating a flexible-schema database as if it were relational forfeits its main advantage without gaining any of a relational database’s enforced-schema guarantees.

Why: MongoDB does not require every document in a collection to share one fixed set of fields — that flexibility is deliberate, not a gap to work around by hand-enforcing a rigid shape.

A document nests what a join would otherwise need

document

field-value pairs

embedded object

nested inline

array

a list, nested too

  1. document — field-value pairs
  2. embedded object — nested inline
  3. array — a list, nested too

Document terms vs. their relational rough-equivalent

Document terms vs. their relational rough-equivalent
MongoDB termRelational rough-equivalentNotable difference
databasedatabaseholds collections instead of tables
collectiontabledocuments inside it need not share one fixed schema
documentrowcan nest objects and arrays, not just scalar columns
fieldcolumnnot every document in a collection must have it
embedded document(needs a second table)related data nested inline, no join needed

Together

json
{
  "_id": "ObjectId(...)",
  "title": "Dune",
  "author": { "name": "Frank Herbert", "country": "USA" },
  "tags": ["sci-fi", "classic"]
}

Remember: A document is a JSON-like BSON record; a collection groups documents that do not have to share one fixed schema. Nesting replaces some joins.

See also: documents and collections · document vs relational

Databases, collections, documents and fields

corebeginner

A server holds databases. A database holds collections. A collection holds documents. A document holds fields, and a field's value can itself be a nested document, an array, or a scalar like a string or number.

Think of it as

Four nested containers, biggest to smallest: server → database → collection → document. Each level is a folder for the level below it, ending in the document itself, which is where the actual field-value data lives.

json
{
  "_id": ObjectId("..."),   // primary key, auto-generated if omitted
  "field": "scalar value",
  "nested": { "field": "value" },  // embedded document
  "list": [1, 2, 3]                // array
}

What we're doing: Show the four nesting levels and a field whose value is itself an embedded document.

book.jsonjson
// db "shop" > collection "books" > this document
{
  "_id": ObjectId("65f1a2b3c4d5e6f7a8b9c0d1"),
  "title": "Dune",
  "author": { "name": "Frank Herbert" },
  "tags": ["sci-fi", "classic"]
}
2
_id is generated automatically as an ObjectId — nothing supplied one here.
4
author is an embedded document, not a reference — its value is itself field-value pairs.

Why this works: The nesting from server down to document is what lets a single write or read touch everything about one thing — author does not need its own collection unless it is looked up or updated independently of the book.

Assuming _id must be an ObjectId

Wrong

text
Believing _id can only ever hold the default, auto-generated ObjectId value.

Better

text
_id can be supplied explicitly as any unique value — a string, a number, even a compound object — as long as the caller guarantees uniqueness.

What you see: Code that assumes _id.getTimestamp() always works breaks the moment a document was inserted with a caller-supplied, non-ObjectId _id.

Why: ObjectId is only the default MongoDB generates when _id is omitted — the field itself accepts any BSON value, so long as it is unique within the collection.

Four nested containers

server

one running mongod / cluster

database

a named group of collections

collection

a named group of documents

document

field-value pairs, BSON-typed

  1. server — one running mongod / cluster
  2. database — a named group of collections
  3. collection — a named group of documents
  4. document — field-value pairs, BSON-typed

Common BSON types

Common BSON types
TypeExampleNotes
string"Dune"UTF-8 text
int32 / int6442two distinct widths — see BSON and data types
double3.14the default for a JS-style decimal literal
booleantrue
dateISODate("2026-01-01")stored as milliseconds since the Unix epoch
nullnulldistinct from a missing field
objectIdObjectId("65f1...")the default type of _id
array["a", "b"]can hold mixed types
object{ "k": "v" }an embedded document

Together

json
{
  "_id": ObjectId("65f1a2b3c4d5e6f7a8b9c0d1"),
  "title": "Dune",
  "pages": 412,
  "published": ISODate("1965-08-01"),
  "inStock": true,
  "tags": ["sci-fi", "classic"]
}

Remember: server → database → collection → document, biggest to smallest. _id is the primary key, an ObjectId by default but any unique value is allowed.

See also: document oriented · document vs relational

Document model vs. relational model

standardbeginner

A relational row is flat and normalized, with related data split into other tables and joined at query time. A document can nest related data directly, trading that join for a single document read.

Think of it as

A relational table normalizes first and joins at query time; a document collection is free to denormalize first and read one document instead. Neither is strictly better — the right one depends on whether the related data is read together and how it grows (see data modeling, later in this section).

text
Relational: SELECT * FROM books JOIN authors ON books.author_id = authors.id
Document:   db.books.find({ title: "Dune" })  // author already inline

Same data, two models

Same data, two models
ConcernRelationalDocument
Related dataseparate table, joined by foreign keynested inline, or referenced by _id
Schemaenforced by table definitionflexible by default, optionally validated
Typical readJOIN across tablessingle document, no join
Typical writeupdate one row, one placemay need to update inside a nested structure

Together

text
Relational:
  books(id, title)
  authors(id, name)
  books.author_id -> authors.id   -- joined at query time

Document:
  { "title": "Dune", "author": { "name": "Frank Herbert" } }   -- nested, no join

Remember: Relational normalizes and joins at query time; documents can nest related data and trade the join for a single read.

See also: document oriented · documents and collections

Advertisement

Working with MongoDB

Naming conventions, the five conceptual CRUD verbs, the mongosh shell, and what "flexible schema" actually means.

Naming conventions and flexible schemas

standardbeginner

Database, collection and field names follow conventions MongoDB does not enforce — lowercase names, camelCase or snake_case fields. A collection is created implicitly on first insert.

Think of it as

Naming is just a convention MongoDB will not enforce for you — pick one and stay consistent, the same discipline a relational schema would otherwise force by requiring a migration to change a table name.

text
use shop           // switches to (or creates) database "shop"
db.orders.insertOne({ status: "pending" })   // creates collection "orders" if new

Naming conventions in practice

Naming conventions in practice
ThingConventionExample
Databaselowercase, shortshop
Collectionlowercase, plural nounorders
FieldcamelCase or snake_case, pick onecreatedAt or created_at
Reservednames starting with $, or _id$set is an operator, not a field name

Together

json
// collection: orders
{
  "_id": ObjectId("..."),
  "customerId": ObjectId("..."),
  "createdAt": ISODate("2026-01-01"),
  "status": "pending"
}

Remember: Lowercase database/collection names, camelCase or snake_case fields, pick one and stay consistent. A collection is created implicitly on first insert.

See also: document oriented · schema flexible not free

CRUD, conceptually

corebeginner

Every operation on a document is one of five verbs: insert (create it), find (read it), update (change some fields), replace (swap the whole document), or delete (remove it).

Think of it as

Insert, find, update, replace and delete map one-to-one onto CRUD's create/read/update/delete — except MongoDB splits "update" into two distinct verbs, because changing a few fields (update) and swapping the whole document (replace) have different costs and different failure modes.

text
db.<collection>.<verb>(<filter>, <update or nothing>)

What we're doing: Show why "update" and "replace" are two different verbs, not one.

crud-concept.txttext
// document before: { "_id": 1, "status": "pending", "total": 42 }

updateOne({ _id: 1 }, { $set: { status: "shipped" } })
// -> { "_id": 1, "status": "shipped", "total": 42 }   total survives

replaceOne({ _id: 1 }, { status: "shipped" })
// -> { "_id": 1, "status": "shipped" }                total is gone
3
update touches only the field named in $set — every other field on the document is left exactly as it was.
6
replace treats the new document as the WHOLE new document — anything not included in it is dropped, except _id.

Why this works: update and replace both "change" a document, but they answer different questions: "adjust these fields" versus "this is now the entire document." Reaching for replace when only one field should change silently deletes every other field the caller did not think to repeat.

Using replace when update was meant

Wrong

text
// intending only to bump status, but using replaceOne with a partial document
replaceOne({ _id: 1 }, { status: "shipped" })
// total, and every other field, is now gone

Better

text
updateOne({ _id: 1 }, { $set: { status: "shipped" } })
// only status changes; every other field survives

What you see: Fields that were never mentioned in the call disappear from the document after it runs — silently, no error.

Why: replace does not merge the given document with the existing one — it becomes the new document, in full. update operators like $set are what express "change only this."

The five verbs, conceptually

insert

create

find

read

update / replace

change

delete

remove

  1. insert — create
  2. find — read
  3. update / replace — change
  4. delete — remove

The five CRUD verbs

The five CRUD verbs
VerbWhat it doesMethod family (section 4)
insertcreates a new documentinsertOne(), insertMany()
findreads documents matching a filterfind(), findOne()
updatechanges specific fields, leaves the restupdateOne(), updateMany()
replaceswaps the whole document except _idreplaceOne()
deleteremoves documents matching a filterdeleteOne(), deleteMany()

Together

text
insert:  db.orders.insertOne({ status: "pending" })
find:    db.orders.find({ status: "pending" })
update:  db.orders.updateOne({ _id: id }, { $set: { status: "shipped" } })
replace: db.orders.replaceOne({ _id: id }, { status: "shipped" })
delete:  db.orders.deleteOne({ _id: id })

Remember: Five verbs: insert, find, update (changes fields), replace (swaps the whole document), delete. update and replace are not the same.

See also: document oriented · mongosh

The MongoDB shell (mongosh)

standardbeginner

mongosh is the current official MongoDB shell — a JavaScript REPL wired to a live connection, where db refers to the current database and every command is a real JavaScript method call.

Think of it as

mongosh is a JavaScript REPL wired directly to a MongoDB connection — every command is a JavaScript expression, and db is a live object pointing at whichever database you last switched to with use.

bash
mongosh "mongodb://localhost:27017"

Common mongosh commands

Common mongosh commands
CommandWhat it does
use shopswitch to (or lazily create) database "shop"
show dbslist databases on the server
show collectionslist collections in the current database
db.orders.find()read every document in the orders collection
db.orders.countDocuments()count documents in a collection
exitleave the shell

Together

text
use shop
db.orders.insertOne({ status: "pending" })
db.orders.find({ status: "pending" })
show collections

Remember: mongosh is a JavaScript REPL: use switches database, db.<collection>.<method>() runs a real method call, not a separate query language.

See also: crud overview · documents and collections

Schema-flexible, not schema-free

corebeginner

MongoDB does not require every document in a collection to match one fixed shape — that is a capability, not a rule against structure. App code and optional validators can still enforce what a collection needs.

Think of it as

Flexible does not mean absent. A collection with no validator is more like a text editor with no spellchecker than a language with no grammar — the structure a domain needs still exists, MongoDB just does not force it onto every document by default the way a relational table definition would.

text
// A validator makes a chosen shape enforced, without giving up flexibility elsewhere
db.createCollection("orders", { validator: { $jsonSchema: { required: ["status"] } } })

What we're doing: Show that an insert missing a field silently succeeds unless a validator is in place.

validator-effect.txttext
// No validator on "orders"
db.orders.insertOne({ total: 42 })   // succeeds — "status" was never required

db.createCollection("orders_v2", {
  validator: { $jsonSchema: { required: ["status"] } }
})
db.orders_v2.insertOne({ total: 42 })   // rejected — "status" is missing
2
With no validator, MongoDB accepts any shape — the missing "status" field is not an error.
7
The validator turns "status" from an unenforced convention into a database-level requirement.

Why this works: Nothing about the document model itself forces a field to exist — that has to be chosen deliberately, either in application code or with a validator. Skipping both does not mean the data has no real shape, only that nothing is checking it.

Reading "no schema enforcement by default" as "no schema exists"

Wrong

text
Building application code that reads every field as possibly present or absent, everywhere, because "MongoDB has no schema."

Better

text
Deciding which fields are actually required for a collection's invariants, then enforcing them with application checks and/or a validator — the same way a relational schema would, just chosen rather than automatic.

What you see: Every read is defensively wrapped in existence checks, even for fields the application itself always writes — a sign the schema was never actually decided, just avoided.

Why: A collection can be exactly as strict as its author needs; "flexible" describes what MongoDB does not force by default, not a ceiling on what an application is allowed to enforce.

Two ways to read "no fixed schema"

Misread: schema-free

  • +No structure exists anywhere
  • +Any shape is fine forever
  • +Nothing needs to agree

Correct: schema-flexible

  • Structure is chosen, not forced by table definitions
  • App code and/or validators still enforce it
  • Flexibility applies where the domain genuinely varies
  • Misread: schema-free
    • No structure exists anywhere
    • Any shape is fine forever
    • Nothing needs to agree
  • Correct: schema-flexible
    • Structure is chosen, not forced by table definitions
    • App code and/or validators still enforce it
    • Flexibility applies where the domain genuinely varies

Where structure can be enforced

Where structure can be enforced
LayerEnforcesBypassed by
Relational table schemaevery row, alwaysnothing — the database refuses a bad row
Application codewhatever the code checksany write that skips the application, e.g. a script
MongoDB collection validatorwhatever the validator checkswrites with a validation action set to warn instead of error
Nothingnothingeverything — the collection accepts any shape

Together

text
// No validator: this insert succeeds even though "status" is missing
db.orders.insertOne({ total: 42 })

// With a validator requiring "status": the same insert is rejected

Remember: Schema-flexible means MongoDB does not force one shape by default — app code or a validator still can, and critical data usually wants one to.

See also: naming and flexible schema · document vs relational

Advertisement