Foundationপ্রথম নীতি থেকে
LEVEL 8লেসন ১৯/১৯কঠিন৫০ মিনিট

NoSQL — document, KV, wide-column, graph

NoSQL: Four Families

Key-value, document, wide-column, graph — চার পরিবারের NoSQL-এর জন্মকথা, ভেতরের যন্ত্র, লুকানো বিল; আর JSONB-যুগে সীমানা ঝাপসা হয়ে যাওয়ার গল্প।

এই লেসন শেষে আপনি পারবেন

  • NoSQL movement-এর আসল চালিকা দুটো — horizontal scale আর schema flexibility — religion নয় — ব্যাখ্যা করে বলতে পারবেন কোন problem কোন family-কে জন্ম দিল
  • চার পরিবারের data-model আর trade-off আলাদা করতে পারবেন — key-value (Redis/DynamoDB-র O(1), TTL), document (MongoDB-র nested docs, schema-on-read), wide-column (Cassandra/Bigtable-এ partition-key = query-design), graph (Neo4j-র index-free adjacency)
  • Document-store-এর লুকানো বিলগুলো চিনতে পারবেন — join-এর অনুপস্থিতি, application-side integrity, denormalize-এর update-anomaly — আর কখন সেই বিল দেওয়া সার্থক
  • Graph-traversal কেন property-graph-এ এত দ্রুত — index-free adjacency-র যন্ত্র — আর recursive SQL CTE-এর সাথে তুলনা করতে পারবেন
  • Polyglot persistence pragmatism আর SQL-renaissance (JSONB + GIN index, recursive CTE) দেখিয়ে বলতে পারবেন কেন 'one database for everything' আজ আর ডিফল্ট উত্তর নয় — কোনোটাই না

আগে যা বোঝা থাকা দরকার

আগে এটা বুঝি

২০০০-এর দশকের মাঝখানে web-এর scale হঠাৎ ভিন্ন গ্রহের হয়ে গেল — Google-এর ডেটা petabyte-ছুঁল, Amazon-এর cart পৃথিবী-জুড়ে, আর relational database-এর তখনকার সবচেয়ে বড় ধর্মগ্রন্থ vertical-scale- কে বলছিল “আরও বড় box কিনুন”। Bigtable (2006) আর Dynamo (2007) paper-দুটো ঘোষণা করল: horizontal scale আর schema-flexibility — এই দুটোর জন্য relational model-এর অংশবিশেষ ছেঁড়ে ফেলা যায়। Movement-টার নাম হলো NoSQL।

আজ, দুই দশক পরে, ঠান্ডা মাথায় ফেরা যায়: NoSQL কোনো religion নয় — চারটা আলাদা engineering-answer, চারটা আলাদা প্রশ্নের:

  • “key দিলে value দাও, আর কিছু নয়” — key-value
  • “nested document জমাই, structure পরে বুঝি” — document
  • “প্রতিটা row-এর column আসলে আলাদা, sparse-ও” — wide-column
  • “data-র চেয়ে relationship-ই প্রধান” — graph

আজকের লেসনে চার পরিবার ঘুরব — প্রতিটার জন্ম-কারণ, ভেতরের যন্ত্র, আর সেই বিজ্ঞাপনে না-লেখা বিলগুলো। শেষে দেখব SQL-জগৎ নিজেই কীভাবে JSONB নিয়ে সীমানা ঝাপসা করল — আর এটাই module-এর শেষ পাতা।

মূল ধারণা

চার পরিবার, চারটা চুক্তিপত্র

FamilyModelQuery-chুক্তিRepresentative
Key-valueopaque blob ↔ keyget(key) / put(key,val) — O(1)-ই সবRedis, DynamoDB
Documentnested JSON-ish docfield-path lookup, doc-scoped aggregationMongoDB, Couchbase
Wide-columnrows × dynamic column-familiespartition-key-scoped readCassandra, Bigtable, HBase
Graphnodes + typed edgestraversal — pointer-chaseNeo4j, Neptune

