Search Tech Journey

Find topics, journeys and posts

6-month learning plan44 / 130
back to blog
data engineeringbeginner 15m read

S044 · NoSQL Landscape — KV, Document, Column, Graph

When relational isn't the answer.

Module M04: Databases & SQL · Session 44 of 130 · Track: DE 🧭

What you'll be able to do after this session

  • Explain NoSQL Landscape to a friend in one minute, out loud, no notes.
  • Recognise it when you see it in real code / systems / papers.
  • Complete the hands-on exercise at the end without looking up help.

Prerequisites

If you can't answer these in 30 seconds each, redo the linked session first:


Pre-read (skim before the session)


(a) Intuition · 5 min

The one-paragraph mental model. Why this concept exists, what problem it solves, and the analogy that makes it stick.

In one sentence: When relational isn't the answer.

For 40 years, "database" meant "relational database" — tables, rows, SQL, ACID. Around 2007, sites like Google, Amazon, and Facebook started hitting a wall: their data no longer fit on one machine, some of it wasn't naturally tabular, and the strict consistency guarantees of SQL were costing them uptime. So they invented (or resurrected) a family of non-relational databases collectively branded NoSQL — which turned out to be a terrible name because it's not really "no SQL," it's "not-only-SQL," and the four families are very different from each other.

Think of it like transport. For 100 years, "vehicle" mostly meant "car." Then people said, "what if I need to carry 40 tons?" → truck. "What if I need to fly?" → plane. "What if I only need to go 500m to the shop?" → bicycle. A car (RDBMS) is a great default. But if your use case is fundamentally shaped like a graph, or a giant hash map, or a firehose of time-series metrics, the relational shape starts fighting you. NoSQL databases are the trucks, planes, and bicycles.

The gotcha to carry forever: choosing NoSQL to escape schema is almost always a mistake. Every serious NoSQL system develops informal schemas over time; the difference is that you enforce them in application code, not the database. Choose NoSQL because of a scale, shape, or latency requirement that relational cannot meet — never because "schemas are annoying." And know that CAP theorem is real: under a network partition, every distributed database picks either consistency or availability. Read the docs. Test it. Don't assume.


(b) Visual walkthrough · 15 min

Diagrams, worked examples, step-by-step visuals.

The four main NoSQL families, with when to reach for each and who uses them in the wild:

FamilyData shapeQuery styleExample DBsSweet-spot use case
Key-Valuekey → opaque blobGET, SET, DEL — no queriesRedis, DynamoDB, Riak, MemcachedSession cache, feature flags, rate limits
DocumentJSON-like documents in "collections"Query by any field, nested paths, secondary indexesMongoDB, Couchbase, Firestore, DocumentDBProduct catalogs, user profiles, CMS
Column-family (wide-column)Rows keyed by row-key, sparse columns per row, sorted on diskRange scans by row-key + columnCassandra, ScyllaDB, HBase, BigtableTime-series, IoT telemetry, event logs
GraphNodes + edges with propertiesTraversal: "friend of friend of X"Neo4j, Amazon Neptune, JanusGraphSocial networks, fraud rings, recommendation

Worked example — pick the store for each requirement:

CAP in one paragraph: in a distributed database, when the network splits (P is inevitable), you must choose. CP systems (MongoDB with majority reads, HBase, Zookeeper) refuse writes on the minority side — they stay consistent but lose availability. AP systems (Cassandra, DynamoDB in eventual mode, Riak) accept writes on both sides and reconcile later — they stay available but may serve stale reads. Redis is CP-ish (with Sentinel/Cluster), single-node Postgres is CA (no P because it's not distributed). If someone says "we run CA in a distributed system," they are wrong.

Anti-example — when NOT to use NoSQL: "I want flexible schemas for my e-commerce app so I'll use MongoDB." Six months in, you realize orders have relationships to customers, products, payments, refunds, and shipments — that's a graph of foreign keys, i.e. exactly what SQL was built for. You end up doing joins in application code, badly. Use Postgres with a JSONB column for the truly variable fields.


(c) Hands-on · 20 min

Code you type and run. Not pseudo-code — real, runnable, copy-pasteable.

Fire up three databases side by side and feel the difference.

# 1. Redis (KV) and Mongo (document) in one docker-compose
cat > /tmp/nosql-lab.yml <<'YML'
services:
  redis:
    image: redis:7
    ports: ["6379:6379"]
  mongo:
    image: mongo:7
    ports: ["27017:27017"]
YML
docker compose -f /tmp/nosql-lab.yml up -d
sleep 4
 
# 2. Redis: sub-ms KV
docker exec -i $(docker ps -qf ancestor=redis:7) redis-cli <<'R'
SET session:abc123 "{"user_id":42,"logged_in":true}"
GET session:abc123
EXPIRE session:abc123 3600
TTL session:abc123
INCR page_views:home
INCR page_views:home
INCR page_views:home
GET page_views:home
R
 
# 3. MongoDB: JSON documents with nested fields
docker exec -i $(docker ps -qf ancestor=mongo:7) mongosh --quiet <<'M'
use shop;
db.products.insertMany([
  {sku:"BOOK-001", name:"DDIA",   price:2500, tags:["book","tech"],
   variants:[{color:"blue",stock:12},{color:"red",stock:0}]},
  {sku:"MUG-001",  name:"Chinni Mug", price:499, tags:["merch"],
   variants:[{color:"white",stock:88}]},
]);
db.products.find({tags:"tech"}).pretty();
db.products.find({"variants.color":"blue","variants.stock":{$gt:0}}).pretty();
db.products.updateOne({sku:"BOOK-001"}, {$inc:{"variants.0.stock":-1}});
db.products.createIndex({sku:1}, {unique:true});
db.products.getIndexes();
M
 
# 4. Compare: same "get product by sku" in each store
docker exec $(docker ps -qf ancestor=redis:7) redis-cli SET "prod:BOOK-001" '{"name":"DDIA","price":2500}'
docker exec $(docker ps -qf ancestor=redis:7) redis-cli --latency-history -i 1 GET prod:BOOK-001 &
# ^C after 5 seconds — you'll see sub-millisecond latencies
 
# Cleanup:
docker compose -f /tmp/nosql-lab.yml down

Observe (checklist):

  1. Redis INCR is atomic and single-digit-microsecond — perfect for counters.
  2. Redis has NO query language beyond commands; you cannot say "find sessions where user_id=42."
  3. MongoDB queries nested arrays with dot notation (variants.color) — SQL would need jsonb_array_elements.
  4. MongoDB requires you to createIndex explicitly, just like SQL. There is no free lunch.
  5. Redis latency histogram shows most calls under 1 ms locally; that's what "cache" means.

Try this modification: load 100,000 products into Mongo (for (let i=0;i<100000;i++) db.products.insertOne({sku:"P"+i,price:Math.random()*1000})), then query without and with an index on price. Use db.products.find({price:{$lt:10}}).explain("executionStats") — same EXPLAIN idea as SQL.


(d) Production reality · 10 min

How this shows up in real systems, gotchas, war stories, common bugs.

Every 6 months there's a post-mortem where a team picked MongoDB for a use case that was inherently relational, spent two years working around it, and eventually migrated back to Postgres. The most famous: Uber moved from Postgres to MySQL in 2016 (documented in the "Why Uber Engineering Switched from Postgres to MySQL" post — a great read, and a great counterpoint to blindly following FAANG choices). Segment publicly migrated from MongoDB to Postgres in 2019 — they wrote a 30-page post-mortem. Lesson: your requirements, not the vendor's marketing, decide.

Cassandra and DynamoDB (wide-column / KV at planet scale) reward you if you design your schema around your access patterns first, and punish you brutally if you don't. Netflix's principle: "there is no SELECT * in Cassandra." Every query must specify the partition key. If you find yourself doing scatter-gather queries, you have modelled it wrong — redesign, don't add secondary indexes to paper over it. Discord's move from Cassandra to ScyllaDB in 2022 shaved P99 latencies from 40ms to 15ms without changing the data model — same paradigm, faster implementation.

Redis is beloved but dangerous when misused: it's an in-memory store, and RDB/AOF persistence is best-effort. Do not use Redis as your source of truth for anything you cannot lose. Use it for cache, session, rate-limit, leaderboards, ephemeral queues. GitHub had a famous 2018 outage caused by a Redis failover with 40 seconds of lost writes.

Finally: multi-model is real but shallow. Postgres has JSONB (does 80% of what document DBs offer), full-text search (does 60% of Elasticsearch for small corpora), pgvector (does 70% of dedicated vector DBs at small scale), and PostGIS (does 100% of geospatial). Before adopting a new NoSQL system, ask "can Postgres do this well enough?" The answer is yes surprisingly often.


(e) Quiz + exercise · 10 min

  1. Name the four main NoSQL families and one representative database for each.
  2. What is the CAP theorem, and which two properties does Cassandra prioritize under a partition?
  3. Give one concrete anti-pattern of using MongoDB for something SQL would handle better.
  4. Why does "flexible schema" almost never justify choosing NoSQL on its own?
  5. What is the "one big rule" when designing a Cassandra table, and why does violating it hurt?

Stretch (connects to S042 — Transactions): MongoDB gained multi-document ACID transactions in v4.0 (2018). Why did it take so long, what is the performance cost, and does adding them make MongoDB "just as good as Postgres for transactional workloads"? Argue both sides.


Explain-out-loud test

If you can't teach these 3 things to a friend without notes, redo the session:

  1. What is NoSQL Landscape? (one sentence, no jargon)
  2. When would you use it? (one concrete example)
  3. When would you NOT use it? (one anti-pattern)

What comes next


Part of a 130-session evergreen learning series. Session structure: (a) intuition · (b) visual walkthrough · (c) hands-on · (d) production reality · (e) quiz + exercise. Duration: 60 minutes.

More from M04 · Databases & SQL

all modules →
  1. 035The Relational Model — Tables, Keys, Normalisation
  2. 036SQL Basics — SELECT, WHERE, ORDER BY, LIMIT
  3. 037Joins — INNER, LEFT, RIGHT, FULL, Anti-Join
  4. 038Aggregations — GROUP BY, HAVING, Subqueries
  5. 039Window Functions — the Game-Changer
  6. 040CTEs & Recursive Queries