DBMS

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

  1. Four Families
  2. Key-Value Stores
  3. Document Stores
  4. Wide-Column Stores
  5. Graph Databases
  6. Comparing Them
  7. 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 10
Where 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 FILTERING
QUERY-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-ValueDocumentWide-ColumnGraphRelational
Query by contentNoYesPartition-scopedYesYes
JoinsNoLimitedNoTraversalYes
TransactionsSingle keyImprovingLimitedYesYes
Ad-hoc queriesNoYesNoYesYes
Horizontal scaleExcellentGoodExcellentHardHard
Schema enforcedNoOptionalPartialOptionalYes
Best atCache, sessionsAggregatesTime-series at scaleTraversalEverything 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

  1. When should a child record be embedded in a document rather than referenced, and what goes wrong if you embed an unbounded collection?
  2. Why does a Cassandra query without the partition key get refused rather than merely being slow?
  3. 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