Key-value — ইচ্ছাকৃত অন্ধত্বর গতি

সবচেয়ে কঠোর চুক্তি: server value-র ভেতরটা দেখে না। ফল: indexing-plan নেই, query-planner নেই, lock-scope একটাই (whole value) — O(1) hash-lookup ছাড়া আর কোনো path-ই exist করে না। Redis-এ সব data memory-তে, persistence optional (RDB snapshot / AOF-log — WAL-পরিচিত মুখ!); DynamoDB-তে LSM Tree-engine write-amplification-tuned disk-এ।

দুটো built-in ধারণা production-এর প্রাণ:

  • TTL (SETEX) — key-এর মৃত্যু-তারিখ আগেই লিখে রাখা; session, cache-entry, OTP — সব auto-expire
  • Atomic ops on value — INCR, HINCRBY — counter-logic server- side, race-free

Document — schema-on-read-এর স্বাধীনতা

Document-store value-র ভেতরটা বোঝে: nested object/array, field-path query, doc-ভিত্তিক index। মূল promise: schema-on-read — লেখার সময় structure enforce নেই, পড়ার সময় আপনি shape ঠিক করবেন। Product-catalog-এর মতো heterogeneous data-র জন্ম: TV-র doc-এ screen_size, book-এর doc-এ page_count — এক collection, কোনো NULL-column-স্তূপ নেই।

চুক্তির অন্ধ-দিক: join নেই। Related-data হয় embed করবেন (denormalize — duplicate + update-anomaly-র ঝুঁকি), নয় client-side multi-query (N+1)। Integrity-ও app-এর দায় — database invariant enforce করে না। Schema-flexibility-র দাম এখানেই: constraint যেখানে লেখা হয় না, সেখানে ভাঙারও সুযোগ।

Wide-column — partition-key-ই আপনার query-design

Cassandra-র table দেখতে SQL-এর মতো, কিন্তু দর্শন উল্টো: আগে query ভাবুন, তারপর table। Partition-key (hash-sharded — sharding লেসনের ring!) ঠিক করে কোন data একসাথে বসবে; clustering-columns ঠিক করে ভেতরের sort-order। Query যদি partition-key include করে — single-node O(log n); না করে — full-cluster scan, production-এ নিষিদ্ধ-প্রায়। Ad-hoc analytics-এর জন্য এই জগৎ নয় — সেটা warehouse-দের কাজ।

Graph — relationship-first জগৎ

Property-graph model: node-এ property-map, edge-এ type+direction+ property। Social-feed, fraud-ring, recommendation — “friends-এর friends-এর liked pages” জাতীয় প্রশ্ন relational-এ multi-way join বা recursive CTE; graph-এ pointer-chase: node-এর adjacency-list memory-তে, প্রতিটা hop constant-cost — এটাই index-free adjacency (neighbor খুঁজতে global index নয়, direct reference)। Depth বাড়লে join-এর খরচ exponential-blowup-ঘেঁষে, traversal-এর linearly।

ভেতরে কী ঘটছে

LSM-connection — wide-column-এর engine

Sharding লেসনের consistent-hash ring Cassandra-র data-placement ঠিক করে; ভেতরের storage-engine আমাদের চেনা: LSM Tree — memtable, SSTable, compaction, bloom-filter। Write-optimized path-ই Cassandra-র write-heavy workload-claim-এর রহস্য। আর delete = Tombstone marker — compaction পর্যন্ত বাঁচে; tombstone-storm (bulk-delete) হলো read-latency-র ক্লাসিক শত্রু। Module-এর আগের লেসনগুলো এখানেই ফসল দিচ্ছে — engine-ভিন্ন হলেও সমস্যা-সেট universal।

Redis-এর persistence — পরিচিত দুই পথ

