What Is MongoDB? Documents, Collections, and How It Differs from SQL

What MongoDB is: documents, BSON, collections and databases, how they map to SQL tables and rows, and when MongoDB is the right choice.

Executive Summary: MongoDB is a document database that stores records as BSON documents grouped into collections, not rows in tables. Here I explain what that means in practice, how SQL concepts like tables, joins and transactions map across, where that mapping breaks, and the questions I ask before choosing MongoDB for a new system.

So what is MongoDB, beyond the “NoSQL database” label it usually gets? I have run it in production next to PostgreSQL and MySQL for years, and the short answer I give engineers is this: MongoDB is a general purpose document database. It stores each record as a self-contained document, it lets you shape that document around how your application reads it, and it gives you indexes, transactions and replication much like a relational database does. Here I write about documents, BSON, collections, how SQL concepts translate, and the places where that translation stops working.

This is the first post in my MongoDB series. Later posts go deeper into CRUD, schema design, indexing and operations, so I keep each topic brief here and focus on the mental model.

What MongoDB Actually Is

MongoDB is a database server (mongod) that stores documents and answers queries over them. You can run it in three shapes: a standalone server for development, a replica set of several servers holding the same data for high availability, or a sharded cluster that spreads data across several replica sets. Most production deployments I have worked on are replica sets, and MongoDB Atlas, the hosted service, gives you a replica set by default.

Under the hood the default storage engine is WiredTiger. It gives you document level concurrency control, so two writes to different documents in the same collection do not block each other, and it compresses data and indexes on disk.

Two facts matter more than anything else when you design for it:

  • A write to a single document is always atomic. If you update five fields and push to an array inside one document, readers see all of it or none of it.
  • Multi-document transactions exist, but they are the exception. They arrived for replica sets in 4.0 and for sharded clusters in 4.2. They work well, yet the official transactions guide itself says that a good document model will reduce the need for them in most cases.

“Schemaless” is the other label that sticks to MongoDB, and it is misleading. The server does not force every document in a collection to share a shape, but it can enforce one through JSON Schema validation. I treat that as a feature to use, not a reason to skip schema design.

Documents and BSON

A document is a set of field and value pairs. It looks like JSON when you print it, but MongoDB stores it as BSON, a binary format with a richer type system. Here is a customer document I will reuse across the series:

use shop

db.customers.insertOne({
  _id: ObjectId("5dd0c2a4e4b0a1b2c3d4e5f6"),
  name: "Asha Rao",
  email: "asha@example.com",
  createdAt: ISODate("2019-11-12T09:30:00Z"),
  loyaltyPoints: NumberInt(120),
  addresses: [
    { label: "home", line1: "12 Lake Road", city: "Pune", postcode: "411001" },
    { label: "work", line1: "4th Floor, Tech Park", city: "Pune", postcode: "411014" }
  ],
  preferences: { newsletter: true, language: "en" }
})

A few things in that document are worth pointing out. addresses is an array of subdocuments, and preferences is a nested document. In a relational design those would be separate tables. Here they live with the customer because the application reads them together.

The types also matter. JSON has strings, numbers, booleans, arrays and objects. BSON adds the ones a database needs:

  • ObjectId, a 12 byte identifier that drivers generate by default for _id. Its leading bytes are a timestamp, so ObjectIds sort roughly by creation time.
  • Date, stored as milliseconds since the Unix epoch. Store dates as dates, never as strings, or range queries and sorting will hurt you later.
  • Decimal128 for exact decimal values such as money. A double cannot represent 0.1 exactly, and that error shows up in totals.
  • Int32, Int64 and Double, which are distinct types. The mongo shell treats a bare number as a double, which is why I wrapped loyaltyPoints in NumberInt.

The BSON types reference lists the full set.

Why Every Document Needs an _id

Every document needs an _id field. It acts as the primary key, MongoDB creates a unique index on it automatically for every collection, and if you do not supply one the driver generates an ObjectId. You can use your own value, such as an order number, as long as it is unique.

There is also a hard limit: a single document cannot exceed 16 mebibytes, as listed on the limits and thresholds page. Normal fields rarely hit it. You hit it with arrays that grow forever, such as appending every page view or every log line to one document. That pattern also slows every update long before the limit arrives.

Collections and Databases

Documents live in collections, and collections live in databases. A collection is roughly what a table is in SQL, minus the enforced column list.

Both are created implicitly the first time you write to them:

use shop
db.orders.insertOne({ status: "pending", createdAt: new Date() })
show collections
customers
orders

Implicit creation is also why bulk loading feels so easy: point insertMany at an array of documents and the collection appears with them. I walked through that in my post on loading documents from a file with insertMany, which is still the quickest way I know to get sample data into a fresh database.

That convenience has a cost. If someone types db.ordres.insertOne(...) in a script, MongoDB happily creates an ordres collection and the bug can sit there for weeks. For collections that matter, I create them explicitly with a validator so that the database itself rejects bad data:

db.orders.drop()

db.createCollection("orders", {
  validator: {
    $jsonSchema: {
      bsonType: "object",
      required: ["customerId", "status", "total", "createdAt"],
      properties: {
        customerId: { bsonType: "objectId" },
        status: { enum: ["pending", "paid", "shipped", "cancelled"] },
        total: { bsonType: "decimal" },
        createdAt: { bsonType: "date" }
      }
    }
  },
  validationLevel: "strict",
  validationAction: "error"
})

Now an insert with a string total or a misspelled status fails with a Document failed validation error. The error does not say which rule broke, so I keep validators small and readable enough that the cause is obvious from the document. The schema validation documentation covers the options, including validationAction: "warn", which logs violations without rejecting them. I use “warn” when adding validation to an existing collection, then switch to “error” once the logs are clean.

Mapping SQL Concepts to MongoDB

Most people arrive at MongoDB from SQL, so a mapping table is the fastest way in. The notes column is the part I care about, because every mapping is approximate.

SQL MongoDB Where the mapping is imperfect
Database Database Close match.
Table Collection No enforced columns unless you add a validator.
Row Document A document can hold nested objects and arrays, so one document often replaces several joined rows.
Column Field Fields can differ between documents in the same collection.
Primary key _id Always present, always indexed, any type except an array.
Index Index Similar B-tree model, plus multikey indexes over arrays.
JOIN Embedding or $lookup $lookup exists but is not the default tool. You usually avoid the join by modelling.
Foreign key Reference by _id No referential integrity. The database will not stop you deleting a parent.
GROUP BY $group in an aggregation pipeline More verbose, and very capable once learned.
Transaction Single document write, or a multi-document transaction Single document atomicity covers most needs if the model is right.

The official SQL mapping chart has query by query equivalents if you want more.

Where the SQL Comparison Breaks Down

The table above makes MongoDB look like SQL with different words. That is the trap. The real difference is that you design around access patterns, not around normalised entities.

Take an order. In SQL I would have an orders table and an order_items table, and every order page would join them. In MongoDB I store the line items inside the order, because nobody ever reads an order without its items:

db.orders.insertOne({
  _id: ObjectId("5dd7a1d4e4b0a1b2c3d4e600"),
  customerId: ObjectId("5dd0c2a4e4b0a1b2c3d4e5f6"),
  status: "paid",
  createdAt: ISODate("2019-11-22T11:15:00Z"),
  items: [
    { sku: "TV-55-OLED", name: "55 inch OLED TV", qty: 1, price: NumberDecimal("89999.00") },
    { sku: "HDMI-2M", name: "HDMI cable 2m", qty: 2, price: NumberDecimal("499.00") }
  ],
  total: NumberDecimal("90997.00")
})

The order page is now one read of one document by its _id, and placing the order is one atomic insert. No transaction needed.

The cost shows up on the other side. A question like “revenue per SKU across all orders last quarter” now needs an aggregation pipeline that unwinds the items arrays. That is perfectly doable and I cover it in the aggregation pipeline tutorial, but it is more work than a GROUP BY over a flat table. Every modelling choice in MongoDB trades one access pattern against another, and you want to make that trade on purpose.

Queries Look Different

In SQL you write a query as a string. In MongoDB a query is itself a document. Here is the same lookup both ways:

SELECT name, email FROM customers
WHERE loyalty_points >= 100
ORDER BY name
LIMIT 10;
db.customers.find(
  { loyaltyPoints: { $gte: 100 } },
  { name: 1, email: 1, _id: 0 }
).sort({ name: 1 }).limit(10)
{ "name" : "Asha Rao", "email" : "asha@example.com" }

The first argument is the filter, the second is the projection that picks fields, and sort and limit chain on the cursor. Dot notation reaches into nested documents and arrays:

db.customers.find({ "addresses.city": "Pune" }, { name: 1 })

That query matches any customer with at least one address in Pune. Querying inside arrays without unwinding anything is one of the things I genuinely enjoy about the document model, and the CRUD tutorial goes through the operators in detail.

Consistency and Durability in Brief

Two settings control how safe your writes and reads are on a replica set.

Write concern says how many members must acknowledge a write before the driver reports success. In MongoDB 4.2 the default is w: 1, which means only the primary has to acknowledge the write, as listed on the default settings page. A w: 1 write can be rolled back if that primary fails before the secondaries copy it. A w: "majority" write has reached most of the voting members, so it survives a failover. The default favours speed, and you have to ask for safety.

Read concern says what data a read is allowed to see. The default, “local”, returns the newest data on the node you read from, even if it could later roll back. “majority” returns only data that a majority has acknowledged.

My rule is simple: anything involving money, orders or user accounts gets majority writes, and I set that explicitly in the connection string with w=majority so nobody has to remember it per query. The replica sets post covers this properly.

