CMD Guide
HomeSystem DesignSystem Design Trade-offs

SQL vs NoSQL (2)

The real choice between SQL and NoSQL is when you pay to assemble an entity: a relational engine stores each fact exactly once in a normalized table and reconstructs an entity at read time by joining B-tree indexes, whereas a NoSQL store pre-shapes data around one dominant access pattern at write time so that a read touches a single key on a single shard — no join, no cross-node coordination. Everything else (schema flexibility, scaling, consistency) follows from that one mechanical difference.

This page goes past the cabinet analogy into the data-model mechanics that page 002 introduces: how each of the four NoSQL families physically lays out data, and how that dictates the sharding, join, and consistency trade-offs you inherit.

The four NoSQL families are four physical access patterns

"NoSQL" is not one thing. Each family is a different on-disk shape optimized for one kind of lookup, and each is bad at the lookups it was not shaped for. That weakness is the price of its speed.

Family (example)Physical layoutO(1) / cheap accessStructurally weak at
Key-value (Redis, DynamoDB)Hash of key → opaque value blobget/put by exact keyrange scans, querying inside the value
Document (MongoDB)Key → JSON/BSON tree + secondary indexesfetch/patch one whole aggregatejoins across documents, multi-doc transactions
Wide-column (Cassandra)Partition key → rows sorted by clustering keyread a contiguous slice of one partitionad-hoc queries on non-key columns
Graph (Neo4j)Nodes with adjacency-list pointers to edgesmulti-hop traversal from a start nodewhole-dataset aggregation, sharding

A relational database refuses to specialize: it keeps every column indexable and every table joinable, so it can answer questions you did not anticipate. That generality is exactly what forces read-time joins and makes horizontal write-sharding hard.

Worked example: rendering one order page

Schema for an e-commerce order screen, normalized the relational way:

users(user_id PK, name, city)
orders(order_id PK, user_id FK, status, created_at)
order_items(order_id FK, product_id FK, qty, unit_price)
products(product_id PK, name, list_price)

Render order 5001 (user 42, two line items). The engine assembles it at read time:

  1. Index seek orders on order_id = 5001 → row (user_id = 42, status = SHIPPED).
  2. Index seek users on user_id = 42(Asha, Chennai).
  3. Index range scan order_items on order_id = 5001 → 2 rows: (prod 88, qty 1), (prod 91, qty 3).
  4. Index seeks products on 88 and 91("USB-C cable", 299), ("Laptop stand", 1499).
  5. Join and nest the rows into one result. Total: 5 index lookups across 4 tables for one screen.

The document store pays that cost once, at write time. One BSON document keyed by order_id embeds a snapshot of the user and copies each product's name and price into the line item:

db.orders.findOne({ _id: 5001 })
// {_id:5001, user:{id:42, name:"Asha", city:"Chennai"}, status:"SHIPPED",
//  items:[ {product_id:88, name:"USB-C cable",  qty:1, unit_price:299},
//          {product_id:91, name:"Laptop stand", qty:3, unit_price:1499} ]}

One read by _id → the whole page, on one shard. But the bill arrives later: rename product 88 and you must now update every order document that embedded it (update fan-out / write amplification), and by default there is no transaction spanning those documents — so different orders show the old and new name for a window. The relational version renames one row in products and every join instantly reflects it.

diagram
diagram

Wide-column worked example: model the query, not the entity

The document example pre-joins one aggregate. Wide-column (Cassandra) goes further: you model each table around one read, and the primary key has two jobs. Take a chat app's "latest messages in a room":

CREATE TABLE messages_by_room (
  room_id     text,
  created_at  timeuuid,
  sender      text,
  body        text,
  PRIMARY KEY ((room_id), created_at)
) WITH CLUSTERING ORDER BY (created_at DESC);

The target read is then one contiguous slice of a single partition on a single node — no join, no scatter-gather:

SELECT * FROM messages_by_room WHERE room_id = ? LIMIT 50;   -- cheap: one partition slice

A second access pattern needs a second table. Ask "all messages by sender X" and this table cannot help — sender is neither the partition nor the clustering key, so Cassandra can only answer with ALLOW FILTERING, a full-cluster scan you must never ship. The query-first fix is to write a second, denormalized table keyed for that read, populated by the same write (ideally in one BATCH):