Snapshot (RDB): fork করে copy-on-write image dump — buffer-pool- flush-এর আত্মীয়। AOF (append-only file): প্রতিটা write-command log — হুবহু WAL-দর্শন, fsync-policy knob (always/everysec/no) — synchronous_commit-ladder-এর ছোট ভাই। Crash-recovery = AOF-replay। “NoSQL-এ durability নেই” myth-এর জবাব: আছে, আপনি টিউন করেন কতটুকু।

Consistency-knob — CAP-লেসনের ফসল

DynamoDB/Cassandra-র consistency-level per-operation: ONE, QUORUM, ALL (write-side W, read-side R)। W+R > N হলে read-write overlap-এ fresh-read guarantee (quorum-গণিত — Level 9- এর পূর্ণ বিষয়)। LWW-conflict, hinted-handoff, read-repair — eventual-consistency-র merge-machinery এখানে production-grade। CAP-লেসনের “per-operation choice” এখানেই বাজারে।

উদাহরণ

একই feature — চার পরিবারে চার রূপ

Feature: e-commerce session + cart + recommendation-graph।

KEY-VALUE (Redis)
  SETEX sess:{token} 1800 '{"uid":42,"cart":[...]}'
  -> ৩০ মিনিটের জীবন, TTL-এ নিজেই মরে; GET O(1)
  -> প্রশ্ন ছাড়া প্রশ্ন: token ছাড়া কিছুই খোঁজা যায় না

DOCUMENT (MongoDB)
  db.carts.insertOne({_id:42, items:[
    {sku:"A1", qty:2}, {sku:"B7", qty:1}],
    coupon:{code:"EID25", pct:25}})
  -> cart এক doc — read atomic, write এক round-trip
  -> কিন্তু "সব user যারা EID25 নিয়েছে" — collection scan
     বা app-side fan-out (coupon-index আলাদা বানানো ছাড়া)

WIDE-COLUMN (Cassandra)
  CREATE TABLE cart_events(
    user_id uuid, ts timeuuid, sku text, qty int,
    PRIMARY KEY ((user_id), ts));
  -> partition=user_id: এক user-এর timeline single-node,
     time-ordered; retention = TTL-per-row
  -> "সব cart যেখানে sku=B7" — partition-key নেই: forbidden

GRAPH (Neo4j)
  (:User {id:42})-[:VIEWED]->(:Product {sku:"B7"})
                 -[:BOUGHT]->(:Product)
  MATCH (u:User {id:42})-[:BOUGHT]->(p)<-[:BOUGHT]-(similar)
        (similar)-[:BOUGHT]->(rec)
  RETURN rec LIMIT 5
  -> "যারা B7 কিনেছে তারা আর কী কিনেছে" — ৩-hop chase,
     index-free adjacency-তে ms-scale

একই domain, চারটা আলাদা প্রশ্ন-প্রকৃতি — আর বাস্তব product-গুলো তাই polyglot: Redis-session + PG-orders + Cassandra-events + graph-service। Pragmatism, fashion নয় — প্রতিটা store-যন্ত্র তার প্রশ্নের জন্য।

ছবিতে

চার মডেলের ভূগোল এক ছবিতে

KEY-VALUE           DOCUMENT             WIDE-COLUMN          GRAPH
┌───────────┐   ┌────────────────┐   ┌───────────────┐   ┌──────────┐
│ k1 ─► ▓▓▓ │   │ {_id:1,        │   │ partition-key │   │ (U42)───►│
│ k2 ─► ▒▒▒ │   │  user:{city:   │   │ ┌───────────┐ │   │    VIEWED │
│ k3 ─► ░░░ │   │   "Dhaka"),    │   │ │row: cols..│ │   │         ▼ │
│  (opaque) │   │  items:[…]}    │   │ │sorted by  │ │   │ (P"B7")   │
│           │   │ (understood!)  │   │ │clustering │ │   │    ▲   │  │
└───────────┘   └────────────────┘   │ └───────────┘ │   │        └──┘
                                     └───────────────┘   (pointer-chase)

