SQL vs NoSQL — When to Use Which

Complete comparison of relational, document, column-family, key-value, and graph databases for system design interviews

In every system design interview, you will be asked to choose a database. "It depends" is the correct philosophical answer — but interviewers want you to know exactly what it depends on. This guide gives you a concrete mental model for choosing the right database every time.

We'll cover the five major database types, their real-world representatives, and a decision framework that works for any system design problem.

1 Five Database Categories

Relational (SQL)

PostgreSQL, MySQL, Oracle, Aurora

Tables, rows, joins. ACID transactions. Schema-first. The default for most systems.

Document

MongoDB, CouchDB, Firestore

JSON documents. Flexible schema. Nested data. Good for 1:many in the same entity.

Column-Family (Wide Column)

Cassandra, HBase, ScyllaDB

Rows of column families. Massive scale writes. Designed for time-series & IoT.

Key-Value

Redis, DynamoDB, Riak, Memcached

Key → value. Blazing fast O(1) lookups. No query language. Caching + sessions.

Graph

Neo4j, Amazon Neptune, ArangoDB

Nodes + edges. Relationship-first. Fraud detection, social graphs, recommendations.

2 Relational (SQL) Databases

Core Properties — ACID

# ACID guarantees (why SQL is trusted for money):
# A — Atomicity:   Transaction is all-or-nothing. "Transfer $100" = both debit AND credit succeed.
# C — Consistency: DB is always in a valid state. Constraints enforced (FK, UNIQUE, NOT NULL).
# I — Isolation:   Concurrent transactions don't interfere. Read Committed, Repeatable Read, Serializable.
# D — Durability:  Committed data survives crashes. WAL (Write-Ahead Log) written to disk first.

# Example: Bank transfer in PostgreSQL — ACID at work
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 'user_a';  -- debit
UPDATE accounts SET balance = balance + 100 WHERE id = 'user_b';  -- credit
COMMIT;  -- both succeed or both roll back (atomicity)

# If server crashes between the two UPDATEs → rollback on restart (durability + atomicity)
# If balance would go negative → check constraint fires → COMMIT fails → no partial state (consistency)

Strengths

  • Joins: Relate data across tables without duplication — the core of normalized data design.
  • Complex queries: SQL is the most expressive query language. GROUP BY, window functions, CTEs, subqueries.
  • Schema enforcement: Data types, foreign keys, and constraints prevent bad data from entering.
  • Mature ecosystem: ORMs, query analyzers, migration tools, monitoring — decades of tooling.
  • Transactions across tables: Multi-table atomic updates work natively.

Weaknesses

  • Horizontal write scaling: A single-primary relational DB tops out at ~10K writes/sec without sharding.
  • Schema rigidity: Adding a column to a 500M-row table requires a migration that can lock for hours.
  • Object-relational impedance mismatch: Mapping OOP objects to tables is awkward for highly nested data.

Choose SQL When…

# Use PostgreSQL/MySQL/Aurora when:
✓ Data has clear relationships (users → orders → line_items → products)
✓ Correctness is paramount (financial, medical, legal)
✓ You need ad-hoc reporting and analytics queries
✓ Team knows SQL well (0 ramp-up cost)
✓ Reasonable scale (tens of millions of rows per table, <10K writes/sec)

# Real examples:
- Banking / fintech (correctness critical)
- E-commerce (orders, inventory, payments)
- SaaS products (user accounts, subscriptions, billing)
- Healthcare (patient records, prescriptions)
- Most CRUD applications built by teams <20 engineers

3 Document Databases (MongoDB, Firestore)

Data Model

# MongoDB document — JSON-like BSON:
{
  "_id": ObjectId("6657e2f0..."),
  "user_id": "u_1234",
  "name": "Arjun Sharma",
  "email": "arjun@example.com",
  "addresses": [                        # ← array of sub-documents
    {"type": "home",   "city": "Bengaluru", "pincode": "560001"},
    {"type": "work",   "city": "Mysuru",    "pincode": "570001"}
  ],
  "preferences": {
    "newsletter": true,
    "theme": "dark"
  }
}

# This eliminates a JOIN that SQL would need (users + addresses tables)
# Read user with all addresses → single document fetch
# Tradeoff: if many users share an address (office building) → data is duplicated in every doc

