NoSQL databases: key-value, document, wide-column and graph

One food-delivery app can use five databases: PostgreSQL for money, Redis for carts, MongoDB for menus, Cassandra for rider GPS and Neo4j for referrals. In this lesson you meet the four NoSQL families, compare them with SQL, watch a cache save a flash sale, design a Cassandra table around its query, and catch a cashback fraud ring with a graph.

Module 2 · Data and Storage⏱ 30 minBeginner → AdvancedRuns in your browser (Python)3 real-time scenariosNoSQL lab on your PC9 practice problems
1 The story

Five databases for one app?

Kabir draws TiffinGo’s architecture from their tech blog for his notes. He expects one database. He finds five: PostgreSQL, Redis, MongoDB, Cassandra and Neo4j. In college they learned only MySQL and normalisation.

TiffinGo appPostgreSQLorders, paymentsRediscarts, sessionsMongoDBkitchen menusCassandrarider GPS historyNeo4jreferral graph One app, five databases, each chosen for one job. myphatshala.in
Kabir

Sir, why five databases? Couldn’t they keep everything in one MySQL database with good indexes?

Arjun Sir

For a small app, yes. But imagine two lakh carts updating every second during a sale, menus whose fields differ for every kitchen, and every rider sending a location every five seconds. A single relational database would struggle with some of these jobs, or cost a fortune.

Riya

So the others are “NoSQL” databases? Does that mean they don’t use SQL at all?

Arjun Sir

“NoSQL” today means not only SQL. They drop some relational features, like fixed schemas or joins, to gain others: massive scale, flexible documents, millisecond look-ups, or easy relationship queries. There are four big families: key-value, document, wide-column and graph.

Kabir

And the orders stay in PostgreSQL?

Arjun Sir

Exactly. Money needs strict transactions, which relational databases do best. Choosing the right store for each job is called polyglot persistence. Today you’ll see a cache save a flash sale, design a Cassandra table around its queries, and catch a fraud ring with a graph.

2 Definition

What is a NoSQL database?

Formal definition (write this in exams)

A NoSQL (“not only SQL”) database is a non-relational database that stores data in models other than fixed relational tables, typically to achieve horizontal scalability, flexible schemas and high performance for specific access patterns. The four main families are key-value stores, document stores, wide-column stores and graph databases. Many NoSQL systems relax strict ACID guarantees in favour of availability and partition tolerance (BASE), often offering tunable or eventual consistency.

In simple words

NoSQL databases are databases that don’t force every record into the same table shape. Some store simple key → value pairs, some store whole JSON documents, some store huge numbers of rows spread across many machines, and some store connections between things. Each one is very good at one kind of job.

Words you’ll meet in this lesson

Key-value pairA unique key and its value: otp:9876 → 482913.
DocumentA self-contained JSON-like record that can have nested fields and lists.
Partition keyThe part of a key that decides which machine stores the row.
Clustering keyIn wide-column stores, the order of rows inside a partition (for example by time).
Node / edgeIn a graph: a thing (account, phone) and a relationship between two things (USES, REFERS).
DenormalisationCopying data into several places so each read needs no joins.
TTL (time to live)An automatic expiry time on a key or row.
Polyglot persistenceUsing several kinds of databases in one system, each for what it does best.

The four families and well-known examples

Key-valueRedis, Amazon DynamoDB, Memcached, etcd · caches, sessions, carts, rate limits, OTPs
DocumentMongoDB, Couchbase, Google Firestore, Azure Cosmos DB · catalogues, user profiles, content, app data
Wide-columnApache Cassandra, Apache HBase, ScyllaDB, Google Bigtable · time series, IoT, event logs, messaging
GraphNeo4j, Amazon Neptune, TigerGraph · fraud rings, recommendations, social networks, knowledge graphs

NoSQL at three levels

Pick your level. New to this? Start at Beginner and climb.

Beginner Four kinds of storage cupboards

  • Key-value is a cloakroom: give a token (key), get your item (value) instantly. Example: Redis storing cart:U9 → {thali: 1}.
  • Document is a file folder per customer: each folder holds a whole form with nested sections, and two folders can have different fields. Example: MongoDB storing a kitchen with its whole menu inside.
  • Wide-column is a giant register split into many books by a key: each rider’s book holds his location pages in time order. Example: Cassandra storing GPS pings per rider.
  • Graph is a friendship map: dots (people, phones, accounts) connected by lines (shares, refers, pays). Example: Neo4j finding who is connected to whom.

Simple rule: money and strict rules → relational (SQL). Speed by key → key-value. Flexible records → document. Huge write streams → wide-column. Relationships → graph.