query-power:   kv <  document  ≈  wide-column*   graph(traversal-এ)
flexibility:   kv >  document  >  wide-column >  graph(schema)
*: শর্তসাপেক্ষ — partition-key-scoped query-তেই
Data-layout চার রকম — access-path-ও তাই; family-বাছাই মানে আসলে dominant-query-বাছাই।

নিজে চালিয়ে দেখুন

EXPERIMENT

JSONB+GIN, TTL-অনুকরণ, আর recursive CTE বনাম N+1

macOS / Linux (PostgreSQL + Python3)· ২৫ মিনিট

ধাপ ১ — document-store pattern, PG-তেই:

CREATE TABLE events (id bigserial PRIMARY KEY, doc jsonb NOT NULL);

INSERT INTO events (doc)
SELECT jsonb_build_object(
  'user', jsonb_build_object('id', g % 5000,
                             'city', (ARRAY['Dhaka','Ctg','Sylhet'])
                                      [1 + g % 3]),
  'action', (ARRAY['view','cart','buy'])[1 + g % 3])
FROM generate_series(1, 200000) g;

\timing on
EXPLAIN ANALYZE SELECT count(*) FROM events
  WHERE doc @> '{"user": {"city": "Dhaka"}}';
-- Seq Scan — পুরো ২ লক্ষ row

CREATE INDEX events_gin ON events USING gin (doc);
EXPLAIN ANALYZE SELECT count(*) FROM events
  WHERE doc @> '{"user": {"city": "Dhaka"}}';
-- Bitmap Index Scan on events_gin — containment-index!

@> containment-operator নেস্টেড path-এও কাজ করে, GIN সেটাই inverted-index করে — MongoDB-র field-index-এর সরাসরি প্রতিযোগী। Timing-দুটো তুলনা করুন — document-pattern-এর দরকারে dedicated document-store কখন দরকার, কখন PG-ই যথেষ্ট, সংখ্যা বলবে।

ধাপ ২ — Redis-style TTL, lazy-expiry মডেল:

import time

store = {}

def setex(key, val, ttl_s):
    store[key] = (val, time.time() + ttl_s)

def get(key):
    item = store.get(key)
    if item is None:
        return None
    val, expires_at = item
    if time.time() > expires_at:
        del store[key]
        return None              # lazy expiry — পড়তে গিয়েই মৃত্যু
    return val

setex("sess:abc", {"uid": 42}, 2)
print(get("sess:abc"))           # {'uid': 42}
time.sleep(2.1)
print(get("sess:abc"))           # None — এবং entry-ও মুছে গেল
print(len(store))                # 0

Real Redis এর সাথে active-expiry-cycle-ও চালায় (background-এ random-key sample-check) — lazy-only হলে অপঠিত expired-key জমে memory-ফাঁদ। দুই strategy-র division-of-labor মনে রাখুন — housekeeping-লেসনের echo!

ধাপ ৩ — graph-traversal: CTE বনাম N+1:

CREATE TABLE person AS
  SELECT g AS id FROM generate_series(1, 400) g;
CREATE TABLE friend (a int, b int);
INSERT INTO friend
  SELECT 1 + floor(random()*400)::int,
         1 + floor(random()*400)::int
  FROM generate_series(1, 1200);

\timing on
-- এক query-তে ৪-depth reach:
WITH RECURSIVE reach AS (
  SELECT a, b, 0 AS depth FROM friend WHERE a = 7
  UNION
  SELECT f.a, f.b, r.depth + 1
  FROM friend f JOIN reach r ON f.a = r.b
  WHERE r.depth < 4
)
SELECT count(DISTINCT b) AS reached FROM reach;
import subprocess, time

def q(sql):
    return subprocess.run(["psql", "-Atc", sql],
                          capture_output=True, text=True).stdout.strip()

t0 = time.perf_counter()
frontier, seen = [7], set()
for depth in range(4):                        # N+1: প্রতি hop-এ এক query
    ph = ",".join(map(str, frontier))
    rows = q(f"SELECT DISTINCT b FROM friend WHERE a IN ({ph});")
    nxt = [int(x) for x in rows.splitlines() if x]
    seen |= set(nxt)
    frontier = nxt or []