A Worked Example: A Product Catalogue

The catalogue is where I think MongoDB earns its place most clearly. A shop sells televisions, sofas and books. A TV has a screen size, a panel type and a refresh rate. A sofa has seats, a fabric and dimensions. A book has an ISBN and an author.

In a relational database this usually ends in one of two bad designs. Either a wide table with hundreds of mostly empty columns, or an entity-attribute-value (EAV) table where every attribute is a row of text. EAV makes every product page a pivot query and every numeric filter a cast from text.

In MongoDB each product carries its own typed specs:

db.products.insertMany([
  {
    sku: "TV-55-OLED", name: "55 inch OLED TV", category: "tv",
    price: NumberDecimal("89999.00"),
    specs: { screenInches: 55, panel: "OLED", refreshHz: 120, hdmiPorts: 4 }
  },
  {
    sku: "SOFA-3S-GRY", name: "Three seater sofa, grey", category: "sofa",
    price: NumberDecimal("34999.00"),
    specs: { seats: 3, fabric: "linen blend", widthCm: 210, depthCm: 92 }
  }
])

db.products.find({ category: "tv", "specs.screenInches": { $gte: 50 } }, { name: 1, _id: 0 })
{ "name" : "55 inch OLED TV" }

The filter is a plain numeric comparison against a real number. You can index the fields that get filtered most, and a wildcard index on specs covers the long tail, which I explain in the indexing tutorial.

What I would not do is move the order ledger and payments into the same decision just because the catalogue went well. Financial records are relational by nature: strict integrity, many cross-entity reports, auditors who expect SQL. The design I recommend for this kind of shop is a hybrid. The catalogue and product content live in MongoDB, while orders and payments stay in a relational database, or move to MongoDB only after the team has modelled them with transactions in mind.

Is MongoDB a Good Fit? My Decision Framework

Before choosing MongoDB for a new system, I ask these questions and write the answers down:

  1. Are the main entities read as a whole? If a screen needs an order with its items, a user with their settings, or a product with its specs, documents fit well.
  2. Does the shape vary between records? Catalogues, content, event payloads and integration data with varying fields all suit documents.
  3. Do I know the access patterns? MongoDB rewards known queries. If the main workload is ad hoc analysis by people writing new questions every day, a relational warehouse is usually better.
  4. How many writes need to span entities atomically? A few are fine with transactions. If nearly every operation touches five entities at once, the data is telling you it is relational.
  5. Will write volume or data size outgrow one machine? Sharding is a real strength, but it is a reason to prefer MongoDB only when the other answers already point that way.

When I Would Not Use MongoDB

I steer teams away from MongoDB when the data is highly relational and join heavy, when the team is fluent in SQL and has no pain with it, when the main workload is reporting and BI, or when strict referential integrity across many tables is a legal or business requirement. I also push back hard when the only argument is “it scales”. PostgreSQL on a decent server handles far more than most products ever need, and my SQL vs NoSQL comparison goes through those trade-offs.

Common Mistakes I See With MongoDB

The most common mistake I have seen is porting a relational schema table for table, then calling $lookup on every request to stitch it back together. You get the costs of both worlds and the benefits of neither.

The others are smaller but add up:

  • Storing dates as strings, which breaks range queries and sorting.
  • Storing money as doubles instead of Decimal128.
  • Skipping validation on collections that other services also write to.
  • Letting arrays grow without a bound, such as activity logs inside a user document.
  • Relying on the default w: 1 write concern for data that must survive a primary failure.

Frequently Asked Questions

Is MongoDB schemaless?

Not really. MongoDB does not require every document in a collection to have the same fields, but it supports JSON Schema validation that can require fields, restrict types and limit values. Your application always has a schema in practice, so I prefer to enforce the important parts in the database as well.

Does MongoDB support joins?

Yes, through the $lookup stage in an aggregation pipeline, which performs a left outer join to another collection. It works, and it can use an index on the joined field. Even so, if you need a join on every request, it usually means the data should be embedded or partly duplicated instead.

Is MongoDB ACID compliant?

Single document operations are atomic, and multi-document ACID transactions are supported on replica sets since 4.0 and sharded clusters since 4.2. Durability depends on write concern, so use w: "majority" for data that must survive a failover.

What is the maximum document size?

16 mebibytes per BSON document. If you are getting anywhere near it, the design is the problem, usually an unbounded array, and the fix is to move that data into its own collection. For large files, GridFS splits content across many documents.

The best first step with MongoDB has nothing to do with installing it. Write down your five most frequent reads and writes, with roughly how often each runs, before you create the first collection. That list will shape your documents far more than any feature comparison, and it is where the schema design post picks up.

Last updated on 26 October 2026

Document Database MongoDB NoSQL SQL vs NoSQL

Share this article

Leave a Reply

Your email address will not be published. Required fields are marked *