βοΈ MongoDB vs SQL Interview Questions
Compare NoSQL and Relational Database Concepts
π― About MongoDB vs SQL Questions
These comparison questions are extremely common in interviews. Interviewers want to see if you understand the fundamental differences and can make informed decisions about when to use each database type.
- Conceptual Understanding: Not just memorized facts, but deep comprehension
- Trade-off Analysis: Can you articulate pros/cons of each approach
- Real-world Application: Practical experience with both systems
- Nuanced Thinking: Avoid "X is always better" - it depends on context
- Migration Experience: Have you moved between systems?
- Saying "NoSQL is better" or "SQL is better" without context
- Not understanding that MongoDB supports transactions (since 4.0)
- Thinking schemas in MongoDB are always schemaless
- Claiming SQL can't scale horizontally
- Ignoring hybrid approaches and polyglot persistence
π Core Differences
Fundamental architectural and conceptual differences.
β Complete Answer:
| Aspect | MongoDB | SQL Databases |
|---|---|---|
| Data Model | Document-oriented (JSON-like BSON) | Table-based (rows and columns) |
| Schema | Flexible/Dynamic schema | Fixed/Rigid schema |
| Relationships | Embedded documents or references | Foreign keys & JOINs |
| Query Language | JSON-based query syntax | SQL (Structured Query Language) |
| Scaling | Built for horizontal (sharding) | Traditionally vertical |
| Terminology | Database β Collection β Document β Field | Database β Table β Row β Column |
Data Model Example:
{
_id: ObjectId("507f1f77bcf86cd799439011"),
name: "Alice Johnson",
email: "alice@example.com",
age: 28,
address: {
street: "123 Main St",
city: "San Francisco",
zip: "94102"
},
orders: [
{ orderId: 101, total: 150, date: ISODate("2024-01-15") },
{ orderId: 102, total: 220, date: ISODate("2024-02-20") }
]
}
// Single document contains nested data
// No need for JOIN operations
-- Users table CREATE TABLE users ( id INT PRIMARY KEY, name VARCHAR(100), email VARCHAR(100), age INT ); -- Addresses table CREATE TABLE addresses ( id INT PRIMARY KEY, user_id INT FOREIGN KEY REFERENCES users(id), street VARCHAR(200), city VARCHAR(100), zip VARCHAR(10) ); -- Orders table CREATE TABLE orders ( id INT PRIMARY KEY, user_id INT FOREIGN KEY REFERENCES users(id), total DECIMAL(10,2), date DATE ); -- Requires JOINs to get complete user data SELECT u.*, a.*, o.* FROM users u LEFT JOIN addresses a ON u.id = a.user_id LEFT JOIN orders o ON u.id = o.user_id WHERE u.id = 1;
Key Conceptual Differences:
1. Schema Flexibility
- MongoDB: Documents in same collection can have different fields
- SQL: All rows must conform to table schema
// MongoDB - Different structures allowed
db.users.insertMany([
{ name: "Alice", email: "alice@example.com" },
{ name: "Bob", email: "bob@example.com", phone: "555-1234" },
{ name: "Charlie", age: 30, address: { city: "NYC" } }
])
// All valid in same collection!
-- SQL - Must ALTER TABLE to add columns
INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');
-- Can't insert phone without adding column first
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
INSERT INTO users (name, email, phone) VALUES ('Bob', 'bob@example.com', '555-1234');
2. Data Relationships
- MongoDB: Embeds related data OR uses references
- SQL: Normalizes data across tables with foreign keys
Don't just list differences. Explain WHEN each approach is better. For example: "MongoDB's flexible schema is ideal for rapidly evolving applications, while SQL's rigid schema enforces data integrity for financial systems."
β Complete Answer:
Choose MongoDB When:
1. Rapid Development & Iteration
Use Case: Startup building MVP, requirements changing weekly
- No time for schema migrations every sprint
- Product features evolving based on user feedback
- Need to add fields without downtime
Example: Social media platform adding new user profile fields (bio, interests, badges) without altering database schema.
2. Hierarchical/Nested Data
Use Case: Content management system, e-commerce product catalog
- Data naturally hierarchical (JSON-like)
- Would require many JOINs in SQL
- Read entire object in one query
// E-commerce product with variants
{
productId: "P123",
name: "T-Shirt",
category: "Clothing",
variants: [
{ size: "S", color: "Red", sku: "TS-S-R", price: 19.99, stock: 50 },
{ size: "M", color: "Red", sku: "TS-M-R", price: 19.99, stock: 30 },
{ size: "L", color: "Blue", sku: "TS-L-B", price: 19.99, stock: 20 }
],
reviews: [
{ user: "Alice", rating: 5, comment: "Great fit!" },
{ user: "Bob", rating: 4, comment: "Nice quality" }
]
}
// Single query gets complete product data
// SQL would need 3+ tables and JOINs
3. High Write Volume / Real-time Analytics
Use Case: IoT sensor data, logging systems, clickstream analytics
- Thousands of writes per second
- Time-series data with flexible attributes
- Need horizontal scaling (sharding)
Example: Smart home system collecting sensor data from millions of devices, each with different sensor types.
4. Geospatial Applications
Use Case: Food delivery, ride-sharing, real estate search
- Built-in geospatial indexes (2dsphere)
- Native GeoJSON support
- Efficient proximity queries
// Find restaurants within 5km
db.restaurants.find({
location: {
$near: {
$geometry: { type: "Point", coordinates: [-122.4194, 37.7749] },
$maxDistance: 5000
}
}
})
5. Horizontal Scaling Requirements
Use Case: Large-scale applications (100M+ users)
- Dataset too large for single server
- Need to distribute across multiple machines
- Automatic sharding built-in
Choose SQL When:
1. Complex Transactions
Use Case: Banking, financial systems, inventory management
- Multi-table ACID transactions critical
- Strong consistency required
- Complex business rules
2. Complex Analytical Queries
Use Case: Business intelligence, reporting systems
- Complex JOINs across many tables
- Aggregations with GROUP BY, HAVING
- SQL's mature query optimization
3. Mature Ecosystem Needed
Use Case: Enterprise applications, legacy system integration
- Decades of tooling and expertise
- ORM support (Hibernate, Entity Framework)
- Integration with BI tools
"We're building a content management system for a news website. Which database would you choose?"
Good Answer: "I'd choose MongoDB because:
- Articles have varying structure (text, images, videos, polls)
- Need to iterate quickly on article metadata
- Reads heavily outweigh writes (can scale with replica sets)
- Natural document structure matches article content
However, if the system required complex analytics across authors, categories, and engagement metrics with many JOINs, SQL might be better for the analytics portion (polyglot persistence)."
β Complete Answer:
ACID Recap:
- Atomicity: All operations succeed or all fail
- Consistency: Database moves from valid state to valid state
- Isolation: Concurrent transactions don't interfere
- Durability: Committed data persists even after crashes
| Property | SQL Databases | MongoDB |
|---|---|---|
| Atomicity | Multi-table transactions | Single document always atomic Multi-document since v4.0 |
| Consistency | Enforced via constraints | Eventual consistency (tunable) Strong consistency with write/read concern |
| Isolation | Multiple isolation levels | Read uncommitted (default) Snapshot isolation in transactions |
| Durability | Write-ahead logging (WAL) | Journal + write concern |
MongoDB Transaction Support:
β YES - MongoDB Supports Transactions (Since v4.0)
Common misconception: People think MongoDB doesn't support transactions. It does!
- v4.0 (2018): Multi-document transactions in replica sets
- v4.2 (2019): Distributed transactions across shards
- Snapshot isolation: Transactions see consistent snapshot of data
// MongoDB Multi-Document Transaction
const session = client.startSession();
try {
session.startTransaction();
// Transfer $100 from Alice to Bob
await accounts.updateOne(
{ name: "Alice" },
{ $inc: { balance: -100 } },
{ session }
);
await accounts.updateOne(
{ name: "Bob" },
{ $inc: { balance: 100 } },
{ session }
);
await session.commitTransaction();
console.log("Transfer successful");
} catch (error) {
await session.abortTransaction();
console.log("Transfer failed, rolled back");
} finally {
session.endSession();
}
-- SQL Transaction (PostgreSQL) BEGIN TRANSACTION; UPDATE accounts SET balance = balance - 100 WHERE name = 'Alice'; UPDATE accounts SET balance = balance + 100 WHERE name = 'Bob'; COMMIT; -- or ROLLBACK on error
Key Differences:
1. Single Document Atomicity
MongoDB Advantage: Single document operations are ALWAYS atomic
// Atomic update on embedded document
db.accounts.updateOne(
{ _id: userId },
{
$inc: { balance: 100 },
$push: {
transactions: {
amount: 100,
date: new Date(),
type: "deposit"
}
}
}
)
// Both balance update AND transaction push succeed or fail together
// No transaction needed!
In SQL, this would require a transaction across 2 tables (accounts + transactions).
2. Transaction Performance
- SQL: Highly optimized, decades of tuning
- MongoDB: Newer, some performance overhead
- Best Practice: Design MongoDB schemas to minimize multi-doc transactions
- Write Concern: MongoDB's "durability" depends on write concern setting
- w:1 (default): Acknowledged by primary only (could lose data on crash)
- w:"majority": Replicated to majority (durable like SQL)
- Consistency: MongoDB is eventually consistent by default, but configurable
Don't say "MongoDB doesn't support ACID" - that's outdated (pre-2018)! Explain that:
- Single document ops are always ACID
- Multi-document transactions supported since v4.0
- ACID properties are tunable via write/read concerns
- MongoDB trades some ACID guarantees for performance/availability by default
π Schema Design
Data modeling and schema design philosophies.
β Complete Answer:
SQL Normalization Philosophy:
Eliminate redundancy by splitting data across multiple tables.
-- Normalized SQL Design (3NF) -- Users table users: id | name | email 1 | Alice | alice@example.com 2 | Bob | bob@example.com -- Orders table orders: id | user_id | total | date 101| 1 | 150 | 2024-01-15 102| 1 | 220 | 2024-02-20 103| 2 | 180 | 2024-03-10 -- Products table products: id | name | price 1 | Widget | 50 2 | Gadget | 75 -- Order_Items table (junction table) order_items: order_id | product_id | quantity 101 | 1 | 3 102 | 2 | 2 103 | 1 | 1 Benefits: β No data redundancy β Easy to update (change product price once) β Data integrity enforced Drawbacks: β Requires complex JOINs (4 tables to get user's orders with products) β JOIN operations expensive
MongoDB Denormalization Philosophy:
Store data together that's accessed together (optimize for reads).
// Denormalized MongoDB Design
{
_id: 1,
name: "Alice",
email: "alice@example.com",
orders: [
{
orderId: 101,
total: 150,
date: ISODate("2024-01-15"),
items: [
{ product: "Widget", price: 50, quantity: 3 }
]
},
{
orderId: 102,
total: 220,
date: ISODate("2024-02-20"),
items: [
{ product: "Gadget", price: 75, quantity: 2 }
]
}
]
}
Benefits:
β Single query gets all data
β No JOINs needed
β Atomic updates on user document
Drawbacks:
β Data duplication (product info repeated)
β If product price changes, must update many documents
β Document size can grow large
Embed vs Reference Decision Framework:
β EMBED When:
- One-to-Few: User has 1-10 addresses
- Data accessed together: Always show addresses with user
- Bounded growth: Won't exceed 16MB document limit
- Child data specific to parent: Order items belong only to that order
// Embed addresses
{
_id: userId,
name: "Alice",
addresses: [
{ type: "home", street: "123 Main St", city: "SF" },
{ type: "work", street: "456 Oak Ave", city: "SF" }
]
}
β REFERENCE When:
- One-to-Many (unbounded): Blog post has 10,000+ comments
- Many-to-Many: Students and courses
- Data accessed independently: Products queried separately from orders
- Frequent updates to child: Product prices change often
// Reference products
// Products collection
{
_id: "product-1",
name: "Widget",
price: 50,
stock: 100
}
// Orders collection (references product)
{
_id: "order-101",
userId: "user-1",
items: [
{ productId: "product-1", quantity: 3 } // Reference, not embed
]
}
// Lookup when needed
db.orders.aggregate([
{ $lookup: {
from: "products",
localField: "items.productId",
foreignField: "_id",
as: "productDetails"
}
}
])
Real-World Example: Blog System
β Embed Comments (Good for small blogs)
{
_id: "post-1",
title: "MongoDB Basics",
content: "...",
comments: [
{ user: "Alice", text: "Great post!", date: "..." },
{ user: "Bob", text: "Very helpful", date: "..." }
]
}
Good when:
- Few comments per post (< 100)
- Comments always shown with post
- Comments don't change often
β Reference Comments (Better for popular blogs)
// Posts collection
{ _id: "post-1", title: "MongoDB", ... }
// Comments collection
{ _id: "c1", postId: "post-1", user: "Alice", ... }
{ _id: "c2", postId: "post-1", user: "Bob", ... }
Better when:
- Thousands of comments per post
- Pagination needed
- Comments have complex queries
When asked about schema design, always:
- Ask about access patterns: "How will data be queried?"
- Ask about scale: "How many related items expected?"
- Ask about update frequency: "Does child data change often?"
- Explain trade-offs of your choice
π Query Comparison
How queries differ between MongoDB and SQL.
β Complete Answer:
Example 1: Simple SELECT
SELECT name, email, age FROM users WHERE age >= 25 AND city = 'San Francisco' ORDER BY name ASC LIMIT 10;
db.users.find(
{ age: { $gte: 25 }, city: "San Francisco" }, // WHERE clause
{ name: 1, email: 1, age: 1, _id: 0 } // SELECT clause (projection)
)
.sort({ name: 1 }) // ORDER BY
.limit(10); // LIMIT
Example 2: JOIN Operations
SELECT u.name, u.email, o.id AS order_id, o.total, o.date FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE u.age > 25;
db.users.aggregate([
{ $match: { age: { $gt: 25 } } }, // WHERE clause
{
$lookup: {
from: "orders", // JOIN orders table
localField: "_id", // users._id
foreignField: "userId", // orders.userId
as: "userOrders" // Result array
}
},
{
$project: { // SELECT clause
name: 1,
email: 1,
"userOrders.orderId": 1,
"userOrders.total": 1,
"userOrders.date": 1
}
}
]);
Example 3: Aggregation (GROUP BY)
SELECT city, COUNT(*) AS user_count, AVG(age) AS avg_age FROM users GROUP BY city HAVING COUNT(*) > 10 ORDER BY user_count DESC;
db.users.aggregate([
{
$group: {
_id: "$city", // GROUP BY city
user_count: { $sum: 1 }, // COUNT(*)
avg_age: { $avg: "$age" } // AVG(age)
}
},
{
$match: { user_count: { $gt: 10 } } // HAVING
},
{
$sort: { user_count: -1 } // ORDER BY
}
]);
Example 4: Complex Aggregation with Multiple JOINs
SELECT u.name, COUNT(DISTINCT o.id) AS total_orders, SUM(o.total) AS total_spent, AVG(o.total) AS avg_order_value FROM users u LEFT JOIN orders o ON u.id = o.user_id WHERE o.date >= '2024-01-01' GROUP BY u.id, u.name HAVING COUNT(o.id) >= 3 ORDER BY total_spent DESC LIMIT 5;
db.users.aggregate([
{
$lookup: {
from: "orders",
localField: "_id",
foreignField: "userId",
as: "orders"
}
},
{
$unwind: "$orders" // Flatten array
},
{
$match: {
"orders.date": { $gte: ISODate("2024-01-01") }
}
},
{
$group: {
_id: "$_id",
name: { $first: "$name" },
total_orders: { $sum: 1 },
total_spent: { $sum: "$orders.total" },
avg_order_value: { $avg: "$orders.total" }
}
},
{
$match: { total_orders: { $gte: 3 } }
},
{
$sort: { total_spent: -1 }
},
{
$limit: 5
}
]);
Key Differences:
| Aspect | SQL | MongoDB |
|---|---|---|
| Syntax | Declarative SQL statements | JSON-based methods/pipelines |
| Query Structure | Single statement | Pipeline of stages |
| JOINs | Native and optimized | $lookup (less optimized) |
| Aggregation | GROUP BY with functions | $group stage with operators |
| Learning Curve | Familiar to most developers | Requires learning new syntax |
SQL databases are highly optimized for JOIN operations after decades of development. MongoDB's $lookup is less mature and can be slower. This is why MongoDB encourages embedding data when possible to avoid JOINs entirely.
π³ Transactions & ACID
Understanding transaction support and ACID compliance in both databases.
β Complete Answer:
Transaction Contention Overview:
When multiple transactions try to modify the same data simultaneously, contention occurs. How databases handle this differs significantly.
| Aspect | SQL Databases | MongoDB |
|---|---|---|
| Locking Mechanism | Row-level, table-level, or page-level locks | Document-level locking (WiredTiger) |
| Concurrency Control | 2PL (Two-Phase Locking) or MVCC | Optimistic concurrency (write conflicts) |
| Conflict Detection | Lock waits or deadlock detection | WriteConflict exceptions |
| Retry Logic | Application handles or automatic | Application must implement retries |
| Performance Impact | Lock contention can cause waits | Write conflicts abort and retry |
SQL Transaction Contention:
-- PostgreSQL Example: Lock Contention -- Transaction 1 BEGIN; SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- Locks row UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- ... processing ... COMMIT; -- Transaction 2 (concurrent) BEGIN; SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- WAITS for lock -- This transaction BLOCKS until Transaction 1 commits UPDATE accounts SET balance = balance + 50 WHERE id = 1; COMMIT; -- Result: Transaction 2 waits (blocks) until Transaction 1 releases lock -- No retry needed, but performance degrades with high contention
SQL Lock Behavior
- Pessimistic Locking: Locks acquired early, held until commit
- Blocking: Concurrent transactions wait in queue
- Deadlock Detection: Automatic detection and rollback
- Lock Timeout: Configurable timeout to prevent infinite waits
MongoDB Transaction Contention:
// MongoDB Example: Write Conflict
// Transaction 1
const session1 = client.startSession();
session1.startTransaction();
await accounts.updateOne(
{ _id: 1 },
{ $inc: { balance: -100 } },
{ session: session1 }
);
// ... processing ...
await session1.commitTransaction();
// Transaction 2 (concurrent)
const session2 = client.startSession();
session2.startTransaction();
try {
await accounts.updateOne(
{ _id: 1 },
{ $inc: { balance: 50 } },
{ session: session2 }
);
await session2.commitTransaction();
} catch (error) {
if (error.hasOwnProperty('errorLabels') &&
error.errorLabels.includes('TransientTransactionError')) {
// WriteConflict - Transaction 2 must RETRY
console.log('Write conflict detected, retrying...');
await session2.abortTransaction();
// Application must implement retry logic
}
}
// Result: Transaction 2 gets WriteConflict and must retry
// No blocking, but requires retry implementation
MongoDB Write Conflict Behavior
- Optimistic Concurrency: No locks, conflicts detected at commit
- Non-Blocking: Failed transaction aborts immediately
- Write Conflicts: Throw TransientTransactionError
- Application Retries: Developer must implement retry logic
Retry Pattern Implementation:
Best Practice: MongoDB Retry Logic
async function runTransactionWithRetry(txnFunc, session, maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
session.startTransaction();
await txnFunc(session); // Execute transaction operations
await session.commitTransaction();
console.log('Transaction committed successfully');
return; // Success!
} catch (error) {
await session.abortTransaction();
// Check if error is transient (write conflict)
if (error.hasOwnProperty('errorLabels') &&
error.errorLabels.includes('TransientTransactionError')) {
console.log(`Attempt ${i + 1} failed, retrying...`);
// Exponential backoff
await new Promise(resolve =>
setTimeout(resolve, Math.pow(2, i) * 100)
);
continue; // Retry
} else {
// Non-transient error, don't retry
throw error;
}
}
}
throw new Error('Transaction failed after maximum retries');
}
// Usage
const session = client.startSession();
try {
await runTransactionWithRetry(async (session) => {
await accounts.updateOne(
{ name: "Alice" },
{ $inc: { balance: -100 } },
{ session }
);
await accounts.updateOne(
{ name: "Bob" },
{ $inc: { balance: 100 } },
{ session }
);
}, session);
} finally {
session.endSession();
}
Performance Comparison:
| Scenario | SQL (Locking) | MongoDB (Optimistic) |
|---|---|---|
| Low Contention | β Excellent (minimal waits) | β Excellent (no conflicts) |
| Medium Contention | β οΈ Good (some blocking) | β οΈ Good (some retries) |
| High Contention | β Degrades (long waits, timeouts) | β Degrades (many retries, failures) |
| Read-Heavy Workload | β Excellent (MVCC allows reads) | β Excellent (no conflicts) |
| Write-Heavy Workload | β οΈ Lock contention increases | β οΈ Write conflicts increase |
Real-World Example: Banking System
Scenario: 1000 concurrent transfers on same account
SQL Approach (PostgreSQL):
- First transaction acquires lock on account row
- Remaining 999 transactions wait in queue
- Executes serially: T1 β T2 β T3 β ... β T1000
- Predictable but slow under high contention
- No application retry logic needed
MongoDB Approach:
- All 1000 transactions attempt concurrently
- First commit wins, rest get WriteConflict
- 999 transactions abort and retry (with backoff)
- Eventually all succeed through retries
- Requires robust retry implementation
Deadlock Handling:
SQL Deadlock Detection
-- Transaction 1 BEGIN; UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- Locks account 1 -- ... processing ... UPDATE accounts SET balance = balance + 100 WHERE id = 2; -- Wants account 2 COMMIT; -- Transaction 2 (concurrent) BEGIN; UPDATE accounts SET balance = balance - 50 WHERE id = 2; -- Locks account 2 -- ... processing ... UPDATE accounts SET balance = balance + 50 WHERE id = 1; -- Wants account 1 COMMIT; -- DEADLOCK! PostgreSQL automatically detects and aborts one transaction -- Error: deadlock detected -- DETAIL: Process 1234 waits for ShareLock on transaction 5678; -- blocked by process 5678. -- Process 5678 waits for ShareLock on transaction 1234; -- blocked by process 1234.
SQL automatically detects circular waits and aborts one transaction.
MongoDB Deadlock Prevention
// MongoDB doesn't have traditional deadlocks because:
// 1. No lock acquisition - optimistic concurrency
// 2. Conflicts detected at commit time
// 3. First commit wins, others retry
// However, distributed deadlocks CAN occur across shards
// MongoDB uses timeout-based detection
// Transaction 1
session1.startTransaction();
await accounts.updateOne({ _id: 1 }, { $inc: { balance: -100 } }, { session: session1 });
await accounts.updateOne({ _id: 2 }, { $inc: { balance: 100 } }, { session: session1 });
await session1.commitTransaction(); // One commits successfully
// Transaction 2
session2.startTransaction();
await accounts.updateOne({ _id: 2 }, { $inc: { balance: -50 } }, { session: session2 });
await accounts.updateOne({ _id: 1 }, { $inc: { balance: 50 } }, { session: session2 });
await session2.commitTransaction(); // Gets WriteConflict, must retry
// MongoDB aborts conflicting transaction - no circular wait
MongoDB prevents deadlocks through optimistic concurrency and transaction timeouts.
Key Differences:
- SQL: Pessimistic locking β transactions wait β no retry needed
- MongoDB: Optimistic concurrency β conflicts abort β application retries
- SQL: Automatic deadlock detection and resolution
- MongoDB: Timeout-based deadlock prevention
- Choose SQL when: High contention expected, predictable performance critical
- Choose MongoDB when: Low-medium contention, horizontal scaling needed
- MongoDB: Always implement exponential backoff retry logic
- MongoDB: Design schemas to minimize multi-document transactions
- SQL: Use appropriate isolation levels (READ COMMITTED vs SERIALIZABLE)
- SQL: Index foreign keys to reduce lock escalation
- Both: Monitor transaction duration and conflict rates
- Both: Use connection pooling to manage concurrent sessions
β Complete Answer:
Isolation Levels Overview:
Isolation determines how transaction integrity is visible to other concurrent transactions. SQL offers multiple levels, MongoDB has different defaults.
| Isolation Level | SQL Support | MongoDB Support |
|---|---|---|
| Read Uncommitted | β Supported | β Not supported |
| Read Committed | β Default (most DBs) | β Default outside transactions |
| Repeatable Read | β Supported | β Snapshot isolation (in transactions) |
| Serializable | β Supported | β Snapshot isolation provides similar guarantees |
Read Phenomena:
1. Dirty Reads
Definition: Transaction reads uncommitted changes from another transaction
-- SQL Example (Read Uncommitted) -- Transaction 1 BEGIN TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; UPDATE accounts SET balance = 1000 WHERE id = 1; -- Not committed yet -- Transaction 2 (concurrent) BEGIN TRANSACTION ISOLATION LEVEL READ UNCOMMITTED; SELECT balance FROM accounts WHERE id = 1; -- Sees 1000 (dirty read!) COMMIT; -- Transaction 1 rolls back ROLLBACK; -- Balance reverts to original value -- Transaction 2 read incorrect data!
- SQL: Possible with READ UNCOMMITTED
- MongoDB: NOT possible (minimum is read committed)
2. Non-Repeatable Reads
Definition: Same query returns different results within a transaction
-- SQL Example (Read Committed) -- Transaction 1 BEGIN; SELECT balance FROM accounts WHERE id = 1; -- Returns 500 -- Transaction 2 (concurrent) BEGIN; UPDATE accounts SET balance = 1000 WHERE id = 1; COMMIT; -- Transaction 1 continues SELECT balance FROM accounts WHERE id = 1; -- Returns 1000 (different!) COMMIT; -- Same row read twice, different values!
- SQL: Prevented by REPEATABLE READ or higher
- MongoDB: Prevented inside transactions (snapshot isolation)
3. Phantom Reads
Definition: New rows appear in repeated query due to inserts
-- SQL Example
-- Transaction 1
BEGIN;
SELECT COUNT(*) FROM accounts WHERE balance > 1000; -- Returns 5
-- Transaction 2 (concurrent)
BEGIN;
INSERT INTO accounts (id, balance) VALUES (999, 2000);
COMMIT;
-- Transaction 1 continues
SELECT COUNT(*) FROM accounts WHERE balance > 1000; -- Returns 6 (phantom!)
COMMIT;
-- New row appeared ("phantom") between reads
- SQL: Prevented only by SERIALIZABLE
- MongoDB: Prevented inside transactions (snapshot isolation)
MongoDB Snapshot Isolation:
MongoDB Transaction Behavior
// MongoDB snapshot isolation
const session = client.startSession();
session.startTransaction();
// Read 1: Sees snapshot at transaction start
const result1 = await accounts.find(
{ balance: { $gt: 1000 } },
{ session }
).toArray();
console.log('Count:', result1.length); // 5 documents
// Concurrent update (different session)
await accounts.insertOne({ _id: 999, balance: 2000 }); // Committed
// Read 2: Still sees SAME snapshot (no phantom reads)
const result2 = await accounts.find(
{ balance: { $gt: 1000 } },
{ session }
).toArray();
console.log('Count:', result2.length); // Still 5 documents!
await session.commitTransaction();
session.endSession();
// Transaction sees consistent snapshot throughout
// No dirty reads, non-repeatable reads, or phantom reads
MongoDB transactions use snapshot isolation by default - preventing all read phenomena.
MongoDB's snapshot isolation in transactions is similar to SQL's SERIALIZABLE, but with better performance characteristics. Outside transactions, MongoDB uses read committed semantics.
π Scaling Strategies
How each database handles growth.
β Complete Answer:
Scaling Approaches:
Vertical Scaling (Scale Up)
Add more resources to single server (CPU, RAM, Disk)
- Pros: Simple, no code changes, strong consistency
- Cons: Hardware limits, expensive, single point of failure
- SQL: Traditional approach, works well initially
- MongoDB: Also supported, but not the main scaling strategy
Horizontal Scaling (Scale Out)
Add more servers to distribute load
- Pros: Nearly unlimited scaling, fault tolerance, cost-effective
- Cons: Complex, consistency challenges, distributed transactions
- SQL: Difficult, requires manual sharding or clustering
- MongoDB: Built-in sharding, designed for this
MongoDB Sharding:
MongoDB Sharding Architecture:
Application
β
βΌ
βββββββββββ
β mongos β β Query router (stateless)
βββββββββββ
β
ββββββββββββββ¬βββββββββββββ¬βββββββββββββ
βΌ βΌ βΌ βΌ
βββββββββββ βββββββββββ βββββββββββ βββββββββββ
β Shard 1 β β Shard 2 β β Shard 3 β β Shard 4 β
β(Replica β β(Replica β β(Replica β β(Replica β
β Set) β β Set) β β Set) β β Set) β
βββββββββββ βββββββββββ βββββββββββ βββββββββββ
userId userId userId userId
1-250K 250K-500K 500K-750K 750K-1M
Features:
β Automatic data distribution
β Automatic chunk splitting and balancing
β Transparent to application
β Add shards on the fly
β Each shard is a replica set (HA)
SQL Horizontal Scaling:
SQL Horizontal Scaling Options:
1. Read Replicas (Easy)
Primary βββ¬ββ> Replica 1 (read-only)
βββ> Replica 2 (read-only)
βββ> Replica 3 (read-only)
β Scale reads only
β Writes still bottlenecked on primary
2. Manual Sharding (Hard)
Application decides which DB to use
if (userId % 4 == 0) β DB Server 1
if (userId % 4 == 1) β DB Server 2
if (userId % 4 == 2) β DB Server 3
if (userId % 4 == 3) β DB Server 4
β Scales reads and writes
β Application manages routing
β Complex queries across shards
β Rebalancing requires manual migration
3. Clustering Solutions (PostgreSQL Citus, MySQL Cluster)
β More automated
β Less mature than MongoDB sharding
β Often limited compared to single-server SQL
Comparison Table:
| Feature | MongoDB Sharding | SQL Horizontal Scaling |
|---|---|---|
| Ease of Setup | Built-in, configure with commands | Manual or third-party solutions |
| Automatic Balancing | Yes, background balancer | No, manual rebalancing |
| Routing | mongos routes automatically | Application logic required |
| Cross-Shard Queries | Supported (broadcast queries) | Complex, often not supported |
| Transactions | Cross-shard since v4.2 | Very difficult or impossible |
| Maturity | Core feature, well-tested | Varies, often immature |
Real-World Example:
Scenario: Social Media App Scaling to 100M Users
MongoDB Approach:
// Enable sharding on database
sh.enableSharding("socialMedia")
// Shard users collection on userId
sh.shardCollection("socialMedia.users", { userId: "hashed" })
// Add more shards as user base grows
sh.addShard("shard5/server13:27017,server14:27017,server15:27017")
// System automatically:
// - Distributes new users evenly
// - Balances chunks across shards
// - Routes queries to correct shards
SQL Approach:
// 1. Decide sharding strategy in application code
function getUserDB(userId) {
const shardNum = userId % 4; // 4 database servers
return dbConnections[shardNum];
}
// 2. Every query must include sharding logic
const db = getUserDB(userId);
const user = await db.query("SELECT * FROM users WHERE id = ?", [userId]);
// 3. Cross-shard queries very complex
// Getting all users? Must query ALL shards and merge results!
// 4. Adding 5th shard = rebalance entire system manually
// Move 20% of users from each shard to new shard
MongoDB was designed from day 1 for horizontal scaling via sharding. SQL databases were designed for vertical scaling, with horizontal scaling added as an afterthought through third-party solutions or application-level sharding.
π Master Database Comparisons!
Understanding MongoDB vs SQL trade-offs is crucial for system design interviews and architectural decisions. Remember: it's not about which is "better" - it's about which is better FOR YOUR USE CASE.
Key Takeaways:
- MongoDB excels at flexible schemas and horizontal scaling
- SQL excels at complex transactions and JOIN operations
- Both support ACID transactions (MongoDB since v4.0)
- Schema design differs: normalize in SQL, often denormalize in MongoDB
- Modern applications often use both (polyglot persistence)