print(f"N+1 ৪-hop: {(time.perf_counter()-t0)*1000:.0f}ms")
# CTE-র EXPLAIN-এর execution-time-এর সাথে তুলনা করুন

CTE single-round-trip-এ set-based; N+1 প্রতি hop-এ process-spawn + network — gap বাড়বে hop-আর latency-বাড়লে। Graph-database-এর traversal-advantage এই gap-এরই বিশাল-সংস্করণ (plus adjacency-in-memory)।

এটা কী প্রমাণ করে

PostgreSQL একাই document-pattern (containment-query + GIN) চালায়; TTL-expiry-র lazy-model হাতে দেখা যায়; আর graph-traversal-এ recursive CTE এক query-তে কাজ শেষ করে যেখানে N+1 নেটওয়ার্ক-রাউন্ডে ডুবে যায়।

নিজে বানান

BUILD IT

TinyDocumentStore — append-only + field-path index

Python · ●●●○○
  1. Append-only JSONL log — put/delete record; restart-এ replay
  2. Nested field-path flattening — user.city জাতীয় path-এ index
  3. Secondary index — path → value → id-list; put/unput maintenance
  4. find({path: value}) API — index-hit হলে postings, না হলে scan
  5. Benchmark — indexed বনাম unindexed-field query, stats ছাপুন

লক্ষ্য: document-store-এর হৃৎপিণ্ড দুটো — durable-append-log আর secondary-index — নিজের হাতে গড়া; benchmark-এ index-এর অস্তিত্বের দাম সংখ্যায়।

import json, os, time

class TinyDocStore:
    """Append-only persisted doc-collection + field-path secondary index"""

    def __init__(self, path="/tmp/tinydocs.jsonl"):
        self.path = path
        self.docs = {}                  # id -> doc
        self.index = {}                 # path -> {value: [ids]}
        self.stats = {"index_hit": 0, "scan": 0}
        if os.path.exists(path):
            with open(path) as f:
                for line in f:
                    rec = json.loads(line)
                    self._apply(rec)
        self.wal = open(path, "a")       # append-only durability

    # ---- log-replay == source of truth (WAL-লেসনের echo) ----
    def _apply(self, rec):
        if rec["op"] == "put":
            old = self.docs.get(rec["id"])
            if old is not None:
                self._unindex(rec["id"], old)
            self.docs[rec["id"]] = rec["doc"]
            self._index(rec["id"], rec["doc"])
        elif rec["op"] == "del":
            old = self.docs.pop(rec["id"], None)
            if old is not None:
                self._unindex(rec["id"], old)

    @staticmethod
    def flatten(doc, prefix=""):
        out = {}
        for k, v in doc.items():
            path = f"{prefix}.{k}" if prefix else k
            if isinstance(v, dict):
                out.update(TinyDocStore.flatten(v, path))
            else:
                out[path] = v
        return out

    def _index(self, did, doc):
        for path, val in self.flatten(doc).items():
            self.index.setdefault(path, {}) \
                      .setdefault(val, []).append(did)

    def _unindex(self, did, doc):
        for path, val in self.flatten(doc).items():
            postings = self.index.get(path, {}).get(val, [])
            if did in postings:
                postings.remove(did)

    def put(self, did, doc):
        rec = {"op": "put", "id": did, "doc": doc}
        self.wal.write(json.dumps(rec) + "\n")
        self.wal.flush()
        self._apply(rec)

    def delete(self, did):
        rec = {"op": "del", "id": did}
        self.wal.write(json.dumps(rec) + "\n")
        self.wal.flush()
        self._apply(rec)                 # tombstone-marker-অভিনয়

    def find(self, where):
        ((path, want),) = where.items()  # single-predicate API
        postings = self.index.get(path, {}).get(want)
        if postings is not None:
            self.stats["index_hit"] += 1
            ids = list(postings)         # O(postings) — সরাসরি
        else:
            self.stats["scan"] += 1
            ids = [i for i, d in self.docs.items()
                   if self.flatten(d).get(path) == want]
        return [self.docs[i] for i in ids]