CREATE TABLE messages_by_sender (
  sender text, created_at timeuuid, room_id text, body text,
  PRIMARY KEY ((sender), created_at)
) WITH CLUSTERING ORDER BY (created_at DESC);

One table per query is normal here — the opposite of relational, where one normalized schema answers every query via joins. Hot-partition trap: the partition key must be high-cardinality and evenly hit. A low-cardinality key (or a date-only key like PRIMARY KEY ((day), created_at)) funnels all of today's traffic onto the single node owning that partition; likewise one giant broadcast room concentrates its whole load on one node. The fix is a higher-cardinality or bucketed key (e.g. ((room_id, day_bucket))) so writes and reads spread across the ring.

Pitfalls a working engineer hits

When to use which — and what it costs

Choose relational (SQL) when your access patterns are varied or not yet known, you need multi-entity ACID transactions (money movement, inventory decrement, double-entry), you want ad-hoc joins and analytics, and your scale fits one write primary plus read replicas — which covers the large majority of applications. What it costs vs NoSQL: a single write primary is a ceiling; sharding writes across nodes is real operational work (Vitess, Citus) and cross-shard transactions get expensive.

Choose NoSQL when you have one dominant, known access pattern, write/scale volume that genuinely exceeds a single primary, evolving or sparse fields, and you can tolerate eventual consistency or confine transactions to one partition. Then pick by family: document for aggregate-oriented apps (a catalog item, an order), key-value for caches/sessions/feature flags, wide-column for high-volume time-series and event logs, graph for relationship traversal (social, fraud rings). What it costs vs SQL: you lose ad-hoc joins, cross-entity atomicity, and the freedom to ask unanticipated questions; denormalization brings update fan-out. Large systems frequently go polyglot rather than forcing one engine to do everything — a relational store for users and money movement alongside a wide-column store for the high-volume timeline and a key-value store for sessions — accepting the operational cost of running several engines in exchange for letting each workload hit the store shaped for it.

The other named alternative — NewSQL (Google Spanner, CockroachDB, Vitess-on-MySQL): horizontal write scale and SQL and distributed ACID. It resolves the dilemma — at the cost of higher write latency (cross-region consensus / two-phase commit) and operational complexity. Reach for it only when you truly need both relational semantics and multi-node write scale.

Decide: Choose relational when correctness across entities and query flexibility matter more than horizontal write scale. Prefer a specific NoSQL family when a single known access pattern at massive scale matters more than joins and cross-entity transactions. Choose NewSQL only when you provably need both.

Takeaways


Re-authored and deepened for this guide. Draws on Martin Kleppmann, Designing Data-Intensive Applications (ch. 2–3, data models & storage engines); the MongoDB and Apache Cassandra data-modeling documentation; and the Amazon Dynamo paper (DeCandia et al., 2007). Complements page 002, "SQL vs NoSQL," with the data-model mechanics and family-level access patterns.

🤖 Don't fully get this? Learn it with Claude

Stuck on SQL vs NoSQL (2)? Open Claude, copy a block below, and it'll teach you this exact concept — visually and interactively.

🎨 Explain it visually

Build the mental picture, not memorization.

I just read a lesson on **SQL vs NoSQL (2)** (System Design) and want to truly understand it. Explain SQL vs NoSQL (2) from first principles using ONE vivid real-world analogy and a visual mental model — draw it as ASCII art or a clear step-by-step diagram — with a concrete example using real numbers. Then ask me one question to check I got the mental picture, and wait for my reply. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🤔 Walk me through it (interactive)

Socratic — adapts to where you're stuck.

Teach me **SQL vs NoSQL (2)** interactively. Ask me ONE guiding question at a time, wait for my answer, and adapt to my confusion — build the idea with me step by step instead of explaining it all at once. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧪 Quiz me & fix my gaps

Active recall exposes what you missed.

Quiz me on **SQL vs NoSQL (2)** with 5 questions, easy to tricky, ONE at a time. Tell me if each answer is right; at the end, explain clearly what I got wrong and why. If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.
🧠 Make it stick

Intuition + hook + flashcards for long-term memory.

Help me remember **SQL vs NoSQL (2)** for the long term: give the one-sentence intuition, a memorable hook/mnemonic, a tiny worked example, and 3 active-recall flashcards (Q -> A). If you're unsure or a claim isn't standard, say so and reason from first principles instead of guessing.

📝 My notes