Intermediate Model data around your queries

  • Access patterns first: in NoSQL you list the questions the app will ask (“get a rider’s last 50 positions”) and shape the data to answer each one with a single look-up. Relational design normalises first and joins later.
  • Denormalisation: data is copied where it is read (a kitchen’s name stored inside each order document) to avoid joins, accepting extra storage and harder updates.
  • Embed or reference (documents): embed things read together and bounded in size (a menu inside a kitchen); reference things that grow without limit (all orders of a kitchen).
  • Partition key (wide-column, key-value at scale): decides which machine holds the data. A good key spreads load evenly and keeps one query on one partition.
  • TTL: many NoSQL stores can expire data automatically (carts after 30 minutes, OTPs after 5 minutes).

Advanced ACID vs BASE, tunable consistency and storage engines

  • ACID vs BASE: relational databases promise Atomicity, Consistency, Isolation, Durability. Many NoSQL systems chose BASE (Basically Available, Soft state, Eventually consistent) to stay available and fast across many machines. Modern MongoDB and DynamoDB do support multi-document transactions, at a cost.
  • Tunable consistency: Cassandra lets each query choose how many replicas must answer. With N replicas, if read replicas R plus write replicas W is greater than N, every read sees the latest write (more in Lesson 3.4).
  • Storage engines: B-trees (most relational databases) are good at reads and updates in place; LSM trees (Cassandra, HBase, RocksDB) turn writes into fast sequential appends and merge files in the background, which suits huge write volumes.
  • Hot partitions: one celebrity account or one city can overload a single partition. Fix with better keys or by splitting (bucketing) hot keys.
  • NewSQL: distributed SQL databases (Google Spanner, CockroachDB, YugabyteDB, TiDB) scale out like NoSQL while keeping SQL and ACID transactions.

Exam tip: Answer “types of NoSQL databases” with all four families: data model, example product, typical use, plus one line each on schema flexibility, horizontal scaling and BASE vs ACID. Add a short SQL vs NoSQL comparison table for full marks.

3 Types and ways to use it

Key-value, document, wide-column and graph

Key-value

A giant dictionary. Get or set by key in about a millisecond. Values are opaque blobs; no queries on their contents.

Fastest look-ups

Document

JSON-like documents with nesting and flexible fields. Query and index by fields inside the document.

Flexible records

Wide-column

Rows grouped into partitions by key and sorted inside each partition. Built for huge write volumes across many nodes.

Massive writes

Graph

Nodes and relationships stored directly. Multi-hop questions (“friends of friends”) are fast and natural.

Relationships

SQL vs NoSQL

Relational (SQL)NoSQL
Data modelTables with rows and columnsKey-value, document, wide-column or graph
SchemaFixed, defined in advanceFlexible; can differ per record
ScalingMostly vertical (bigger server); read replicasHorizontal (add nodes) by design
JoinsPowerful, central to designLimited or none; data denormalised
TransactionsStrong ACIDOften BASE / tunable; some support ACID
Design approachNormalise, then query anythingStart from the queries, shape data for them
Best forPayments, orders, accounting, reportsCaches, catalogues, events, time series, graphs

Same order, four models

FamilyHow order O1043 might be stored
Key-valueorder:O1043 → {"user": "U9", "total": 420, "status": "DELIVERED"}
Document{"_id": "O1043", "user": {"id": "U9", "name": "Riya"}, "items": [{"dish": "thali", "qty": 1}], "total": 420}
Wide-columnpartition user_id = U9, rows sorted by order_time DESC: O1043, O1012, O0987…
Graph(Riya)-[:PLACED]->(O1043)-[:FROM]->(Annapurna Kitchen)

NoSQL is not “better” than SQL. Most companies keep money in a relational database and add NoSQL stores around it. In interviews, always justify a NoSQL choice by the access pattern and scale, not by fashion.

4 Step by step