# ---------------- benchmark ----------------
db = TinyDocStore("/tmp/tinydocs.jsonl")
if os.path.exists("/tmp/tinydocs.jsonl"):
    os.remove("/tmp/tinydocs.jsonl"); db = TinyDocStore(
        "/tmp/tinydocs.jsonl")

N = 50000
t0 = time.perf_counter()
for i in range(N):
    city = ("Dhaka", "Ctg", "Sylhet", "Khulna")[i % 4]
    db.put(i, {"name": f"u{i}", "age": 20 + i % 30,
               "address": {"city": city}})
load_s = time.perf_counter() - t0

def bench(where, n=200):
    t0 = time.perf_counter()
    for _ in range(n):
        hits = db.find(where)
    ms = (time.perf_counter() - t0) / n * 1000
    return len(hits), ms

h1, ms1 = bench({"address.city": "Dhaka"})   # indexed path
h2, ms2 = bench({"nonexistent.field": "x"})  # unindexed -> scan

print(f"loaded {N} docs in {load_s:.2f}s "
      f"({N/load_s:,.0f} puts/s)")
print(f"indexed query : {h1} hits, {ms1:.2f} ms/query")
print(f"scan query    : {h2} hits, {ms2:.2f} ms/query")
print(f"speedup       : {ms2/ms1:.0f}×")
print(f"stats         : {db.stats}")
db.delete(0)
assert all(d["name"] != "u0" for d in db.find({"age": 20}))
print("delete+tombstone replay: ok")

প্রত্যাশিত output (hardware-ভেদে সংখ্যা):

loaded 50000 docs in 1.31s (38,168 puts/s)
indexed query : 12500 hits, 8.41 ms/query
scan query    : 12500 hits, 61.72 ms/query
speedup       : 7×
stats         : {'index_hit': 200, 'scan': 200}
delete+tombstone replay: ok

তিনটা পাঠ: (১) index-hit postings-থেকে সরাসরি — hit-count বাড়লেও latency তেমন বাড়ে না, scan-এ linearly বাড়ে; (২) unindexed-field-এর query নীরবে scan-mode-এ ঢুকে যায় — stats-counter-ই একমাত্র সতর্কবার্তা (document-store-এ “missing index” silently slow); (৩) restart করে চালালে log-replay সব state ফিরিয়ে আনবে — durability append-log-ই।

নিজে বাড়ান:

  1. Multi-predicate AND — intersection-of-postings algorithm
  2. Log-compaction — পুরনো put-overwrite গুটিয়ে snapshot+tail (LSM-compaction-এর মিনি-রূপ)
  3. Range-predicate (age >= 25) — sorted-postings + bisect

বাস্তব সিস্টেমে

বাস্তব জগতে চার পরিবার

Redis — session/cache/rate-limit এর universal sidekick। প্রায় সব web-stack-এর পাশে একটা Redis; TTL, INCR, pub/sub, sorted-set leaderboard — ছোট ছোট চুক্তি, বিশাল কাজ।

MongoDB — content/catalog/profile। Schema-on-read-এর আসল খোরাক heterogeneous-documents; Atlas-যুগে vector-search-সহ AI-era feature-rush। Aggregation-pipeline আজ যথেষ্ট SQL-ish — irony-টা লক্ষ্য করুন।

Cassandra/Bigtable-lineage — telemetry আর time-series-র রাজ্য। Apple, Netflix, Uber-scale write-volume; partition-per-device/user timeline; TTL-per-row retention — housekeeping-কে write-path-এই সরিয়ে দেওয়া।

Neo4j — relationship-ই product যেখানে। Fraud-detection (ring- pattern), knowledge-graph, recommendation; GraphQL-resolver-পেছনের fan-out-ও প্রায়ই graph-store।

