S044 · NoSQL Landscape — KV, Document, Column, Graph
Four families, one truth: NoSQL means giving up something you had in Postgres for something you needed more. Learn which one to reach for and — more importantly — when not to.
🎯 Explain the four NoSQL families in one paragraph each, name a canonical database for each, and pick the right one (or reject NoSQL entirely) for a given workload.
Why this session exists
"NoSQL" is one of the most misleading terms in engineering — the databases in it share almost nothing except not being Postgres. There are actually four wildly different families (key-value, document, column-family, graph), each solving a different problem, each with a different failure mode. Half of every "we should use MongoDB" argument you've ever heard was really "we don't know Postgres well enough yet." The other half was solving a real problem MongoDB is genuinely good at. This session teaches you to tell them apart.
- Name the four NoSQL families and one canonical database per family.
- Explain in one sentence what each family gives up to gain what.
- Reject ‘we need NoSQL because scale’ as a default answer and ask three follow-ups.
- Read a Cassandra data model and predict which queries it can and cannot answer efficiently.
- Sketch when a graph database beats a relational one — and when it doesn't.
Prerequisites
- S042 — Transactions & ACID (so you know what most NoSQL is relaxing).
- S043 — Query Planning (contrast with NoSQL's ‘denormalise for queries’ approach).
(a) Intuition · 5 min
Imagine your kitchen has four separate storage rooms. The KV room is 10 000 identical numbered lockers — instant lookup by number, but you can only ask "what's in locker 743?" and nothing else. The document room has folders where each folder can hold anything (a recipe, a receipt, a photo) and you can search inside them. The column room is a warehouse of pallets where every item on a pallet has the same schema, indexed by which shelf/aisle. The graph room is a wall of pinned photos with strings between them — you don't search photos, you follow strings from photo to photo.
Each room is genuinely better than the others at exactly one thing. Choosing the wrong room means your kitchen is slow.
KV = Redis, DynamoDB, Memcached — one key → one blob. Sub-millisecond, dumb, massive scale. Session tokens, feature flags, cache.
Document = MongoDB, CouchDB, Firestore — one key → a JSON tree you can query into. Great for objects with variable shape (product catalogues, CMS content).
Column-family = Cassandra, ScyllaDB, HBase, Bigtable — one row-key + column-key → cell. Predictable-shape, write-heavy, tunable consistency. Time-series, activity feeds, IoT.
Graph = Neo4j, JanusGraph, Amazon Neptune, TigerGraph — nodes + edges + properties. Optimised for "friends of friends of friends" style multi-hop traversal. Fraud rings, social graphs, knowledge graphs.
The trade you're making, per family
- KV — gives up rich queries. Gains ~O(1) lookup at any scale.
- Document — gives up cross-document joins and strict schema. Gains flexible per-doc shape and easy horizontal partitioning by document key.
- Column-family — gives up JOINs, secondary-index-first querying, and cross-partition transactions. Gains linear scale for known query patterns.
- Graph — gives up horizontal partitioning at will (graphs are hard to shard). Gains multi-hop traversal that would kill a relational database.
A quick history so you know why the world looks like this
- 1979Relational becomes standardIBM System R, Oracle v2 ship. SQL wins for 25 years.
- 2003Google Bigtable paperColumn-family for petabyte-scale sparse tables. Inspires HBase, Cassandra.
- 2007Amazon Dynamo paperEventually-consistent KV over consistent hashing. Inspires Riak, Cassandra's replication.
- 2009MongoDB, Cassandra, Redis all in production‘NoSQL’ term catches on. Overpromises begin.
- 2012Neo4j hits 1.0 GAProperty graphs become mainstream; Cypher query language ships.
- 2015NewSQL emergesSpanner, CockroachDB, YugabyteDB — ‘we want SQL + horizontal scale + ACID’.
- 2022‘Postgres is enough’ meme winsPostgres 15 ships JSONB, logical replication, partitioning. Kills 70 % of the NoSQL use case.
(b) Visual walkthrough · 15 min
The four families side by side
The mental model to hold
Model the world, query anything.
- Normalize into 3NF then JOIN as needed
- ACID by default
- Vertical scaling — one big box
- Best when queries evolve over time
One key, one blob, blazing fast.
- No queries, only GET/SET/DEL
- Sub-ms latency at 100k+ QPS per node
- Great for cache, session, rate-limit
- Bad when you need any secondary lookup
One JSON blob per business object.
- Query inside the JSON with rich operators
- Sharded by document key
- Weak on multi-document txns until 4.2
- Best when each object stands alone
Wide-row, write-optimised, tunable consistency.
- Model per query: one table per query
- Linear write scale; every write is a local append
- No JOINs, no ad-hoc queries
- Best when write volume >> read variety
Nodes + typed edges + properties.
- Cypher language: MATCH (a)-[:KNOWS]->(b)
- Constant-time traversal per hop
- Hard to shard; usually single-master
- Best when hops > 2 dominate the query
Cassandra data modelling: query-first, not entity-first
e.g. ‘get last 20 messages between Alice and Bob’. This is the schema driver, not the entity list.
Denormalise ruthlessly. Same fact may live in 3 tables — writes go to all 3.
Partition key = the ‘folder’ that must live on one node. Cluster key = the ‘sort order within folder’.
Cassandra loves writes. Rewriting the same fact 3× is cheap. Rewriting queries after schema is set is what's expensive.
"NoSQL means schemaless — you can just throw documents in and figure out the structure later."
There is no such thing as schemaless data, only schema-on-read. The schema still exists; you have moved it from the database into every application that reads the data. Instead of one declaration the engine enforces, you now have an implicit schema duplicated across N services, enforced by none of them, and versioned by nobody.
Because the early experience is genuinely better: no migrations, no ALTER TABLE, ship features fast. The cost is deferred, not avoided, and it arrives as defensive code — every reader checking whether a field exists, handling three historical shapes of the same document, and guessing what a missing value means. By then the data is in production and there is no single place to fix it, because there is no single place at all.
The schema did not disappear; it moved into the reader:
# Three generations of the same document, all live in the collection
{'user_id': 1, 'name': 'A'} # v1
{'user_id': 2, 'name': 'B', 'email': 'b@x.com'} # v2
{'user_id': 3, 'profile': {'name': 'C', 'email': 'c@x.com'}} # v3 - restructured
# Every reader now carries the schema, forever:
def get_email(doc):
if 'profile' in doc:
return doc['profile'].get('email')
return doc.get('email') # may be absent - and is that 'unknown' or 'none'?
# In SQL this is one migration and one NOT NULL constraint,
# enforced once, for every reader, permanently.Why does the CAP theorem force a choice, and why is "CP versus AP" a misleading way to describe real systems?
- 1A distributed system holds copies of data on multiple nodes connected by a network.forced by · replication is the only way to survive a machine failure and the only way to scale reads beyond one machine
- 2Networks partition — packets are dropped, links fail, nodes are unreachable. This is not a rare event to be engineered away; it is a permanent property.forced by · any physical network can fail, and at scale something is always failing somewhere
- 3During a partition, a node receiving a write cannot confirm that its peers have it, and a node receiving a read cannot confirm it has the latest value.forced by · coordination requires communication, and communication is exactly what has failed
- 4So that node has precisely two choices: answer with possibly-stale or unreplicated data (available, not consistent), or refuse to answer (consistent, not available).forced by · there is no third option — respond or do not respond
- 5Therefore P is not a choice you make; it is a condition imposed on you. The only real choice is what to do during a partition: C or A.forced by · you cannot opt out of the network failing
Therefore "pick two of three" is a poor summary — you must tolerate partitions, so you are picking one of two, and only during the partition.
And note what this predicts: the far more useful framing is PACELC — during a Partition choose A or C, Else (normal operation) choose Latency or Consistency. That second clause is the one that governs your system 99.9% of the time, and CAP says nothing about it. Every quorum setting you tune — W + R > N, read-your-writes, bounded staleness — is a point on the latency/consistency curve during normal operation, which is where you actually live.
Do not choose a datastore by its category label. Choose it by the access pattern it makes cheap, because every store is fast at exactly one shape of access and mediocre or terrible at the rest.
Key-value is a hash map: instant if you know the key, useless otherwise. Document is a key-value whose values you can index into: good when one document is one aggregate you read whole. Wide-column is a sorted map on a partition key: brilliant for time-ordered rows within a partition. Graph is precomputed adjacency: cheap multi-hop traversal that would be n self-joins in SQL. Relational is the general-purpose one: it does everything adequately and nothing optimally.
- Model the queries first, then the data. In NoSQL, denormalisation and duplication are the design, not a compromise — you store data once per access pattern.
- A partition key that concentrates traffic creates a hot partition, and no amount of cluster size fixes it. Key design is the dominant performance decision.
- "Eventually consistent" has no bound unless the system gives you one. Ask what the staleness window actually is and whether read-your-own-writes is guaranteed.
- Losing joins and multi-key transactions is the real cost, and it is paid in application code — the complexity does not vanish, it relocates.
Fire this model the moment you see: a proposal to migrate to NoSQL "for scale" without a stated access pattern · a single-node relational database at its write ceiling · time-series or event data keyed by entity · a session/cache store · a recommendation or fraud problem that is fundamentally about relationships · a document that is always read and written whole.
A service needs to scale past what one relational instance can handle. Shard the relational database, adopt a NoSQL store, or scale up and optimise?
The order matters: optimise, then shard, then adopt a specialised store — and only move down the list when you have measured that the previous step is exhausted. A great deal of NoSQL adoption is a response to an unindexed query, and migration is a vastly more expensive fix than an index.
What actually happens in mature systems is polyglot persistence: relational as the system of record because constraints and transactions are worth real money, with specialised stores alongside it for the specific patterns that justify them. The cost of that is keeping them in sync, which is why change data capture and event streaming exist — and why the next few sessions are about exactly that.
(c) Hands-on · 25 min
A minimum viable feel of all four families using local Docker. Save as nosql_tour.sh.
What each block does
Anatomy of the script
Extend the Cassandra section with a second table that lets you query "all messages sent by Alice, most recent first":
CREATE TABLE demo.messages_by_sender (
sender TEXT,
sent_at TIMESTAMP,
chat_id UUID,
body TEXT,
PRIMARY KEY ((sender), sent_at)
) WITH CLUSTERING ORDER BY (sent_at DESC);Then modify the app to INSERT into BOTH tables on every message. That is idiomatic Cassandra — writes are cheap, reads are the constraint.
(d) Production reality · 15 min
Discord's first message store was MongoDB. It worked to ~100 M messages, then the working set exceeded RAM and reads slowed by 10×. Adding indexes didn't help because the queries were "last N messages in channel X" — Mongo's B-tree wasn't optimised for that access pattern.
They rewrote to Cassandra with schema PRIMARY KEY ((channel_id, bucket_id), message_id), where bucket_id groups messages into ~10-day windows. Every "read last N in channel" hits one partition, sequentially. They now handle billions of messages per day on the same cluster shape.
Uber outgrew a single Postgres cluster for trip data. They moved to MySQL with a homegrown sharding layer (‘Schemaless’), then later to a mix of MySQL + Cassandra + Redis, each for a different access pattern.
The lesson wasn't ‘Postgres bad’ — it was ‘monolithic DB bad at Uber scale’. They ended up with 4 different databases in production, each tuned to a workload. The 2020 refactor unified some of these on YugabyteDB.
The famous case in the opposite direction: Stack Overflow serves the entire site — questions, answers, votes, users, tags — from a single SQL Server (and mirror). No sharding, no NoSQL, no microservices. It handles 6 000 requests/second on well-tuned hardware.
Nothing to fix. The lesson: with careful indexes, connection pooling, and a good caching layer (Redis), a single well-run relational DB serves an internet-scale site. NoSQL was not required.
Where this shows up in the rest of the plan
(e) Recall + stretch · 10 min
Explain-out-loud test
- The four NoSQL families in 60 seconds — name, canonical DB, what it gives up.
- ‘Why not just Postgres?’ — the JSONB argument in one sentence.
- Cassandra's core rule — ‘one table per query’ and why.
What comes next
Hub: The 6-Month Learning Plan
Part of a 130-session evergreen learning series. Session structure: intuition → visual → hands-on → production war stories → recall. Duration: 90 minutes.