One order across five databases

  1. Key-value: Riya’s cart in Redis

    import time
    cache = {}
    def set_key(k, v, ttl):
        cache[k] = (v, time.time() + ttl)
    def get_key(k):
        v, exp = cache.get(k, (None, 0))
        return v if time.time() < exp else None
    set_key("cart:U9", {"thali": 1, "lassi": 2}, ttl=1800)
    set_key("otp:9876", "482913", ttl=-1)          # already expired
    print(get_key("cart:U9"), get_key("otp:9876"))
    {'thali': 1, 'lassi': 2} None
    A cart is read and written by one key, many times a minute, and is useless after half an hour. A key-value store with a TTL does exactly this in about a millisecond, and the expired OTP disappears by itself.
  2. Document: each kitchen is one document

    kitchens = [
        {"_id": "K11", "name": "Annapurna", "veg": True, "menu": [{"dish": "thali", "price": 220}]},
        {"_id": "K12", "name": "Spice Box", "veg": False, "menu": [{"dish": "biryani", "price": 280}], "halal": True},
    ]
    veg = [k["name"] for k in kitchens if k["veg"]]
    print("veg kitchens:", veg, "| K12 extra field halal:", kitchens[1].get("halal"))
    veg kitchens: ['Annapurna'] | K12 extra field halal: True
    The menu lives inside the kitchen document, so one read fetches the whole page. Kitchens can have extra fields (like halal) without changing a schema for everyone.
  3. Wide-column: rider pings partitioned by rider

    pings = {}   # partition key = rider, rows kept sorted by time
    for rider, t, lat in [("R7", 3, 18.52), ("R9", 1, 19.07), ("R7", 1, 18.50), ("R7", 2, 18.51)]:
        pings.setdefault(rider, []).append((t, lat))
        pings[rider].sort()
    print("R7 partition:", pings["R7"])
    print("latest for R7:", pings["R7"][-1])
    R7 partition: [(1, 18.5), (2, 18.51), (3, 18.52)] latest for R7: (3, 18.52)
    Each rider’s pings live together in one partition, kept sorted by time. “Latest position of R7” reads one partition, even when there are billions of pings in total spread over many nodes.
  4. Graph: who referred whom

    refers = {"Riya": ["Kabir", "Meera"], "Kabir": ["Arjun"], "Meera": [], "Arjun": ["Zoya"]}
    level1 = refers["Riya"]
    level2 = [p for f in level1 for p in refers[f]]
    print("referred by Riya:", level1, "| their referrals:", level2)
    referred by Riya: ['Kabir', 'Meera'] | their referrals: ['Arjun']
    Relationships are stored directly, so “referrals of my referrals” is a short walk. In SQL each extra hop is another self-join on a large table.
  5. Relational: the payment itself

    When Riya pays ₹420, the order row, the payment row and the wallet balance change together inside one PostgreSQL transaction: all or nothing.

    This is where ACID matters most. If the payment fails half-way, nothing is saved. That is why money stays in a relational database.
  6. Polyglot persistence

    One order touched five databases, each doing what it does best. Events from all of them also flow into the data lake for analytics.

    The cost: more systems to run, monitor and keep in sync. Teams add a new database only when an access pattern truly needs it.
5 See it happen

One app session: what Riya sees, and which database answers

Step through the six frames. Left: Riya using the app. Right: the database that serves each step, and the query it runs.

What Riya sees, and which database answers 6 steps
What you see Welcome back, RiyaHi Riya!Pune · KharadiAnnapurna KitchenThali₹220Lassi₹100Gulab jamun₹60Pure veg · 4.2 ★CartThali × 1Lassi × 2₹420PAYPaid₹420 · UPITrackingrider 1.2 km awayFor youYour friends ordered:Kabir · BiryaniMeera · DosaRefer & earn ₹50 myphatshala.in
Behind the screen Redis (key-value)GET session:7f3a… → {user: U9, city: Pune}session token from the phone is the key≈ 1 msMongoDB (document)find({_id: "K11"}) → name, rating, menu[…]whole kitchen page in one document read≈ 5 msRedis (key-value)HSET cart:U9 thali 1 lassi 2 · EXPIRE 1800every tap updates the cart key; expires in 30 min≈ 1 ms per updatePostgreSQL (relational)BEGIN; INSERT order; INSERT payment; COMMIT;all-or-nothing ACID transaction for moneysafe, consistentCassandra (wide-column)INSERT INTO pings (rider, ts, lat, lon) …partition = rider R77, rows sorted by time1 write per rider every 5 sNeo4j (graph)(Riya)-[:FRIEND]->(f)-[:ORDERED]->(dish)two hops through the relationship graph≈ 20 ms myphatshala.in
6 Real-world use

Real-time scenarios: NoSQL under pressure

Three live scenarios: a key-value cache that saves a flash sale, a Cassandra table designed around its query, and an advanced graph search that exposes a fraud ring.

Real-time scenario 1

Flash sale: a key-value cache saves the database

At midnight a sale starts and everyone opens the same few kitchen pages. Watch the cache-aside pattern take the load off the database.

Cache-aside with Redis 5 steps
Animated example AppserverRediscacheDatabasePostgreSQL00:00 · flash sale: 2 lakh menu reads per secondEvery page asks the database for the same hot menus.The database CPU hits 100%; pages take 3 seconds.AppserverRediscacheDatabasePostgreSQLmissread from database (≈ 80 ms under load)Cache-aside: check the cache firstFirst request: not in cache (miss), so read the databaseand store the result in Redis with a 60 s TTL.AppserverRediscacheDatabasePostgreSQLhitNext requests: cache hitThe next 59 seconds of requests are served from memoryin about 1 ms. The database barely notices.AppserverRediscacheDatabasePostgreSQLhitHit ratio 99%DB reads / s2,000cache reads / s1,98,000Only 1% of reads reach the database.AppserverRediscacheDatabasePostgreSQLhitPrice change? Invalidate the keyWhen a kitchen edits its menu, the app deletes that key,so the next read fetches fresh data. Short TTLs limit staleness. myphatshala.in
Real-time scenario 2