Polyglot-এর বাস্তব দাম। প্রতিটা বাড়তি store = আরেকটা operational surface — backup, monitoring, security, team-skill। তাই mature-teams- এর প্রশ্ন প্রথমে: “PG দিয়ে কি আদৌ যায়?” — আর ২০২৬-এর উত্তর প্রায়ই “হ্যাঁ”: JSONB-document, pgvector-embedding, TimescaleDB-series, Apache AGE-graph — extension-ecosystem-ই one-database-many-models- এর যুক্তি। NoSQL-এর সবচেয়ে বড় উত্তরাধিকার হয়তো NoSQL-নিজেই নয় — SQL-জগৎকে ভাঙা-চৌচির comfort-zone থেকে তোলা।

যে ভুলগুলো সবাই করে

“NoSQL মানে 'no SQL' — আর SQL-মানে legacy।”

নামটাই historical accident — মূল hashtag ছিল “No Relational” / “Not Only SQL”। আজ MongoDB-র aggregation JSON-SQL-এর মতোই verbose; Cassandra-র CQL literally SQL-syntax; আর PostgreSQL JSONB+CTE document-ও traversal-ও গিলে ফেলে। Movement-এর আসল উত্তরাধিকার data-model-diversity, language-war নয়।

“Schema-less মানে schema নেই — বাগান-বাঁধাই নেই।”

Schema database-থেকে application-এ সরে গেল, হারায়নি — migration হলো deploy-time code-change, validation হলো hand-written layer। Field-name typo এখন silent-null; rename মানে সব doc-এ backfill-script। “Flexibility”-র আসল অর্থ: schema-র দায়িত্ব আপনার কাঁধে — escape নয়।

“Denormalization document-store-এ free — embed করেই ফেলি।”

Embed-করা data duplicate — আর duplicate-এর ক্লাসিক বিল: update- anomaly। User-এর name হাজার order-doc-এ embedded থাকলে rename মানে হাজার doc-rewrite (বা চিরকালের পুরনো-নাম inconsistency)। Embed করুন শুধু child-data যেটা parent-এর সাথে lifecycle-share করে (order-items order-এর সাথেই জন্ম-মৃত্যু) — reference-data নয়।

“Redis-এ data রাখলেই "cache তো" — হারালে ক্ষতি নেই।”

Session, cart, rate-limit-counter, distributed-lock — Redis-এ production-critical state-এর বিশাল ভাগ আজ। Persistence off বা failover-misconfigured হলে restart মানে login-storm সব-user-এ। Cache-vs-source-of-truth distinction architecture-decision, product-name নয়।

বুঝেছেন কি না দেখুন

1

চার পরিবারের প্রতিটার এক-লাইনের core-contract কী?

স্মরণ

KV: key-দিলে opaque-value, আর কিছু নয় (O(1))। Document: nested-doc store + field-path query, schema-on-read। Wide-column: partition-key- scoped sorted-read, ad-hoc নয়। Graph: nodes+typed-edges, traversal first-class।

2

Schema-on-read-এর সুবিধা আর দাম — একটা concrete scenario-তে দুটোই দেখান (যেমন product-catalog)।

যুক্তি

সুবিধা: TV-doc-এ screen_size, book-doc-এ page_count — এক collection- এ heterogeneous-products, নতুন category-তে migration লাগে না, deploy- ই নতুন shape। দাম: “catalog-এর সব item-এ price আছে?” invariant-টা database enforce করে না — app-layer validation; typo-field silent; analytics-এ union-of-shapes-এর জটিলতা। Flexibility = responsibility-shift।

3

Index-free adjacency কেন recursive-SQL-এর চেয়ে deep-traversal-এ জেত? খরচ-বক্ররেখা দুটোর আকৃতি বর্ণনা করুন।

যুক্তি

Relational-এ প্রতিটা hop একটা join/index-probe — global structure-এ খোঁজা; depth d-তে cost বাড়ে branching-factor-এর মতো (recursive CTE set-based হলেও materialize+dedupe overhead)। Graph-এ neighbor-রা node-object-এর direct-reference — hop = pointer-dereference, memory- local; cost ≈ visited-nodes-এর linear। তাই d বাড়লে SQL-কার্ভ steep, traversal-কার্ভ gentle — crossover-এর পরে graph দূরে ছাড়ে।