Strengths

  • Schema flexibility: Add fields without migrations. Field X can exist in doc A but not doc B.
  • Hierarchical data: Nested arrays and objects fit naturally (user → addresses, tweet → media attachments).
  • Developer speed: No ORM needed — JSON from API maps directly to document.
  • Horizontal scaling: MongoDB Atlas shards automatically by shard key.

Weaknesses

  • No joins: Many-to-many relationships require either duplication or application-level joining (multiple queries).
  • Consistency: Historically eventual consistency across replica sets (now supports multi-document transactions, but with overhead).
  • Ad-hoc analytics: MongoDB queries are less expressive than SQL for complex aggregations.
Choose Document when: Content management systems, user profiles with variable attributes, product catalogs with different fields per category, real-time apps where schema evolves frequently (mobile, gaming).

4 Column-Family Databases (Cassandra, HBase)

Data Model — Optimized for Write-Heavy at Scale

# Cassandra table design — think "what queries will I run?" FIRST
# Design tables around query patterns, not entities

# Query: "Give me all messages in conversation X, newest first"
CREATE TABLE messages (
    conversation_id  UUID,
    message_id       TIMEUUID,       -- time-sortable UUID (newest first)
    sender_id        UUID,
    content          TEXT,
    PRIMARY KEY (conversation_id, message_id)  -- partition by conversation
) WITH CLUSTERING ORDER BY (message_id DESC);

# Query executes as single partition lookup → O(1) → handles 1M msgs/sec

# Cassandra's write path (why it's fast):
# 1. Write to commit log (sequential disk write) — crash durability
# 2. Write to memtable (in-memory structure) — fast acknowledgement
# 3. memtable → SSTable flush periodically (background)
# Reads are slower: must check memtable + SSTables + bloom filters + compaction
# Tradeoff: Cassandra is "write-optimized" — writes faster than reads

# Replication factor = 3, consistency = QUORUM:
# Write: wait for 2/3 replicas to confirm → W=2
# Read:  query 2/3 replicas and return newest → R=2
# W + R > RF → strong consistency
# Tunable: use ONE for max throughput, QUORUM for balanced, ALL for maximum safety

When to Choose Cassandra

# Choose Cassandra / HBase when:
✓ Write throughput > 50K/sec consistently
✓ Data naturally fits time-series (IoT sensor readings, event logs, metrics)
✓ Need geo-distributed multi-active writes (Cassandra has no single master)
✓ Data access pattern is simple (no complex JOINs, always query by partition key)
✓ Storing billions of rows across many nodes (petabytes of data)

# Real examples:
- Netflix: stores streaming playback events (1B+ writes/day)
- Uber: stores real-time trip/driver location data
- Discord: chat message storage (switched FROM Cassandra due to hot partition issues)
- Apple: stores 1B+ devices' iCloud backup metadata
- Time-series: Metrics, IoT, recommendation scores with TTL expiry
Anti-patterns: Don't use Cassandra for ad-hoc queries ("find all users where age > 30"), frequent updates to the same row (creates tombstones, degrades performance), or many-to-many relationships. Cassandra's flexibility is a trap — it only works when query patterns are known upfront.

5 Key-Value Stores (Redis, DynamoDB)

The Simplest Database Model

# Redis — the Swiss Army knife of key-value stores
# Not just a key-value store: supports Strings, Hashes, Lists, Sets, Sorted Sets, Streams

# String (simple cache):
redis.setex("user:1234:session", 3600, json.dumps(session_data))  # TTL = 1 hour

# Hash (structured object):
redis.hset("user:1234", mapping={"name": "Anjali", "email": "anjali@example.com", "login_count": "42"})
redis.hincrby("user:1234", "login_count", 1)  # atomic increment

# Sorted Set (leaderboard):
redis.zadd("leaderboard:2026-06", {"player_1": 9500, "player_2": 8800})
redis.zrevrange("leaderboard:2026-06", 0, 9, withscores=True)  # top 10

# List (message queue, recent activity):
redis.lpush("feed:user_456", tweet_id)    # prepend
redis.ltrim("feed:user_456", 0, 799)     # keep last 800 items only

# Redis use cases:
# 1. Session store     → SETEX session_id payload EX 3600
# 2. Rate limiting     → INCR + EXPIRE (sliding window counter)
# 3. Cache             → GET/SET/SETEX with eviction policy (LRU)
# 4. Pub/Sub           → PUBLISH channel message; SUBSCRIBE channel
# 5. Distributed locks → SET key value NX EX 10
# 6. Leaderboards      → Sorted Sets (ZADD, ZRANK, ZREVRANGE)
# 7. Queue/dequeue     → RPUSH/BLPOP (blocking pop for workers)