Designing a Cassandra table around its query

TiffinGo stores every rider’s GPS ping, crores per day. Support agents need “the last 50 positions of rider R77”. Watch a natural-looking table fail, and a query-first design succeed.

Partition key design 5 steps
Animated example Query: last 50 positions of rider R77Wrong design: PRIMARY KEY (city, ts)one partition per city, every rider mixed inPartitioning by city looked natural.Let’s see what happens.Partition sizes by cityMumbaiPuneThaneNashikNagpurSuratMumbai’s partition is huge and growing forever:one node does most of the work (a hot partition).And the query is slowscan Mumbai partition → filter rider = R77reads millions of rows to return 50The table was not designed for the question asked.Better: PRIMARY KEY ((rider_id, day), ts)R77·19R12·19R77·20R40·19R12·20R9·19Many small partitions of similar size (same scale asbefore), spread evenly. Rows sorted by ts DESC inside.The query is now one partition readSELECT … WHERE rider_id='R77' AND day='2026-10-19'LIMIT 50 → first 50 rows of one partitionMilliseconds, whatever the total data size.Design the key from the query: that is NoSQL modelling. myphatshala.in
Real-time scenario 3 · advanced

Catching a cashback fraud ring with a graph

A promotion gives ₹500 to every new user. Fraudsters create many accounts. Each account looks innocent alone; the fraud only shows up in the connections between them.

Graph traversal 5 steps
Animated example A1A2A3A4A5phone 98xx01UPI x@okdevice D7Accounts (blue) and the phones, UPI ids and devicesthey use (violet). A5 shares nothing.A1A2A3A4A5phone 98xx01UPI x@okdevice D7A1 claims a ₹500 new-user cashback.Is A1 really a new user?A1A2A3A4A5phone 98xx01UPI x@okdevice D7Hop 1–2: A1 shares a phone number with A2.A1A2A3A4A5phone 98xx01UPI x@okdevice D7Hops 3–6: A2 shares a UPI id with A3, who sharesa device with A4. Four “new users”, one person: a ring.A1A2A3A4A5phone 98xx01UPI x@okdevice D7Graph query: one traversal, milliseconds.In SQL: a chain of self-joins on huge tables, often minutes. myphatshala.in

NoSQL in Indian products you use

cache

1. Caches, OTPs and rate limits: key-value

Payment, food and shopping apps use Redis or similar stores for sessions, carts, OTPs with expiry and “max 5 attempts per minute” rate limits.

Request→Redis by key (~1 ms)→Answer
{ }

2. Product catalogues: document stores

Phones, sarees and groceries have completely different attributes. Shopping platforms store each product as a document with its own fields and nested variants.

Seller upload→Document per product→Search + product page

3. Messages and IoT: wide-column

Chat histories, smart-meter readings and vehicle telemetry are huge append-only streams read by key and time. Wide-column stores such as Cassandra and HBase are built for this.

Devices→Partition by device + day→Recent history

4. Fraud and recommendations: graphs

Banks and fintechs look for rings of accounts sharing devices and identifiers; e-commerce and social apps recommend “people also bought” from relationship graphs.

Events→Graph of entities→Ring alerts, suggestions
7 Run the code

Four NoSQL models in plain Python

Each NoSQL family is simulated with ordinary Python data structures: a dictionary for key-value, nested dictionaries for documents, partitions of sorted rows for wide-column, and an adjacency list for a graph. Run it, then try the challenges: cache-aside, denormalisation, choosing a partition key, quorum consistency and graph hops.

# 1. Key-value: get/set by key only
kv = {"session:7f3a": "U9", "cart:U9": {"thali": 1, "lassi": 2}}
print("KV       :", kv["cart:U9"])

# 2. Document: nested, flexible records, queried by fields
kitchens = [{"_id": "K11", "city": "Pune", "veg": True, "menu": ["thali", "lassi"]},
            {"_id": "K12", "city": "Pune", "veg": False, "menu": ["biryani"], "halal": True}]
print("Document :", [k["_id"] for k in kitchens if k["city"] == "Pune" and k["veg"]])

# 3. Wide-column: partition by key, rows sorted inside the partition
pings = {"R77": [(1, "18.50,73.85"), (2, "18.51,73.86"), (3, "18.52,73.86")],
         "R12": [(1, "19.07,72.87")]}
print("Wide-col :", "latest R77 ->", pings["R77"][-1])

