Beyond Relational
The NoSQL Families
KEY-VALUE opaque blob, addressed by one key
JrCodex·8 min read
Jr Codex DBMS Notes
Level: Intermediate Prerequisites: Chapter 1: Why NoSQL Appeared Time to complete: ~20 minutes
Table of Contents
- Four Families
- Key-Value Stores
- Document Stores
- Wide-Column Stores
- Graph Databases
- Comparing Them
- Summary & Next Steps
1. Four Families
Organised by Data Model
─────────────────────────────────────────
KEY-VALUE opaque blob, addressed by one key
Redis, DynamoDB, etcd
DOCUMENT self-describing nested records
MongoDB, CouchDB, Firestore
WIDE-COLUMN rows with dynamic column sets,
partitioned by key
Cassandra, HBase, ScyllaDB
GRAPH nodes and edges, traversed
Neo4j, Neptune, ArangoDB
Each answers a different question well and the
others badly.
─────────────────────────────────────────
2. Key-Value Stores
The Model
─────────────────────────────────────────
GET(key) ──► value
PUT(key, value)
DELETE(key)
The value is OPAQUE — the store does not
interpret it. No querying by content, no
secondary indexes in the simplest designs.
─────────────────────────────────────────
import redis
r = redis.Redis()
r.set("session:a4f9", '{"user_id": 42, "role": "admin"}', ex=3600) # TTL, built in
print(r.get("session:a4f9"))
r.incr("page:views:home") # ATOMIC counter — Module 7, Chapter 5's pattern
r.zadd("leaderboard", {"asha": 880, "meera": 910})
print(r.zrevrange("leaderboard", 0, 9, withscores=True)) # top 10Where It Wins
─────────────────────────────────────────
✓ SESSIONS — keyed by id, expire naturally
✓ CACHING — the canonical use
✓ RATE LIMITING — atomic counters with TTL
✓ QUEUES and leaderboards (Redis's data types)
✓ FEATURE FLAGS and configuration
✗ any query that is not "by this exact key"
✗ relationships between values
✗ reporting or analytics
─────────────────────────────────────────
The Honest Framing
─────────────────────────────────────────
A key-value store is usually a COMPLEMENT to a
relational database, not a replacement.
Redis in front of PostgreSQL is one of the most
common and sensible architectures in existence.
Redis as your system of record usually is not.
─────────────────────────────────────────
3. Document Stores
The Model
─────────────────────────────────────────
Records are self-describing documents — JSON or
BSON — that may nest. Each document may have a
different shape.
Queryable BY CONTENT, unlike key-value, and
indexable on nested fields.
─────────────────────────────────────────
// One document holds what would be several tables.
{
_id: ObjectId("..."),
name: "Asha",
email: "asha@example.com",
addresses: [ // embedded — no join needed
{ type: "home", city: "Pune", postcode: "411001" },
{ type: "work", city: "Mumbai" } // note: no postcode. Different shape.
],
orders: [ { id: 991, total: 45.00 } ]
}db.customers.find({ "addresses.city": "Pune" }); // query into nested arrays
db.customers.createIndex({ "addresses.city": 1 });
db.customers.updateOne({ _id: id }, { $push: { orders: newOrder } });Embed or Reference — the Central Design Question
─────────────────────────────────────────
EMBED when the child:
- is always read WITH the parent
- does not grow without bound
- is not referenced from elsewhere
──► order line items inside an order
REFERENCE when the child:
- is queried independently
- is large or unbounded
- is shared by many parents
──► a product, referenced by many orders
This is Module 4's normalisation question in
different clothing — and getting it wrong has the
same consequences: an unbounded embedded array
eventually exceeds the document size limit.
─────────────────────────────────────────
Where It Wins
─────────────────────────────────────────
✓ content management, catalogues, user profiles
✓ genuinely varying shapes per record
✓ rapid iteration without migrations
✓ read patterns that fetch one whole aggregate
✗ many-to-many relationships
✗ reporting across documents
✗ anything needing cross-document transactions
(supported now, but the model fights you)
─────────────────────────────────────────
4. Wide-Column Stores
The most misunderstood family, and the one whose design most rewards understanding.
The Model
─────────────────────────────────────────
PARTITION KEY decides which node stores the row
CLUSTERING KEY decides the ORDER of rows within
the partition
A partition is a unit of storage and retrieval.
Rows in one partition are physically together and
sorted.
─────────────────────────────────────────
-- Cassandra. Note the primary key structure — this IS the design.
CREATE TABLE readings (
sensor_id UUID,
reading_at TIMESTAMP,
value DOUBLE,
PRIMARY KEY ((sensor_id), reading_at) -- partition key, clustering key
) WITH CLUSTERING ORDER BY (reading_at DESC);
-- FAST: one partition, a contiguous sorted range within it.
SELECT * FROM readings
WHERE sensor_id = ? AND reading_at > '2026-09-01'
LIMIT 100;
-- REFUSED: no partition key means scanning every node.
SELECT * FROM readings WHERE value > 100; -- error, unless ALLOW FILTERINGQUERY-FIRST DESIGN
─────────────────────────────────────────
In a relational database you model the DATA, then
write whatever queries you need (Module 4).
In a wide-column store you start from the
QUERIES, and create one table per query pattern —
duplicating data across them deliberately.
Need readings by sensor AND by region? That is
TWO tables, both written on every insert.
This is not a workaround. It is the model: writes
are cheap and predictable, so you trade write
amplification for guaranteed read performance.
─────────────────────────────────────────
Where It Wins
─────────────────────────────────────────
✓ time-series and event logs at very large scale
✓ enormous write volume
✓ predictable single-partition reads
✓ multi-datacentre with no single primary
✗ ad-hoc queries — genuinely cannot
✗ joins, aggregations across partitions
✗ anything where query patterns are still
changing
─────────────────────────────────────────
5. Graph Databases
The Model
─────────────────────────────────────────
NODES with properties, connected by typed,
directed EDGES that also have properties.
Traversal is a FIRST-CLASS operation, and it is
O(edges followed) — independent of total graph
size, because each node stores pointers to its
neighbours.
─────────────────────────────────────────
// Neo4j's Cypher. The syntax mirrors the shape of the query.
MATCH (a:Person {name: 'Asha'})-[:FOLLOWS*2..3]->(suggestion:Person)
WHERE NOT (a)-[:FOLLOWS]->(suggestion) AND a <> suggestion
RETURN suggestion.name, COUNT(*) AS mutual
ORDER BY mutual DESC LIMIT 10;Why This Beats SQL Here
─────────────────────────────────────────
"Friends of friends of friends" in SQL is a
three-way self-join, or a recursive CTE
(Module 3, Chapter 5).
Each hop is an INDEX LOOKUP whose cost grows with
the table size. Six hops is often impractical.
In a graph database each hop follows a stored
pointer. Cost depends on the neighbourhood
traversed, not on how many people exist.
This is Module 1, Chapter 2's "relational is bad
at graph traversal", and the concrete reason.
─────────────────────────────────────────
Where It Wins
─────────────────────────────────────────
✓ social networks, recommendations
✓ fraud rings, money-flow analysis
✓ dependency and impact analysis
✓ knowledge graphs
✓ shortest path, centrality, community detection
✗ aggregate reporting over all nodes
✗ high-volume simple writes
✗ anything where relationships are shallow —
a one-hop join does not need this
─────────────────────────────────────────
6. Comparing Them
| Key-Value | Document | Wide-Column | Graph | Relational | |
|---|---|---|---|---|---|
| Query by content | No | Yes | Partition-scoped | Yes | Yes |
| Joins | No | Limited | No | Traversal | Yes |
| Transactions | Single key | Improving | Limited | Yes | Yes |
| Ad-hoc queries | No | Yes | No | Yes | Yes |
| Horizontal scale | Excellent | Good | Excellent | Hard | Hard |
| Schema enforced | No | Optional | Partial | Optional | Yes |
| Best at | Cache, sessions | Aggregates | Time-series at scale | Traversal | Everything else |
The Pattern in That Table
─────────────────────────────────────────
Read the "ad-hoc queries" and "horizontal scale"
rows together.
They are almost inverted. Systems that scale out
best are those that gave up arbitrary querying —
because arbitrary queries require coordinating
across all the data, which is precisely what
distribution makes expensive.
That is the trade, stated as compactly as it can
be.
─────────────────────────────────────────
7. Summary & Next Steps
Key Takeaways
- Key-value stores answer only "by this exact key" and are usually a complement to a relational database rather than a replacement for it.
- Document stores turn Module 4's normalisation question into embed-or-reference, with the same failure mode when an embedded collection grows unbounded.
- Wide-column stores require query-first design: one table per access pattern, with data deliberately duplicated, because ad-hoc queries are genuinely impossible.
- Graph traversal follows stored pointers rather than repeated index lookups, which is why cost depends on the neighbourhood rather than the total data size.
Concept Check
- When should a child record be embedded in a document rather than referenced, and what goes wrong if you embed an unbounded collection?
- Why does a Cassandra query without the partition key get refused rather than merely being slow?
- Explain why "ad-hoc queries" and "horizontal scale" tend to be inversely related across these families.
Next Chapter
→ Chapter 3: CAP, Consistency and Sharding
Jr Codex — 1-on-1 Personalized Coaching | Back to Module Index | Back to DBMS Index