# DynamoDB vs Redis:
# Redis: in-memory, nanosecond latency, limited by RAM, no complex queries
# DynamoDB: persistent, millisecond latency, unlimited storage, secondary indexes
Choose Redis when: Caching (L1 cache for your DB), session management, rate limiting, real-time leaderboards/counters, message queues, pub/sub for real-time features. Almost every high-scale system has Redis — it's the universal "fast layer."

6 The Decision Framework

Full Comparison Table

FeatureSQL (PostgreSQL)Document (MongoDB)Column-Family (Cassandra)Key-Value (Redis)Graph (Neo4j)
ACID transactions✅ Full⚠️ Limited❌ Tunable❌ Single key only✅ Full
SchemaStrictFlexibleSemi-flexibleNoneFlexible
JOIN support✅ Native❌ App-level❌ Design-time❌ None✅ Native
Write scalability⚠️ Vertical✅ Horizontal✅✅ Extreme✅ In-memory⚠️ Limited
Read scalability✅ Read replicas✅ Sharding⚠️ Slower reads✅✅ Sub-ms⚠️ Complex
Query expressiveness✅✅ SQL✅ MQL⚠️ CQL (limited)❌ Key-only✅ Cypher
Data modelTablesJSON docsWide rowsKey→ValueNodes+Edges

5-Question Decision Flow

# Q1: Do you need complex queries or ad-hoc reporting?
# → YES: SQL (PostgreSQL/MySQL)

# Q2: Is consistency / ACID critical? (money, health records, legal)
# → YES: SQL

# Q3: Is write throughput > 50K/sec or data > 10TB?
# → YES: Cassandra or DynamoDB (depending on query patterns)

# Q4: Is your data highly nested / schema frequently changes?
# → YES: MongoDB / Firestore

# Q5: Is this session management, caching, leaderboards, or pub/sub?
# → YES: Redis

# Q6: Is your primary use case finding relationships between entities?
# → YES: Neo4j (fraud detection, recommendations, social graph traversal)

System Design Interview — Database Choice Signals

  • URL shortener → PostgreSQL (small dataset, ACID for uniqueness) + Redis cache for hot URLs
  • Chat app messages → Cassandra (partition by conversation_id, time-series writes)
  • Social feed (Twitter) → PostgreSQL for tweets + Redis for feed cache
  • E-commerce products → PostgreSQL (joins between products/inventory/orders) or MongoDB (variable product attributes)
  • Fraud detection → Neo4j (traverse user→transaction→merchant relationships)
  • Rate limiter → Redis (INCR + EXPIRE, nanosecond latency essential)
  • Analytics / BI → ClickHouse or BigQuery (columnar, OLAP — a 6th category!)
  • Multi-tenant SaaS → PostgreSQL with row-level security or sharding by tenant_id
Polyglot Persistence: Large systems almost always use multiple database types. Twitter uses MySQL + Redis + Manhattan (internal KV store). Facebook uses MySQL + TAO (graph) + Memcached. Don't try to fit everything into one DB — use the right tool for each use case.

7 Common Interview Mistakes

# Mistake 1: "We need scale, so use NoSQL"
# Reality: PostgreSQL on a high-end instance handles 10M+ users.
# Instagram ran on PostgreSQL for years with 400M users. Scale requires proof first.

# Mistake 2: "MongoDB has no schema, so it's more flexible"
# Reality: Schema-less = application enforces schema.
# If your app code expects "email" to be a string, any other type causes bugs.
# Schema is always there — it just lives in the code, not the DB.

# Mistake 3: "Cassandra gives us eventual consistency, that's fine for user data"
# Reality: Eventual consistency on user profile data means:
# - User changes password → old password may be readable for seconds
# - Dangerous for auth-related data — use Quorum reads or use SQL instead

# Mistake 4: Choosing a DB based on the company's tech stack
# "Netflix uses Cassandra, so we should too"
# Netflix has 250M subscribers, IoT-scale event streams, and 50+ engineers on Cassandra.
# Your startup with 10K users doesn't share the same requirements.

# Right approach: explain tradeoffs
# "I'd start with PostgreSQL. At our current scale (1M users, <5K writes/sec),
# it handles everything with read replicas. If user events grow to 50K/sec
# I'd move that specific table to Cassandra while keeping user profiles in Postgres."

What to Study Next