# 4. Graph: nodes and relationships
friends = {"Riya": ["Kabir", "Meera"], "Kabir": ["Arjun"], "Meera": ["Zoya"]}
two_hops = [ff for f in friends["Riya"] for ff in friends.get(f, [])]
print("Graph    :", "friends of Riya's friends ->", two_hops)
KV : {'thali': 1, 'lassi': 2} Document : ['K11'] Wide-col : latest R77 -> (3, '18.52,73.86') Graph : friends of Riya's friends -> ['Arjun', 'Zoya']
  • print("KV :", kv["cart:U9"])Key-value stores answer one question very fast: “what is stored under this key?”
  • {"_id": "K12", "city": "Pune", "veg": False, "menu": ["biryani"], "halal": True}]Documents can have different fields. K12 has halal; K11 doesn't, and nothing breaks.
  • print("Wide-col :", "latest R77 ->", pings["R77"][-1])All of R77's rows sit in one partition, already sorted, so “latest” is the last row.
  • two_hops = [ff for f in friends["Riya"] for ff in friends.get(f, [])]Following relationships hop by hop is what graph databases are built for.

Run it on your PC: key-value, documents and a graph

Python's standard library includes a real on-disk key-value store (dbm) and SQLite with JSON functions. This program uses them to store carts by key with an expiry, query JSON kitchen documents by their fields, and find a fraud ring in a small graph. Save it as nosql_lab.py in an empty folder and run it.

NoSQL ideas with the standard library

"""Three NoSQL ideas using only Python's standard library:
1. a real key-value store on disk (dbm) used as a shopping-cart store
2. JSON documents queried with SQLite's JSON functions (a document-store style)
3. a small graph searched for a fraud ring"""
import dbm, json, sqlite3
from collections import deque

# ---- 1. Key-value: carts stored by key, with an expiry time
with dbm.open("carts_db", "c") as kv:
    now = 1_000_000                       # fixed clock so the output is repeatable
    kv["cart:U9"] = json.dumps({"items": {"thali": 1, "lassi": 2}, "expires": now + 1800})
    kv["cart:U12"] = json.dumps({"items": {"dosa": 1}, "expires": now - 5})   # already expired
    for key in [b"cart:U9", b"cart:U12", b"cart:U99"]:
        raw = kv.get(key)
        if raw is None:
            print(key.decode(), "-> not found")
            continue
        cart = json.loads(raw)
        state = "expired" if cart["expires"] < now else "live"
        print(key.decode(), "->", cart["items"], f"({state})")

# ---- 2. Documents: each kitchen is one JSON document with a nested menu
db = sqlite3.connect(":memory:")
db.execute("CREATE TABLE kitchens (doc TEXT)")
docs = [
    {"_id": "K11", "name": "Annapurna", "city": "Pune", "veg": True,
     "menu": [{"dish": "thali", "price": 220}, {"dish": "lassi", "price": 100}]},
    {"_id": "K12", "name": "Spice Box", "city": "Pune", "veg": False,
     "menu": [{"dish": "biryani", "price": 280}], "delivery_partner": "own riders"},
    {"_id": "K13", "name": "Udupi Corner", "city": "Mumbai", "veg": True,
     "menu": [{"dish": "dosa", "price": 150}, {"dish": "filter coffee", "price": 60}]},
]
db.executemany("INSERT INTO kitchens VALUES (?)", [(json.dumps(d),) for d in docs])
rows = db.execute("""
    SELECT json_extract(doc, '$.name'), json_array_length(doc, '$.menu')
    FROM kitchens
    WHERE json_extract(doc, '$.city') = 'Pune' AND json_extract(doc, '$.veg') = 1
""").fetchall()
print("\nVeg kitchens in Pune (name, dishes on menu):", rows)
extra = db.execute("SELECT json_extract(doc, '$.name') FROM kitchens WHERE json_extract(doc, '$.delivery_partner') IS NOT NULL").fetchall()
print("Documents with an extra field nobody planned for:", extra)

# ---- 3. Graph: accounts linked by shared phones and UPI handles
edges = [("A1", "phone:98xx01"), ("A2", "phone:98xx01"), ("A2", "upi:x@ok"), ("A3", "upi:x@ok"),
         ("A3", "device:D7"), ("A4", "device:D7"), ("A5", "phone:77xx22")]
graph = {}
for a, b in edges:
    graph.setdefault(a, set()).add(b)
    graph.setdefault(b, set()).add(a)
start, seen, queue = "A1", {"A1"}, deque(["A1"])
while queue:
    node = queue.popleft()
    for nxt in graph[node]:
        if nxt not in seen:
            seen.add(nxt)
            queue.append(nxt)
ring = sorted(n for n in seen if n.startswith("A"))
print("\nAccounts connected to A1 through shared phones, UPI ids or devices:", ring)

# In a terminal:  python nosql_lab.py
cart:U9 -> {'thali': 1, 'lassi': 2} (live) cart:U12 -> {'dosa': 1} (expired) cart:U99 -> not found Veg kitchens in Pune (name, dishes on menu): [('Annapurna', 2)] Documents with an extra field nobody planned for: [('Spice Box',)] Accounts connected to A1 through shared phones, UPI ids or devices: ['A1', 'A2', 'A3', 'A4']