4

Cassandra-table-এ PRIMARY KEY ((user_id), event_time)। এই চার query- এর কোনটা efficient, কোনটা forbidden-class? (a) user X-এর last-10 events; (b) event_time BETWEEN t1,t2 (global); (c) user X-এর event_time-range; (d) সব user-এর count।

প্রয়োগ

(a) ✓ — partition-key + clustering-order reverse; (b) ✗ forbidden — partition-key নেই, full-cluster scatter; (c) ✓ — partition-scoped range, clustering-sort-এর মূল লাভ; (d) ✗ — global aggregate, OLTP- path-এ নয় (warehouse/stream-aggregate-এ যাবে)। Rule: partition-key query-তে থাকলেই efficient-path।

5

Startup-এ একটা MVP: user-profile + activity-feed + real-time-presence + analytics। Team-এর proposal: চারটা আলাদা best-of-breed store day-one- এ। Review-position দিন — polyglot-এর কোন দাম তারা হিসাবেই নেইনি, আর pragmatic-alternative কী?

ডিজাইন

হিসাবের বাইরে: ৪× operational-surface (backup/security/on-call/skill), cross-store consistency-ঝামেলা (profile-change feed-এ propagate?), early-stage-এ query-pattern এখনো অস্থির — premature specialization। Pragmatic: day-one একটা PG — profile=row, feed=table(+partition later), presence=UNLOGGED/TTL-ish table বা tiny-Redis, analytics=read-replica

  • scheduled-rollup। Scale-signal আসলে প্রতিটা অংশ আলাদা হবে — তখন extract, evidence-ভিত্তিক। Polyglot হবে ভাগ্যের ভরসায় নয়, মাপা সিদ্ধান্তে।

এরপর কী

এরপর কী — module wrap-up

Databases module সম্পূর্ণ। আমরা যাত্রা করেছি disk-এর byte থেকে planet-scale architecture — storage-page-র tuple, buffer-pool-এর victim-selection, B-Tree-র split, MVCC-র xmin/xmax, isolation-এর anomaly-লড়াই, WAL-এর ordering-contract, ARIES-র তিন pass, checkpoint- vacuum-এর housekeeping, replication-এর lag-গণিত, sharding-এর ring, CAP-র বাইনারি মুহূর্ত, আর আজ চার পরিবারের model-diversity। মূল সূত্র একটাই ছিল সব লেসন জুড়ে: প্রতিটা feature-এর একটা লুকানো বিল আছে, আর production-ই সেই বিল-পাঠের পরীক্ষাকেন্দ্র।

এরপর Level 9 — distributed systems: এখানে যেগুলোর teaser দেখেছি, সেগুলোর পূর্ণ গণিত — consensus (Raft/Paxos), quorum-এর intersection-বীজগণিত, logical/vector clock, distributed transactions আর 2PC-র ভঙ্গুরতা, failure-detector আর FLP-impossibility। Database- module-এর প্রতিটা “এক machine-এ সত্য” সেখানে প্রশ্ন হয়ে ফিরবে: “ক’টা machine-এ সত্য বানাব কীভাবে?”

আরও পড়ুন

  • Designing Data-Intensive Applications, Chapters 2–3 — Data Models and Storage Engines — Martin Kleppmann · Document/graph model-এর trade-off আর four-family taxonomy-র সেরা আলোচনা
  • Bigtable: A Distributed Storage System for Structured Data — Fay Chang, Jeffrey Dean, Sanjay Ghemawat, et al. — OSDI 2006 · Wide-column model-টার মূল paper — Cassandra/HBase-এর পূর্বপুরুষ
  • PostgreSQL Documentation — JSON Functions and Operators — PostgreSQL Global Development Group · JSONB containment, GIN indexing — SQL-renaissance অংশের উৎস