SQL vs NoSQL
If you come from a relational background, MongoDB will feel familiar in some places and completely alien in others. This page maps the two worlds side by side: terminology, schemas, relationships, transactions, and scaling — and, most importantly, when each approach wins.
Terminology Mapping
Most relational concepts have a direct MongoDB counterpart. Learn this table and half of MongoDB's vocabulary is already yours:
SQL (Relational) | MongoDB | Notes |
|---|---|---|
Database | Database | Same concept — a namespace for collections |
Table | Collection | No fixed schema enforced by default |
Row | Document | A BSON document, up to 16 MB |
Column | Field | Fields can differ between documents |
Primary key | _id field | Automatic; defaults to ObjectId |
Index | Index | Nearly identical concept (B-tree based) |
JOIN | $lookup / embedding | Often avoided by embedding related data |
Foreign key | Reference (manual) | No enforced referential integrity |
GROUP BY | $group (aggregation) | Part of the aggregation pipeline |
ALTER TABLE | — (not needed) | Just write documents with new fields |
Transaction | Transaction | Multi-document ACID since MongoDB 4.0 |
View | View / on-demand materialized view | Aggregation-defined |
The Same Data, Two Ways
Consider a blog post with an author and tags. Relational modeling normalizes it into several tables:
Relational: three tables plus a join table
CREATE TABLE authors ( id SERIAL PRIMARY KEY, name VARCHAR(100) ); CREATE TABLE posts ( id SERIAL PRIMARY KEY, author_id INT REFERENCES authors(id), title VARCHAR(200), body TEXT ); CREATE TABLE tags ( id SERIAL PRIMARY KEY, label VARCHAR(50) ); CREATE TABLE post_tags ( post_id INT REFERENCES posts(id), tag_id INT REFERENCES tags(id) ); -- Reading one post = a 4-table join SELECT p.title, a.name, t.label FROM posts p JOIN authors a ON a.id = p.author_id LEFT JOIN post_tags pt ON pt.post_id = p.id LEFT JOIN tags t ON t.id = pt.tag_id WHERE p.id = 1;
MongoDB embeds the data that is read together into a single document:
MongoDB: one document, one read
db.posts.insertOne({
title: "Understanding Indexes",
body: "Indexes are B-trees that...",
author: { name: "Ada Lovelace", authorId: ObjectId("...") },
tags: ["mongodb", "performance", "indexes"],
createdAt: new Date()
})
// Reading the whole post — no joins
db.posts.findOne({ _id: ObjectId("...") })Schema: Enforced vs Flexible
Aspect | SQL | MongoDB |
|---|---|---|
Schema definition | Required up front (CREATE TABLE) | Optional; documents define their own shape |
Adding a field | ALTER TABLE (migration, possible locking) | Just insert/update documents with the new field |
Different shapes per record | Not possible (NULL-filled columns) | Native — each document can differ |
Validation | Types, constraints, foreign keys enforced | Opt-in via JSON Schema validators |
Relationships: Joins vs Embedding
This is the deepest philosophical difference. SQL normalizes: every entity gets its own table, and queries reassemble data with joins. MongoDB encourages you to model around your access patterns:
Embed related data that is read together and belongs to one parent (a post's comments, an order's line items). One read, no joins, atomic updates within the document.
Reference data that is shared, unbounded, or accessed independently (a product referenced by thousands of orders). Store the ObjectId and resolve it with a second query or a
$lookup.
Embedding vs referencing
// Embedded — line items live inside the order
db.orders.insertOne({
orderNumber: 1001,
items: [
{ sku: "KB-01", name: "Keyboard", qty: 1, price: 79.99 },
{ sku: "MS-02", name: "Mouse", qty: 2, price: 24.99 }
],
total: 129.97
})
// Referenced — orders point at a shared customer document
db.orders.insertOne({
orderNumber: 1002,
customerId: ObjectId("665f1a2b3c4d5e6f7a8b9c0d"),
total: 59.99
})
// Resolve the reference with $lookup (MongoDB's LEFT OUTER JOIN)
db.orders.aggregate([
{ $lookup: {
from: "customers",
localField: "customerId",
foreignField: "_id",
as: "customer"
} }
])Transactions and Atomicity
SQL databases treat multi-row ACID transactions as the default unit of work. MongoDB's atomicity model is different:
Single-document operations are always atomic — even when updating multiple fields and nested arrays at once. Because embedding puts related data in one document, this covers many cases where SQL would need a transaction.
Multi-document ACID transactions exist (MongoDB 4.0+ on replica sets, 4.2+ on sharded clusters) but carry performance overhead and are meant to be the exception, not the rule.
A multi-document transaction in mongosh
const session = db.getMongo().startSession()
session.startTransaction()
try {
const accounts = session.getDatabase("bank").accounts
accounts.updateOne({ _id: "alice" }, { $inc: { balance: -100 } })
accounts.updateOne({ _id: "bob" }, { $inc: { balance: 100 } })
session.commitTransaction()
} catch (e) {
session.abortTransaction()
throw e
} finally {
session.endSession()
}Scaling Models
Aspect | SQL (typical) | MongoDB |
|---|---|---|
Primary scaling strategy | Vertical (bigger server), read replicas | Horizontal (sharding) + replica sets |
High availability | Failover setups, often add-on tooling | Replica sets with automatic failover, built in |
Distributing writes | Hard (single writer or complex multi-master) | Sharding routes writes across shards by shard key |
Joins across machines | Expensive or unsupported | Avoided by design (embedding, shard-local data) |
Sharding is where document modeling pays off: because a document is self-contained, MongoDB can place it on any shard and still serve reads without cross-machine joins. Normalized relational data resists this — related rows must either travel together or be joined over the network.
Query Language Comparison
Common queries side by side
// SQL: SELECT * FROM users WHERE age >= 18 ORDER BY name LIMIT 10;
db.users.find({ age: { $gte: 18 } }).sort({ name: 1 }).limit(10)
// SQL: SELECT name, email FROM users WHERE status = 'active';
db.users.find({ status: "active" }, { name: 1, email: 1, _id: 0 })
// SQL: SELECT city, COUNT(*) FROM users GROUP BY city HAVING COUNT(*) > 5;
db.users.aggregate([
{ $group: { _id: "$city", count: { $sum: 1 } } },
{ $match: { count: { $gt: 5 } } }
])
// SQL: UPDATE users SET status = 'inactive' WHERE lastLogin < '2025-01-01';
db.users.updateMany(
{ lastLogin: { $lt: new Date("2025-01-01") } },
{ $set: { status: "inactive" } }
)
// SQL: DELETE FROM sessions WHERE expires < NOW();
db.sessions.deleteMany({ expires: { $lt: new Date() } })When Each Wins
Choose SQL when... | Choose MongoDB when... |
|---|---|
Data is highly relational and queried in many different join combinations | Data is naturally document-shaped (profiles, catalogs, content, events) |
You need strict constraints, foreign keys, and rigid schemas | The schema evolves rapidly or varies per record |
Heavy ad-hoc analytical reporting is the core workload | Read/write patterns are known and can drive the document design |
Every operation is a multi-entity transaction | Most operations touch one entity (one document) at a time |
The dataset fits comfortably on one beefy server | You need horizontal scale-out or geo-distribution from day one |
Summary
Tables become collections, rows become documents, columns become fields — but documents can nest objects and arrays, which changes how you model everything.
SQL normalizes and joins at read time; MongoDB embeds data that is read together and references data that is shared.
Single-document operations in MongoDB are always atomic; multi-document ACID transactions are available but should be occasional.
SQL typically scales up; MongoDB scales out with replica sets (availability) and sharding (capacity).
Neither is universally better — match the database to the data shape and access patterns.