Recorded from a real run with Python 3.13; any Python 3.9+ gives the same output. The program creates carts_db files (their exact names depend on your operating system). To try the real products, install Redis or MongoDB Community, or use their free cloud tiers, and repeat the same three ideas.

8 Line by line

Cache-aside, one line at a time

Three requests arrive for kitchen pages. Watch the first request for each kitchen miss the cache and read the database, and the repeat request hit the cache.

Worked examples: real problems, solved and traced in memory

Four NoSQL design tasks, from choosing a family to finding a ring in a graph. Each runs live and is traced.

Example 1BeginnerSystem design · choosing

Which database family for each feature?

Problem statement

Pick a database family for each feature using its main access pattern.

Input
payments, user sessions, product catalogue, chat history, friend suggestions
Expected output
payments            -> relational (SQL)
user sessions       -> key-value
product catalogue   -> document
chat history        -> wide-column
friend suggestions  -> graph

Approach

  1. Write each feature's access pattern.
  2. Map patterns to families with a dictionary.
  3. Print the choice.

Solution: run it live

What happens in memory, step by step

Recorded from a real Python run: press Next to execute one line at a time. Watch variables change in each frame, and lists and dictionaries (numbered #1, #2 …) fill up as objects.

Example 2IntermediateDocument store · embedding

Embed or reference?

Problem statement

For each child collection, decide whether to embed it inside the parent document or keep it separate: embed if it is always read with the parent and stays small (under 100 items); otherwise reference it.

Input
menu (40 items, read together), reviews (grows forever), addresses (3, read together), orders (grows forever)
Expected output
menu     : embed (small, read with parent)
reviews  : reference (large or read separately)
addresses: embed (small, read with parent)
orders   : reference (large or read separately)

Approach

  1. Describe each child with its expected size and whether it is read with the parent.
  2. Apply the rule.
  3. Print the decision and why.

Solution: run it live

What happens in memory, step by step

Recorded from a real Python run: press Next to execute one line at a time. Watch variables change in each frame, and lists and dictionaries (numbered #1, #2 …) fill up as objects.

Example 3IntermediateWide-column · key design

Measure partition balance for two key designs

Problem statement

Messages are partitioned either by group_id or by (group_id, month). Compute the number of partitions and the largest partition for each design.

Input
3 groups with 1,200, 90 and 30 messages over 6 months
Expected output
group       :  3 partitions, largest 1200 messages
group+month : 18 partitions, largest 200 messages

Approach

  1. Start from the number of messages per group (spread evenly over 6 months).
  2. Compute partition sizes for each candidate key.
  3. Compare the largest partition: smaller is safer.

Solution: run it live

What happens in memory, step by step

Recorded from a real Python run: press Next to execute one line at a time. Watch variables change in each frame, and lists and dictionaries (numbered #1, #2 …) fill up as objects.

Example 4AdvancedFraud analytics · graph

Find connected groups of accounts

Problem statement

Accounts are linked when they share an identifier. Find every connected group (ring) with more than one account.

Input
5 accounts; pairs that share a phone or device
Expected output
Rings: [['A1', 'A2'], ['A3', 'A4', 'A5']]

Approach

  1. Build an undirected adjacency list from the pairs of accounts that share an identifier.
  2. For each unvisited account, walk the graph to collect its whole group.
  3. Every group found this way has at least two accounts: a possible ring.

Solution: run it live

What happens in memory, step by step

Recorded from a real Python run: press Next to execute one line at a time. Watch variables change in each frame, and lists and dictionaries (numbered #1, #2 …) fill up as objects.

9 Quiz and revision

Check yourself

Quick revision

Lesson 2.4 in one screen

  • NoSQL = not only SQL: non-relational models, flexible schemas, horizontal scaling.
  • Key-value (Redis, DynamoDB) · Document (MongoDB) · Wide-column (Cassandra, HBase) · Graph (Neo4j).
  • SQL: fixed schema, joins, ACID, vertical scaling. NoSQL: flexible, denormalised, horizontal, often BASE/tunable.
  • Model from queries: denormalise; embed small, bounded children; reference unbounded ones.
  • Partition key spreads data and load; avoid hot partitions; clustering key sorts rows inside.
  • Cache-aside: check cache → on miss read DB and store with TTL → invalidate on change.
  • Quorum: R + W > N for read-your-latest-write. LSM trees for heavy writes. Polyglot persistence = right store per job.

Interview and viva questions

What are the types of NoSQL databases? Give examples.

Key-value stores (Redis, Amazon DynamoDB, Memcached) map unique keys to values for very fast look-ups, used for caches, sessions and carts. Document stores (MongoDB, Couchbase, Firestore) hold JSON-like documents with nested, flexible fields, used for catalogues and profiles. Wide-column stores (Cassandra, HBase, Bigtable) organise rows into partitions sorted by clustering keys across many nodes, used for time series, logs and messaging. Graph databases (Neo4j, Amazon Neptune) store nodes and relationships, used for fraud detection, recommendations and social networks.

Differentiate SQL and NoSQL databases.

SQL databases use tables with predefined schemas, support joins and ACID transactions, and typically scale vertically with read replicas. NoSQL databases use key-value, document, wide-column or graph models, allow flexible schemas, scale horizontally, denormalise data around access patterns and often offer eventual or tunable consistency. SQL suits transactional systems and ad-hoc analysis; NoSQL suits very high scale, flexible data and specific access patterns.

What is the CAP-related trade-off in NoSQL databases?

In a distributed database, when the network partitions, a system must choose between consistency (every read sees the latest write) and availability (every request gets a response). Many NoSQL systems, such as Cassandra, favour availability with tunable consistency, while others, such as HBase, favour consistency. Lesson 3.4 explains the CAP theorem in detail.

When should you not use NoSQL?

When you need complex multi-table transactions (payments, accounting), rich ad-hoc queries and joins across many entities, or strong consistency everywhere without careful design, and when the data comfortably fits a single well-indexed relational database. Adding a NoSQL store also adds operational complexity.

What is polyglot persistence?

Using several kinds of data stores in one system, each chosen for a specific workload: for example a relational database for orders and payments, Redis for caching and sessions, a document store for catalogues, a wide-column store for event history, and a search engine for text search.

Practice problems

NoSQL modelling and access-pattern exercises. The last two are business tasks.

P1Easy

Get with a default

Read a key from a key-value dictionary; print “not found” if it is missing.

Input
keys cart:U9, cart:U99
Expected output
cart:U9 -> {'thali': 1}
cart:U99 -> not found
Show solution and explanation
kv = {"cart:U9": {"thali": 1}}
for k in ["cart:U9", "cart:U99"]:
    print(k, "->", kv.get(k, "not found"))
cart:U9 -> {'thali': 1} cart:U99 -> not found

How it works: get with a default avoids an error for missing keys.

P2Easy

Query documents by a field

Print the names of kitchens in Pune.

Input
3 documents
Expected output
['Annapurna', 'Spice Box']
Show solution and explanation
docs = [{"name": "Annapurna", "city": "Pune"}, {"name": "Udupi", "city": "Mumbai"}, {"name": "Spice Box", "city": "Pune"}]
print([d["name"] for d in docs if d["city"] == "Pune"])
['Annapurna', 'Spice Box']

How it works: Document stores filter and index by fields inside the documents.

P3Easy

Expired keys

Each key has an expiry time. At time 100, print the keys still alive.

Input
otp1 exp 90, otp2 exp 120, cart exp 300
Expected output
['otp2', 'cart']
Show solution and explanation
expiry = {"otp1": 90, "otp2": 120, "cart": 300}
now = 100
print([k for k, exp in expiry.items() if exp > now])
['otp2', 'cart']

How it works: This is what a TTL does automatically in Redis or Cassandra.

P4Medium

Latest rows from a partition

Pings are stored per rider. Print the 2 most recent pings of R7.

Input
R7: t=1,2,3,4
Expected output
[(4, 'D'), (3, 'C')]
Show solution and explanation
pings = {"R7": [(1, "A"), (2, "B"), (3, "C"), (4, "D")], "R9": [(1, "X")]}
print(sorted(pings["R7"], reverse=True)[:2])
[(4, 'D'), (3, 'C')]

How it works: In Cassandra, clustering by time descending makes this a cheap read of the first rows.

P5Medium

Friends of friends

Print people two hops from Riya who are not already her friends.

Input
small friend graph
Expected output
['Arjun', 'Zoya']
Show solution and explanation
g = {"Riya": ["Kabir", "Meera"], "Kabir": ["Arjun", "Meera"], "Meera": ["Zoya"]}
direct = set(g["Riya"])
two = {ff for f in direct for ff in g.get(f, [])} - direct - {"Riya"}
print(sorted(two))
['Arjun', 'Zoya']

How it works: Set operations remove existing friends and Riya herself.

P6Advanced

Hit ratio needed

The database handles at most 3,000 reads per second. Traffic is 2,00,000 reads per second. What minimum cache hit ratio is needed?

Input
3,000 vs 2,00,000
Expected output
Minimum hit ratio: 98.5%
Show solution and explanation
db_max, traffic = 3000, 200000
needed = 1 - db_max / traffic
print(f"Minimum hit ratio: {needed * 100:.1f}%")
Minimum hit ratio: 98.5%

How it works: Only misses reach the database, so misses must stay at or below 3,000 per second.

P7Advanced

Quorum settings

For N = 5 replicas, print every (W, R) pair with R + W > N where both are between 1 and 5, and the smallest such R for W = 3.

Input
N = 5
Expected output
15 valid pairs
smallest R for W=3: 3
Show solution and explanation
N = 5
pairs = [(w, r) for w in range(1, 6) for r in range(1, 6) if r + w > N]
print(len(pairs), "valid pairs")
print("smallest R for W=3:", min(r for w, r in pairs if w == 3))
15 valid pairs smallest R for W=3: 3

How it works: Stronger writes (bigger W) allow lighter reads (smaller R), and the other way round.

P8Business problem

Redis memory for sessions

5 crore daily users, 30% active at peak, each session is 2 KB. How many GB of cache memory are needed (add 30% overhead)?

Input
5 crore, 30%, 2 KB, +30%
Expected output
Memory needed: 37 GB
Show solution and explanation
users = 5_00_00_000
active = users * 0.30
gb = active * 2 / 1024 / 1024 * 1.3
print(f"Memory needed: {gb:.0f} GB")
Memory needed: 37 GB

How it works: Capacity planning for an in-memory store: active keys × size × overhead.

P9Business problem

Cache saving

Each database read costs Rs 0.002 in compute; a cache read costs Rs 0.0001. With 1 crore reads a day and a 95% hit ratio, print the daily cost with and without the cache.

Input
1 crore reads, 95% hits
Expected output
Without cache: Rs 20000
With cache   : Rs 1950
Show solution and explanation
reads = 1_00_00_000
no_cache = reads * 0.002
with_cache = reads * 0.95 * 0.0001 + reads * 0.05 * 0.002
print("Without cache: Rs", round(no_cache))
print("With cache   : Rs", round(with_cache))
Without cache: Rs 20000 With cache : Rs 1950

How it works: A cache cuts both latency and cost, as long as the data can tolerate a short staleness. (Costs are illustrative.)

Real-world scenario questions (asked in company interviews)

NoSQL design questions from real interviews and projects. Decide your answer first, then open the model answer.

System design · storage choiceFresher

SQL or NoSQL for a new feature?

A ride-hailing app wants to store (a) payments, (b) the live location of every driver, updated every 4 seconds, (c) driver profiles with optional documents, and (d) “riders who travel similar routes”. Which store for each?

The interviewer is checking: can you justify a database choice from access patterns?

Show a strong answer
  • Payments: relational (PostgreSQL/MySQL): ACID transactions.
  • Live locations: key-value (Redis) keyed by driver for the current position, plus a wide-column store (Cassandra) partitioned by driver and day for history.
  • Driver profiles: document store (MongoDB): flexible optional fields.
  • Similar routes: a graph or batch analytics in the lake, depending on how real-time it must be.

Then mention the cost of running several systems and keeping them consistent.

Avoid: choosing NoSQL because it is “faster” or “modern”, or MongoDB for money transfers without discussing transactions.

Data modelling · Cassandra1–3 years

A Cassandra table keeps timing out

A messages table uses PRIMARY KEY (group_id, sent_at). A few huge groups now time out on reads, and their nodes run hot.

The interviewer is checking: do you understand partition sizing and hot partitions?

Show a strong answer

Huge groups create unbounded partitions that live on one set of replicas. Re-key to bound partition size, for example PRIMARY KEY ((group_id, month), sent_at) or add a bucket number for very active groups. Read the latest bucket first and page backwards. Monitor partition sizes and set alerts on large partitions. Migrate by dual-writing to the new table and backfilling.

Avoid: adding more nodes, which doesn’t split a single hot partition.

Reliability · caching1–3 years

Customers see old prices

After adding a Redis cache in front of the product database, some customers see yesterday’s prices for hours.

The interviewer is checking: do you know cache invalidation and TTL trade-offs?

Show a strong answer
  1. Set a sensible TTL so stale entries expire (minutes, not days).
  2. On every price update, delete or update the cache key in the same code path (cache-aside invalidation), or publish a change event that cache updaters consume.
  3. For the final checkout price, always read from the source database.
  4. Monitor hit ratio and staleness.

Avoid: disabling the cache entirely, or caching prices with no expiry.

Fundamentals · interviewFresher

“What is the difference between SQL and NoSQL?”

A standard opening question in data engineering interviews.

The interviewer is checking: can you compare them fairly, with examples and trade-offs?

Show a strong answer

SQL databases store data in tables with fixed schemas, support joins and strong ACID transactions, and usually scale vertically (plus read replicas). NoSQL databases use other models (key-value, document, wide-column, graph), allow flexible schemas, scale horizontally across many nodes, and are designed around specific access patterns, often with relaxed or tunable consistency. Use SQL for transactional systems and complex ad-hoc queries; use NoSQL for very high scale, flexible records, low-latency key look-ups, time series or relationship-heavy queries. Many systems use both.

Avoid: saying NoSQL has no schema at all, or that SQL can’t scale.