এই পর্বে যা শিখবেন
- NoSQL শব্দের আসল অর্থ, এর উৎস, আর কেন এসেছে — সাথে কী দাম দিতে হয়
- একই গ্রাহক আর তাঁর অর্ডার key-value, document, wide-column আর graph মডেলে কীভাবে রাখা হয় — আর প্রতিটিতে কোন প্রশ্ন সহজ, কোনটি কঠিন
- embedding বনাম referencing আর query-first টেবিল ডিজাইনের নিয়ম, ধাপে ধাপে
- SQLite-এর JSON ফাংশন, UPSERT আর recursive CTE দিয়ে চারটি মডেলের অনুকরণ — লাইভ
- BASE, tunable consistency, polyglot persistence আর কখন কোনটি বাছবেন তার চেকলিস্ট
১ · NoSQL মানে কী — একটি পরিবার, একটি ডেটাবেস নয়
পর্ব ৪-এ (ডেটা মডেল) NoSQL-কে এক নজরে দেখেছিলেন। এবার ভেতরে ঢোকার পালা। প্রথমে নামটি। NoSQL শুনে মনে হয় “SQL নেই”, কিন্তু আজকের প্রচলিত অর্থ “Not only SQL” — শুধু টেবিল আর SQL নয়, অন্য আকারেও ডেটা রাখা যায়। নামটি কোনো একটি প্রযুক্তির নয়। এটি একটি ছাতা, যার নিচে খুব আলাদা আলাদা ডেটাবেস দাঁড়িয়ে আছে: Redis আর Neo4j-এর মধ্যে মিল প্রায় ততটুকু, যতটুকু রিকশা আর লঞ্চের মধ্যে — দুটোই যাত্রী বহন করে।
একটি বড় বাজারের কথা ভাবুন। মুদি দোকানের বাকির খাতা কলাম কাটা — নাম, তারিখ, টাকা (relational). ব্যাগ রাখার কাউন্টারে টোকেন নম্বর দিলেই ব্যাগ ফেরত — ভেতরে কী আছে কাউন্টারের লোক জানেন না (key-value). দর্জির দোকানে প্রতিটি খদ্দেরের একটি ফাইল, তাতে মাপ, কাপড়ের নমুনা, আগের অর্ডার সব একসাথে (document). ঘটকের খাতায় কে কার আত্মীয়, কে কার বন্ধু — সম্পর্কই আসল তথ্য (graph). সবাই “তথ্য রাখে”, কিন্তু প্রত্যেকের খাতা নিজের কাজের মাপে বানানো।
নামটি কোথা থেকে এল
ইতিহাসের প্রচলিত বর্ণনা এরকম। ১৯৯৮ সালে Carlo Strozzi নিজের একটি ছোট relational ডেটাবেসের নাম দেন NoSQL, কারণ সেটি SQL ভাষা ব্যবহার করত না — আজকের অর্থের সাথে এর সম্পর্ক সামান্য। আজকের অর্থে শব্দটি ছড়ায় ২০০৯ সালে, যখন San Francisco-তে Johan Oskarsson খোলা-উৎসের distributed, non-relational ডেটাবেস নিয়ে একটি meetup আয়োজন করেন, আর তার নাম (বা hashtag) হিসেবে “NoSQL” বেছে নেওয়া হয়। নামটি নাকি ছিল তাৎক্ষণিক একটি বাছাই — পরে এটি পুরো একটি আন্দোলনের নাম হয়ে যায়।
প্রযুক্তির দিক থেকে দুটি গবেষণা-প্রবন্ধকে সাধারণত প্রধান অনুপ্রেরণা ধরা হয়। Google-এর Bigtable (OSDI ২০০৬): হাজার হাজার সার্ভারে ছড়ানো একটি বিশাল, sorted map — row key, column family আর timestamp দিয়ে ঠিকানা। এ থেকে এসেছে HBase আর wide-column ধারণা। Amazon-এর Dynamo (SOSP ২০০৭): শপিং কার্টের মতো সেবা, যা কখনো “লেখা যাবে না” বলবে না — key দিয়ে পড়া-লেখা, অনেক কপি, আর মাঝে মাঝে কপিগুলোর অমিল মেনে নেওয়া। Cassandra (Facebook-এ শুরু, পরে Apache প্রকল্প) এই দুটির মিশ্রণ: ডেটার আকার Bigtable-ধাঁচের, কপি ছড়ানোর কৌশল Dynamo-ধাঁচের।
🔑 Key-value
key → মান। মান ভেতরে কী, ডেটাবেস প্রায় জানে না। Redis, Amazon DynamoDB, Riak.
📄 Document
প্রতিটি রেকর্ড একটি JSON-ধাঁচের document, ভেতরের ঘর ধরে কোয়েরি। MongoDB, Couchbase, CouchDB.
🧱 Wide-column
partition-এ ভাগ করা, sorted সারির বিশাল টেবিল; বিপুল লেখার জন্য। Apache Cassandra, HBase, ScyllaDB.
🕸️ Graph
node আর সম্পর্ক (relationship) — বহু ধাপের যোগাযোগ খোঁজার জন্য। Neo4j, Amazon Neptune.
“NoSQL ডেটাবেস কেমন?” — প্রশ্নটির সঠিক উত্তর নেই, কারণ প্রশ্নটিই ভুল। জিজ্ঞেস করতে হয়: কোন NoSQL পরিবার, কোন সিস্টেম, কোন কাজের জন্য? এই পর্বের বাকি অংশ এই তিনটি প্রশ্নের উত্তর।
২ · কেন এল — চারটি প্রেরণা আর তার দাম
২০০০-এর দশকের শেষে বড় ওয়েব কোম্পানিগুলোর সমস্যা ছিল একটাই: ডেটা আর ব্যবহারকারী এত বেশি যে একটি সার্ভারে আঁটে না। সমাধানের চেষ্টা থেকে চারটি প্রেরণা দাঁড়ায়।
১. Scale-out
বড় সার্ভার কেনা (vertical scaling) এক সময় থেমে যায় — সবচেয়ে বড় যন্ত্রেরও সীমা আছে, আর দাম লাফিয়ে বাড়ে। Horizontal scaling মানে সস্তা সার্ভার যোগ করে ডেটা ভাগ করা (sharding, পর্ব ৫৪)। NoSQL সিস্টেমগুলো শুরু থেকেই এর জন্য নকশা করা।
২. নমনীয় গঠন
প্রতিটি পণ্যের বৈশিষ্ট্য আলাদা: মোবাইলের RAM, শাড়ির কাপড়, বইয়ের লেখক। টেবিলে সব কলাম আগে ঠিক করা কঠিন; document-এ প্রতিটি রেকর্ড নিজের ঘর রাখতে পারে।
৩. নির্দিষ্ট access pattern
শুধু key দিয়ে মাইক্রোসেকেন্ডে পড়া (session), সেকেন্ডে লাখো লেখা (sensor, message), বা ছয় ধাপের বন্ধুত্ব খোঁজা — এক একটি কাজের জন্য বিশেষ যন্ত্র সাধারণ যন্ত্রের চেয়ে দ্রুত।
৪. ডেভেলপারের সুবিধা
অ্যাপের ভেতরে ডেটা থাকে object আর JSON আকারে। document ডেটাবেসে সেটি প্রায় হুবহু রাখা যায় — ORM আর অনেক JOIN-এর impedance mismatch কমে।
কিন্তু প্রতিটি সুবিধার একটি দাম আছে, আর সেগুলো প্রায়ই পরে চোখে পড়ে:
- দুর্বল transaction (অন্তত শুরুতে): অনেক সার্ভার জুড়ে ACID নিশ্চয়তা দেওয়া ব্যয়বহুল, তাই প্রথম দিকের বহু NoSQL সিস্টেম শুধু একটি key বা একটি document-এর ভেতরে atomic লেখা দিত। দুটি document একসাথে বদলানো অ্যাপের দায়িত্ব।
- ডেটার কপি: JOIN নেই বা দুর্বল, তাই একই তথ্য কয়েক জায়গায় রাখতে হয় — আর বদলালে সব জায়গায় বদলাতে হয়।
- কোয়েরির সীমা: যে প্রশ্নের জন্য ডেটা সাজানো হয়নি, সেটি হয় খুব ধীর, নয়তো একেবারে অসম্ভব। নতুন রিপোর্ট চাইলে প্রায়ই নতুন টেবিল বা নতুন pipeline.
- লুকোনো schema: ডেটাবেস যাচাই না করলেও অ্যাপ ঠিকই ধরে নেয় যে
nameঘরটি আছে। নিয়ম তখন কোডের ভেতরে ছড়ানো।
Schema-on-write বনাম schema-on-read
শেষ কথাটির একটি নাম আছে। relational টেবিলে লেখার সময়েই যাচাই হয় — কলাম আছে কি না, টাইপ ঠিক কি না, NOT NULL মানা হলো কি না। এটি schema-on-write. বেশিরভাগ document আর key-value সিস্টেমে লেখার সময় যাচাই নেই বা ঐচ্ছিক; পড়ার সময় প্রোগ্রাম ঠিক করে কোন ঘরের কী মানে — schema-on-read. তাই “schemaless” কথাটি বিভ্রান্তিকর: schema থাকে, শুধু সেটি ডেটাবেসের বদলে কোডে। পুরোনো document-এ phone নেই আর নতুনটিতে আছে — পড়ার প্রতিটি কোডকে দুটি রূপই সামলাতে হয়।
| বিষয় | Schema-on-write | Schema-on-read |
|---|---|---|
| যাচাই কখন | লেখার সময়, ডেটাবেস করে | পড়ার সময়, অ্যাপ করে |
| নতুন ঘর যোগ | ALTER TABLE, migration (পর্ব ৫০) | নতুন document-এ লিখে দিলেই হলো |
| ভুল ডেটা ঢোকা | কঠিন — constraint আটকায় | সহজ — টাইপের ভুল চুপচাপ ঢোকে |
| পুরোনো রেকর্ড | সবাই একই আকারে | কয়েক প্রজন্মের আকার একসাথে |
মাঝামাঝি পথও আছে: MongoDB-তে collection-এ $jsonSchema দিয়ে যাচাইয়ের নিয়ম দেওয়া যায়, আর PostgreSQL-এর JSONB কলামে CHECK constraint (পর্ব ৩০)। অর্থাৎ পছন্দটি ডেটাবেসের পরিবার নয়, প্রতিটি সংগ্রহের নকশার সিদ্ধান্ত।
৩ · একই দোকান, চারটি মডেলে
তুলনাটি ন্যায্য করতে পুরো পর্বে একই ডেটা ব্যবহার করব — কোর্সের দোকান (ই-কমার্স) ডেটাবেস। এর টেবিল: customers (customer_id, name, city, phone, joined_on), products (product_id, name, category_id, price, stock), orders (order_id, customer_id, order_date, status) আর order_items (order_id, product_id, quantity, unit_price) — কোন অর্ডারে কোন পণ্য কয়টি, কত দামে। আমাদের নায়ক গ্রাহক 1, Rahim Uddin (ঢাকা): তাঁর তিনটি অর্ডার 1001, 1003 আর 1009, মোট ছয়টি লাইন। relational মডেলে এই তথ্য চারটি টেবিলে ছড়ানো, আর জোড়া লাগে JOIN দিয়ে। নিচের চারটি ট্যাবে একই তথ্য চারটি NoSQL আকারে (এগুলো চালানোর কোড নয়, শুধু আকার দেখানো):
key একটি স্ট্রিং, মান একটি “বস্তা”। পড়ার একমাত্র পথ: key জানা থাকা।
customer:1 → {"name":"Rahim Uddin","city":"Dhaka"}
customer:1:orders → [1001, 1003, 1009]
order:1009 → {"date":"2025-10-08","status":"pending",
"items":[[101,1,14500],[110,1,3500]]}
cart:1 → {"102": 2}
“Rahim-এর অর্ডার” পেতে দুই ধাপ: আগে customer:1:orders, তারপর প্রতিটি order:…. “কারা Mobile Phone কিনেছেন?” — key-তে নেই, তাই সব key খুলে দেখা ছাড়া উপায় নেই।
গ্রাহকের একটি document, অর্ডারগুলো তার ভেতরে একটি array — একবার পড়লেই সব:
{
"_id": 1, "name": "Rahim Uddin", "city": "Dhaka",
"orders": [
{ "order_id": 1001, "date": "2025-09-01", "status": "delivered",
"items": [ { "product_id": 101, "qty": 1, "price": 15000 },
{ "product_id": 102, "qty": 2, "price": 1200 } ] },
{ "order_id": 1003, "date": "2025-09-10", "status": "shipped", "items": [ … ] },
{ "order_id": 1009, "date": "2025-10-08", "status": "pending", "items": [ … ] }
]
}ডেটাবেস ভেতরের ঘর বোঝে, তাই orders.items.product_id = 101 ধাঁচের কোয়েরি আর তার ওপর index সম্ভব।
একটি partition = একজন গ্রাহক; ভেতরের সারিগুলো তারিখ অনুযায়ী সাজানো, ডিস্কেও পাশাপাশি:
partition customer_id = 1 ┌ order_date ─┬ order_id ┬ status ────┬ total ┐ │ 2025-10-08 │ 1009 │ pending │ 18000 │ │ 2025-09-10 │ 1003 │ shipped │ 1780 │ │ 2025-09-01 │ 1001 │ delivered │ 17400 │ └─────────────┴──────────┴────────────┴───────┘ partition customer_id = 2 …
“গ্রাহক 1-এর অর্ডার, নতুন আগে” এক ঝটকায়। কিন্তু “সব pending অর্ডার” প্রশ্নটি এই টেবিলের জন্য নয় — তার জন্য আলাদা টেবিল লাগবে (অংশ ৬)।
গ্রাহক, অর্ডার আর পণ্য — সবাই node; “দিয়েছেন”, “আছে” — সম্পর্ক (edge), যার নিজেরও বৈশিষ্ট্য থাকে:
(Rahim:Customer)-[:PLACED]->(1001:Order)-[:CONTAINS {qty:1}]->(Mobile Phone:Product)
└-[:CONTAINS {qty:2}]->(Headphone:Product)
(Rahim)-[:FRIEND]-(Karima:Customer)-[:PLACED]->(1007:Order)-[:CONTAINS]->(Headphone)
প্রশ্ন যত বেশি “সম্পর্ক ধরে হাঁটা” — বন্ধুর বন্ধু, একই জিনিস কেনা অন্য গ্রাহক — graph তত স্বাভাবিক।
একটি শব্দ মনে রাখুন: aggregate. key-value, document আর wide-column — তিনটিই ডেটাকে এমন “গোছা”-য় রাখে যা সাধারণত একসাথে পড়া-লেখা হয় (একজন গ্রাহক আর তাঁর অর্ডার)। এই গোছাই একটি সার্ভারে থাকে, এর ভেতরে লেখা atomic, আর একে ধরেই ডেটা ভাগ হয়। তাই এদের বলা হয় aggregate-oriented — আর graph ব্যতিক্রম, কারণ তার মূল জিনিস গোছা নয়, সম্পর্ক।
৪ · Key-value — key দিয়ে রাখুন, key দিয়ে আনুন
সবচেয়ে সরল মডেল: একটি বিশাল dictionary বা hash table. অপারেশন তিনটি — GET key, PUT key value, DELETE key. সরলতাই শক্তি: key-এর hash দেখে সরাসরি সঠিক সার্ভার আর সঠিক জায়গা, তাই পড়া-লেখা গড়ে O(১)। Redis সব ডেটা RAM-এ রাখে বলে একটি GET সাধারণত এক মিলিসেকেন্ডেরও অনেক কম সময়ে ফেরে। সাধারণ কাজ: লগইনের session, শপিং cart, cache, rate limit (এক মিনিটে কতবার OTP চাওয়া হলো), লিডারবোর্ড।
SET session:a9f3 '{"customer_id":1}' EX 1800 # ৩০ মিনিট পর নিজে মুছে যাবে
GET session:a9f3
TTL session:a9f3 # আর কত সেকেন্ড বাকি
HSET cart:1 101 1 102 2 # cart একটি hash: পণ্য → সংখ্যা
HINCRBY cart:1 102 1 # Headphone আরও একটি
HGETALL cart:1
EXPIRE cart:1 604800 # ৭ দিন কেউ না ছুঁলে cart মুছে যাবেTTL (time to live) key-value store-এর একটি বিশেষ সুবিধা: মেয়াদ শেষে key নিজেই মুছে যায়, তাই পুরোনো session পরিষ্কারের আলাদা কাজ লাগে না। Redis শুধু স্ট্রিং নয়, hash, list, set, sorted set-এর মতো গঠনও রাখে (বিস্তারিত পর্ব ৫৮)। Amazon DynamoDB-ও key দিয়ে পড়ার জন্য তৈরি — প্রতিটি item-এর একটি partition key (ঐচ্ছিকভাবে সাথে একটি sort key):
aws dynamodb put-item --table-name Carts \
--item '{"customer_id": {"N": "1"}, "items": {"M": {"102": {"N": "2"}}}}'
aws dynamodb get-item --table-name Carts --key '{"customer_id": {"N": "1"}}'SQLite-এ এই মডেলের অনুকরণ খুব সহজ: দুই কলামের একটি টেবিল, key-তে PRIMARY KEY. “PUT” মানে নেই থাকলে ঢোকানো, থাকলে বদলানো — অর্থাৎ UPSERT (পর্ব ১৪)। টেবিল kv: k (key), v (মান, JSON স্ট্রিং), expires_at (মেয়াদ; NULL মানে চিরকাল)। শুরুতে দুটি key আছে: session:a9f3 আর cart:3.
INSERT INTO kv (k, v) VALUES ('cart:1', '{"101":1}')
ON CONFLICT(k) DO UPDATE SET v = excluded.v;
INSERT INTO kv (k, v) VALUES ('cart:1', '{"101":1,"102":2}')
ON CONFLICT(k) DO UPDATE SET v = excluded.v;
SELECT v FROM kv WHERE k = 'cart:1';
SELECT k, v FROM kv
WHERE k = 'session:a9f3'
AND (expires_at IS NULL OR expires_at > '2025-10-10 15:10:00');
EXPLAIN QUERY PLAN SELECT v FROM kv WHERE k = 'cart:1';
EXPLAIN QUERY PLAN SELECT k FROM kv WHERE v LIKE '%"102"%';দুটি INSERT-এর প্রতিটিতে “১টি সারি বদলেছে”: প্রথমটিতে নতুন key ঢুকল, দ্বিতীয়টিতে একই key-র সাথে সংঘাত হলো, তাই DO UPDATE পুরো মান বদলে দিল (excluded মানে যে সারিটি ঢোকানোর চেষ্টা হয়েছিল)। GET-এর ফল {"101":1,"102":2}. session-এর কোয়েরি ১৫:১০-এ মেয়াদ বাকি দেখে সারিটি দেয় — Redis-এর TTL এই কাজ নিজেই করে। শেষ দুটি plan-ই আসল শিক্ষা: key দিয়ে খোঁজায় SEARCH kv USING INDEX sqlite_autoindex_kv_1 (k=?), আর মানের ভেতরে খোঁজায় SCAN kv — প্রতিটি সারি খোলা। SQLite তবু প্রশ্নটির উত্তর দেয়; বিশুদ্ধ key-value store-এ “মানের ভেতরে কী আছে” জিজ্ঞেস করার কোনো কমান্ডই সাধারণত থাকে না।
key-value store-এর আরেকটি চেনা সমস্যা: মান একটি গোটা বস্তা, তাই বদলাতে হলে পড়া → বদলানো → পুরোটা লেখা। দুটি browser tab একসাথে এটি করলে একটির বদল হারায় (lost update, পর্ব ৪৭)। প্রতিকার optimistic locking: প্রতিটি মানের সাথে একটি version, আর লেখার শর্ত “version এখনো আমার পড়াটিই”. নিচের ছোট Python প্রোগ্রাম (শুধু stdlib) একটি খেলনা key-value store বানায় — TTL আর version-সহ:
import json
store, now = {}, 0 # key -> (value as text, version, expires_at)
def get(key):
e = store.get(key)
if e and e[2] is not None and e[2] <= now:
del store[key]; e = None # expired: behaves as if it never existed
return (json.loads(e[0]), e[1]) if e else (None, 0)
def put(key, value, ttl=None, if_version=None):
old_version = get(key)[1]
if if_version is not None and old_version != if_version:
return False # someone wrote first: reject (compare-and-set)
store[key] = (json.dumps(value), old_version + 1, now + ttl if ttl else None); return True
put("session:a9f3", {"customer_id": 1}, ttl=1800) # 30 minutes
put("cart:1", {"101": 1}); put("cart:2", {"104": 1}); put("cart:3", {"102": 1})
cart, v = get("cart:1"); print("GET cart:1 ->", cart, "version", v)
cart["102"] = 2; print("tab A put:", put("cart:1", cart, if_version=v))
print("tab B put:", put("cart:1", {"101": 1, "110": 1}, if_version=v)) # still holds v1
print("GET cart:1 ->", get("cart:1"))
now = 1799; print("t=1799 session:", get("session:a9f3")[0])
now = 1800; print("t=1800 session:", get("session:a9f3")[0])
print("carts with 102 (scan", len(store), "keys):",
[k for k, e in store.items() if k.startswith("cart:") and "102" in json.loads(e[0])])আউটপুট:
GET cart:1 -> {'101': 1} version 1
tab A put: True
tab B put: False
GET cart:1 -> ({'101': 1, '102': 2}, 2)
t=1799 session: {'customer_id': 1}
t=1800 session: None
carts with 102 (scan 3 keys): ['cart:1', 'cart:3']
দুটি tab-ই version 1 পড়েছিল। A আগে লিখল, version হলো 2; B-এর শর্ত “version 1” আর সত্য নয়, তাই তার লেখা বাতিল — B আবার পড়ে নতুন করে চেষ্টা করবে, Headphone হারাবে না। session ঠিক ১৮০০ সেকেন্ডে উধাও। শেষ লাইনটি দেখায় “কোন cart-এ 102 আছে” জানতে সব key ঘুরতে হলো (এখানে ৩টি, বাস্তবে কোটি)। Redis-এ এই compare-and-set হয় WATCH/MULTI/EXEC দিয়ে, DynamoDB-তে condition expression দিয়ে।
৫ · Document — embedding না referencing
document ডেটাবেস key-value-এর পরের ধাপ: মান এখন আর অন্ধ বস্তা নয়, একটি গঠনওয়ালা JSON-ধাঁচের document (MongoDB ভেতরে রাখে BSON নামের binary রূপে)। তাই ভেতরের যেকোনো ঘর ধরে খোঁজা, index আর আংশিক বদল সম্ভব। MongoDB-র shell mongosh-এ:
db.customers.insertOne({
_id: 1, name: "Rahim Uddin", city: "Dhaka",
orders: [
{ order_id: 1009, date: ISODate("2025-10-08"), status: "pending",
items: [ { product_id: 101, qty: 1, price: 14500 },
{ product_id: 110, qty: 1, price: 3500 } ] }
]
})
db.customers.find({ "orders.items.product_id": 101 }, { name: 1 })
db.customers.createIndex({ "orders.items.product_id": 1 }) // array-র ভেতরেও index
db.customers.updateOne({ _id: 1, "orders.order_id": 1009 },
{ $set: { "orders.$.status": "shipped" } }) // শুধু একটি ঘর বদলএখানে অর্ডারগুলো গ্রাহকের ভেতরে embed করা (embedding). বিকল্প হলো অর্ডারকে আলাদা collection-এ রাখা আর তাতে customer_id লেখা — referencing, অনেকটা foreign key-র মতো (তবে ডেটাবেস সাধারণত referential integrity যাচাই করে না)। দরকার হলে MongoDB-র aggregation-এ $lookup দুটি collection জোড়া দেয়, কিন্তু সেটি relational JOIN-এর মতো স্বাভাবিক বা সস্তা নয়।
db.orders.insertOne({ _id: 1009, customer_id: 1, date: ISODate("2025-10-08"),
status: "pending", items: [ /* লাইনগুলো এখানেই embed */ ] })
db.orders.createIndex({ customer_id: 1, date: -1 })
db.orders.find({ customer_id: 1 }).sort({ date: -1 })
db.customers.aggregate([
{ $match: { _id: 1 } },
{ $lookup: { from: "orders", localField: "_id", foreignField: "customer_id", as: "orders" } }
])লক্ষ করুন, referencing-এর নকশাতেও অর্ডারের লাইনগুলো অর্ডারের ভেতরে embed করা। একই ডেটাবেসে দুটি পথই চলে; প্রতিটি সম্পর্কের জন্য আলাদা সিদ্ধান্ত। কীভাবে নেবেন, ধাপে ধাপে:
| সংখ্যা | |
| বাড়ার সীমা | |
| একসাথে পড়া | |
| সিদ্ধান্ত |
- অল্প, স্থির, একসাথে পড়া — embed. অর্ডারের লাইন অর্ডার ছাড়া অর্থহীন, সংখ্যায় অল্প, আর অর্ডারের পাতা খুললেই দরকার। এক পড়ায় সব, আর একটি document-এর ভেতরের লেখা atomic — আলাদা transaction লাগে না।
- সীমাহীনভাবে বাড়ে — reference. অর্ডার embed করলে গ্রাহকের document প্রতি অর্ডারে বড় হয়, আর MongoDB-তে একটি document-এর সর্বোচ্চ আকার ১৬ MB. তার আগেই প্রতিটি পড়া-লেখায় পুরো বড় document নাড়াচাড়া করতে হয়। তাই আলাদা collection আর
customer_id-এ index. হোমপেজের জন্য শেষ ৩টির ছোট সারাংশ গ্রাহকের ভেতরে রাখা যায় (subset pattern). - বদলায় এমন জিনিস — reference, কিন্তু ইতিহাস হলে snapshot. পণ্যের বর্তমান stock প্রতিটি অর্ডারে কপি করলে সব কপি মেলাতে হবে। কিন্তু কেনার দিনের দাম আর নাম ইতিহাস — পরে বদলানোর কথা নয় — তাই সেটি কপি করাই সঠিক, ঠিক relational ডিজাইনের
unit_price-এর মতো (পর্ব ৩৮)। - many-to-many — reference. দুই দিকেই embed করলে একই সম্পর্ক দুই জায়গায়, আর একটি বদলালে অন্যটি ভুলে যাওয়ার ঝুঁকি। id-র array রাখুন। “বন্ধুর বন্ধু” ধাঁচের গভীর প্রশ্ন বেশি হলে graph ডেটাবেস ভাবুন (অংশ ৭)।
- নিয়মটি এক লাইনে: যা একসাথে পড়া হয় তা একসাথে রাখুন, যদি তা সীমিত আকারের হয়; যা আলাদা বাড়ে বা আলাদা পড়া হয়, তা আলাদা রাখুন। ডিজাইন ঠিক হয় পাতা আর প্রশ্নের তালিকা দেখে, ডেটার “প্রকৃতি” দেখে নয়।
এবার SQLite-এ document store বানাই। নিচের এডিটর দোকানের চারটি relational টেবিল থেকে customers_doc টেবিল তৈরি করে: id আর doc — প্রতিটি গ্রাহকের একটি JSON document, অর্ডারগুলো তার ভেতরে embed, আর প্রতিটি অর্ডারের ভেতরে তার লাইন। json_object একটি object বানায়, json_group_array অনেক সারিকে একটি array-তে জড়ো করে (পর্ব ৩০); ভেতরের subquery-র ফল যাতে স্ট্রিং না হয়ে JSON হিসেবেই বসে, তাই json(…) দিয়ে মোড়ানো।
CREATE TABLE customers_doc (id INTEGER PRIMARY KEY, doc TEXT NOT NULL);
INSERT INTO customers_doc
SELECT c.customer_id, json_object('name', c.name, 'city', c.city,
'orders', json((SELECT json_group_array(json_object(
'order_id', o.order_id, 'date', o.order_date, 'status', o.status,
'items', json((SELECT json_group_array(json_object('product_id', i.product_id,
'qty', i.quantity, 'price', CAST(i.unit_price AS INTEGER)))
FROM order_items i WHERE i.order_id = o.order_id))))
FROM orders o WHERE o.customer_id = c.customer_id)))
FROM customers c;
SELECT doc FROM customers_doc WHERE id = 1;
SELECT id, length(doc) AS bytes FROM customers_doc ORDER BY id;INSERT-এ ৮টি সারি — প্রতি গ্রাহকে একটি document. Rahim-এর document (এক লাইনে আসে, ফলের বাক্সে ডানে সরিয়ে দেখুন): {"name":"Rahim Uddin","city":"Dhaka","orders":[{"order_id":1001,"date":"2025-09-01","status":"delivered","items":[…]}, …]} — তিনটি অর্ডার, ছয়টি লাইন, ঠিক অংশ ৩-এর document ট্যাবের মতো। আকারের তালিকা: 491, 349, 158, 200, 51, 158, 155, 152 বাইট। Tanvir (5) কোনো অর্ডার দেননি, তাই "orders":[] আর মাত্র ৫১ বাইট; Rahim সবচেয়ে বড়। প্রতিটি নতুন অর্ডারে তাঁর document আরও বড় হবে — embedding-এর “বাড়ার সীমা” প্রশ্নটি এখানে চোখে দেখা যায়।
document-এর ভেতরে প্রশ্ন করতে json_each: এটি একটি table-valued ফাংশন, যা একটি JSON array-র প্রতিটি উপাদানকে একটি সারি বানায় (value কলামে)। একটির ভেতরে আরেকটি json_each দিয়ে অর্ডার, তারপর লাইন — MongoDB-র $unwind-এর মতো:
SELECT json_extract(d.doc, '$.name') AS name,
json_array_length(d.doc, '$.orders') AS n_orders,
SUM(json_extract(it.value, '$.qty') * json_extract(it.value, '$.price')) AS spent
FROM customers_doc d
LEFT JOIN json_each(d.doc, '$.orders') o
LEFT JOIN json_each(o.value, '$.items') it
GROUP BY d.id ORDER BY d.id;
SELECT DISTINCT json_extract(d.doc, '$.name') AS bought_101
FROM customers_doc d, json_each(d.doc, '$.orders') o, json_each(o.value, '$.items') it
WHERE json_extract(it.value, '$.product_id') = 101;প্রথম ফল আট সারি: Rahim Uddin | 3 | 37180, Karima Begum | 2 | 5000, Sabbir Hasan | 1 | 4500, Nusrat Jahan | 1 | 5050, Tanvir Ahmed | 0 | NULL, Farhana Akter | 1 | 3500, Imran Hossain | 1 | 1040, Mitu Rani | 1 | 550. Rahim-এর হিসাব: ১৭৪০০ + ১৭৮০ + ১৮০০০ = ৩৭১৮০ (বাতিল অর্ডারও গোনা হয়েছে — Sabbir-এর ৪৫০০ আসলে cancelled; ফিল্টার চাইলে o.value-এর status দেখুন)। LEFT JOIN না দিলে Tanvir উধাও হতেন — পর্ব ১৯-এর পুরোনো শিক্ষা, এখানেও খাটে। দ্বিতীয় ফল এক সারি: Rahim Uddin. “গ্রাহকের সব কিছু” প্রশ্নটি document মডেলে সহজ — একটি document. কিন্তু “কারা 101 কিনেছেন” প্রশ্নে প্রতিটি document খুলে প্রতিটি অর্ডারের প্রতিটি লাইন দেখতে হলো; বাস্তবে এর জন্য orders.items.product_id-এ multikey index লাগে।
৬ · Wide-column — আগে প্রশ্ন, পরে টেবিল
Cassandra-র টেবিল দেখতে SQL টেবিলের মতো, আর তার ভাষা CQL দেখতে SQL-এর মতো — এই মিলই নতুনদের সবচেয়ে বড় ফাঁদ। ভেতরে এটি একটি distributed, sorted map. মূল ধারণা দুটি। Partition key: এর hash ঠিক করে সারিটি কোন সার্ভারে (আর তার কপিগুলো কোন কোন সার্ভারে) থাকবে; একটি partition-এর সব সারি একসাথে থাকে। Clustering key: partition-এর ভেতরে সারিগুলো কোন ক্রমে সাজানো থাকবে। JOIN নেই, আর partition key ছাড়া কোয়েরি সাধারণত চলেই না। তাই নকশার নিয়ম উল্টে যায় — query-first design: আগে প্রশ্নের তালিকা, তারপর প্রতিটি প্রশ্নের জন্য একটি টেবিল।
CREATE KEYSPACE shop WITH replication =
{'class': 'NetworkTopologyStrategy', 'dhaka': 3}; -- প্রতিটি সারির ৩টি কপি
CREATE TABLE shop.orders_by_customer (
customer_id int,
order_date date,
order_id int,
status text,
total decimal,
customer_name text,
PRIMARY KEY ((customer_id), order_date, order_id) -- (partition), clustering…
) WITH CLUSTERING ORDER BY (order_date DESC, order_id ASC);
SELECT order_date, order_id, status, total
FROM shop.orders_by_customer WHERE customer_id = 1; -- একটি partition: দ্রুত
SELECT * FROM shop.orders_by_customer WHERE status = 'pending';
-- ত্রুটি: partition key নেই; ALLOW FILTERING দিলে চলবে, কিন্তু সব সার্ভার ঘুরে| টেবিল | |
| যে সার্ভার ছোঁয়া হয় | |
| প্রতি অর্ডারে লেখা |
- প্রশ্নের তালিকা আগে। relational ডিজাইনে আগে entity আর normalization, পরে কোয়েরি। এখানে উল্টো: প্রতিটি পাতা কোন প্রশ্ন করে, কত ঘনঘন — এটি না জেনে টেবিল বানানো যায় না।
- WHERE থেকে partition key, ORDER BY থেকে clustering key. Q1 সবসময় একজন গ্রাহক ধরে চায়, তাই customer_id partition key; নতুন আগে চায়, তাই order_date DESC. order_id যোগ না করলে একই দিনের দুটি অর্ডারের primary key এক হতো, আর Cassandra-য় একই key-তে লেখা মানে আগেরটির ওপর লেখা (upsert) — একটি অর্ডার নিঃশব্দে হারাত।
- ডেটা কোথায় যায়। partition key-এর hash (Murmur3) একটি token দেয়, আর token-এর ring-এ তার অবস্থান ঠিক করে কোন সার্ভার মালিক — consistent hashing-এর ধারণা। replication factor ৩ হলে আরও দুটি সার্ভারে কপি। একটি partition-এর সারিগুলো একই জায়গায়, sorted.
- পড়া এক জায়গায়। Q1-এর জন্য coordinator শুধু partition 1-এর একটি (বা কয়েকটি) কপির কাছে যায় আর আগে থেকে সাজানো সারি একটানা পড়ে। সার্ভার ১০টি হোক বা ১০০০টি, এই প্রশ্নের খরচ প্রায় একই — horizontal scaling-এর আসল সুবিধা এটিই।
- নতুন প্রশ্ন = নতুন টেবিল। Q2-তে customer_id নেই, তাই আগের টেবিল অচল। একই ডেটা আরেক আকারে আরেকটি টেবিলে। শুধু
status-কে partition key করলে সব pending অর্ডার একটি বিশাল “hot” partition-এ জমত, তাই মাস যোগ করে ভাগ (bucketing). দাম: প্রতিটি অর্ডার এখন দুটি টেবিলে লিখতে হয়, আর দুটিকে মিলিয়ে রাখা আপনার দায়িত্ব।
SQLite-এ এর কাছাকাছি অনুকরণ: composite PRIMARY KEY-ওয়ালা একটি WITHOUT ROWID টেবিল। এমন টেবিলে সারিগুলো primary key-র B-tree-তেই সাজানো থাকে (পর্ব ৪০-এর clustered index), ঠিক Cassandra partition-এর ভেতরের সারির মতো। orders_by_customer-এ প্রতি অর্ডারে একটি সারি: customer_id, order_date, order_id, status, total (লাইনগুলোর যোগফল, আগে থেকে হিসাব করে রাখা) আর customer_name (গ্রাহকের নামের কপি)।
CREATE TABLE orders_by_customer (
customer_id INTEGER, order_date TEXT, order_id INTEGER,
status TEXT, total INTEGER, customer_name TEXT,
PRIMARY KEY (customer_id, order_date DESC, order_id)
) WITHOUT ROWID;
INSERT INTO orders_by_customer
SELECT o.customer_id, o.order_date, o.order_id, o.status,
(SELECT CAST(SUM(quantity * unit_price) AS INTEGER) FROM order_items i
WHERE i.order_id = o.order_id), c.name
FROM orders o JOIN customers c ON c.customer_id = o.customer_id;
SELECT order_date, order_id, status, total
FROM orders_by_customer WHERE customer_id = 1;
SELECT * FROM orders_by_customer WHERE customer_id = 1 AND order_date >= '2025-10-01';INSERT-এ ১০টি সারি। প্রথম SELECT-এ ORDER BY নেই, তবু ফল নতুন আগে: 2025-10-08 | 1009 | pending | 18000, 2025-09-10 | 1003 | shipped | 1780, 2025-09-01 | 1001 | delivered | 17400 — কারণ সারিগুলো key-র ক্রমেই রাখা, আর SQLite সেই ক্রমে পড়েছে (তবে SQL-এ ORDER BY ছাড়া ক্রমের নিশ্চয়তা নেই; বাস্তব কোডে লিখুন। Cassandra-য় partition-এর ভেতরে clustering ক্রম নিশ্চিত)। দ্বিতীয়টি partition-এর ভেতরে clustering key-র একটি পরিসর — এক সারি: 1 | 2025-10-08 | 1009 | pending | 18000 | Rahim Uddin. দেখুন, অর্ডারের পাতার সব তথ্য — total আর নাম পর্যন্ত — একটি টেবিলেই, কোনো JOIN ছাড়া। এটিই কপি রাখার (denormalization) উদ্দেশ্য।
এবার “কঠিন” প্রশ্নটি — সব pending অর্ডার — আর তার query-first সমাধান। EXPLAIN QUERY PLAN দেখাবে কোনটি এক জায়গায় যায় আর কোনটি পুরো টেবিল ঘোরে:
EXPLAIN QUERY PLAN
SELECT * FROM orders_by_customer WHERE customer_id = 1;
EXPLAIN QUERY PLAN
SELECT * FROM orders_by_customer WHERE status = 'pending';
CREATE TABLE orders_by_status (
status TEXT, order_date TEXT, order_id INTEGER, customer_id INTEGER,
PRIMARY KEY (status, order_date, order_id)) WITHOUT ROWID;
INSERT INTO orders_by_status
SELECT status, order_date, order_id, customer_id FROM orders_by_customer;
SELECT order_date, order_id, customer_id FROM orders_by_status WHERE status = 'pending';
EXPLAIN QUERY PLAN
SELECT * FROM orders_by_status WHERE status = 'pending';প্রথম plan: SEARCH orders_by_customer USING PRIMARY KEY (customer_id=?) — সোজা partition-এ। দ্বিতীয়: SCAN orders_by_customer — প্রতিটি সারি। Cassandra-য় এটিই ALLOW FILTERING: শুধু একটি টেবিল নয়, প্রতিটি সার্ভারের প্রতিটি partition. তারপর নতুন টেবিলে ১০টি সারি কপি হলো, আর pending প্রশ্নের উত্তর দুই সারি, পুরোনো আগে: 2025-09-20 | 1006 | 6, 2025-10-08 | 1009 | 1. শেষ plan: SEARCH orders_by_status USING PRIMARY KEY (status=?). একই ডেটা, দুটি আকার, দুটি প্রশ্ন — প্রতিটি দ্রুত। (SQLite-এ অবশ্য সহজ পথ ছিল status-এ একটি secondary index; Cassandra-রও secondary index আছে, কিন্তু অনেক সার্ভারে ছড়ানো বলে সেটি সাধারণত সীমিত ক্ষেত্রেই ভালো চলে।)
৭ · Graph — সম্পর্কই প্রধান তথ্য
graph ডেটাবেসে দুটি মৌলিক জিনিস: node (গ্রাহক, পণ্য, অর্ডার) আর relationship বা edge (FRIEND, BOUGHT, PLACED). দুটিরই label আর বৈশিষ্ট্য (property) থাকতে পারে — এই নকশার নাম property graph. Neo4j-এর কোয়েরি ভাষা Cypher ছবি আঁকার মতো: (a)-[:FRIEND]->(b) মানে “a থেকে b-তে একটি FRIEND সম্পর্ক”. গ্রাফের মৌলিক ধারণা (BFS, adjacency list) DSA কোর্সের graph পর্বে আছে।
// node আর সম্পর্ক তৈরি
CREATE (r:Customer {id: 1, name: 'Rahim Uddin'}),
(k:Customer {id: 2, name: 'Karima Begum'}),
(p:Product {id: 102, name: 'Headphone'}),
(r)-[:FRIEND]->(k),
(r)-[:BOUGHT {order_id: 1001, qty: 2}]->(p);
// বন্ধুর বন্ধু, যাঁরা এখনো সরাসরি বন্ধু নন — "যাঁদের চিনতে পারেন"
MATCH (me:Customer {id: 1})-[:FRIEND]-(f)-[:FRIEND]-(fof)
WHERE fof <> me AND NOT (me)-[:FRIEND]-(fof)
RETURN DISTINCT fof.name;
// Headphone যাঁরা কিনেছেন, তাঁরা আর কী কিনেছেন
MATCH (:Product {id: 102})<-[:BOUGHT]-(c:Customer)-[:BOUGHT]->(other:Product)
WHERE other.id <> 102
RETURN other.name, count(DISTINCT c) AS customers
ORDER BY customers DESC;relational-এ একই প্রশ্ন করা যায় — একটি edge টেবিল আর recursive CTE (পর্ব ২৫)। পার্থক্য খরচে। relational-এ প্রতিটি ধাপ একটি JOIN, আর প্রতিটি JOIN একটি index lookup — O(log n) — যা পুরো টেবিলের আকারের ওপর নির্ভর করে। native graph ডেটাবেসে (যেমন Neo4j) প্রতিটি node নিজের সম্পর্কগুলোর সরাসরি ঠিকানা রাখে — “index-free adjacency” — তাই এক ধাপ এগোনোর খরচ মোটামুটি সেই node-এর সম্পর্কের সংখ্যার সমান, গোটা গ্রাফ কত বড় তার ওপর নয়। দোকানের গ্রাহকদের মধ্যে বন্ধুত্ব ধরা যাক: Rahim(1)–Karima(2), Rahim–Sabbir(3), Karima–Nusrat(4), Sabbir–Nusrat, Sabbir–Tanvir(5), Nusrat–Farhana(6), Tanvir–Imran(7), Farhana–Mitu(8). Rahim থেকে দুই ধাপ পর্যন্ত হাঁটা, ধাপে ধাপে:
| working table | |
| মোট সারি | |
| পাওয়া মানুষ | |
| প্রতি ধাপের খরচ |
- শুরু: anchor. recursive CTE-র প্রথম অংশ শুধু Rahim-কে বসায় — ০ ধাপ, path-এ শুধু তিনি। path রাখা হয় যাতে কেউ দুবার না আসে (চক্র ঠেকানো, পর্ব ২৫)।
- পাক ১. working table-এর সারি (person 1) edge টেবিলের সাথে JOIN হয়ে তাঁর দুই বন্ধু দেয়: Karima আর Sabbir, ১ ধাপে। এই দুটি সারিই এবার পরের পাকের ইনপুট।
- পাক ২. Karima-র বন্ধু Rahim আর Nusrat; Rahim path-এ আছেন, তাই বাদ। Sabbir-এর বন্ধু Rahim, Nusrat আর Tanvir; Rahim আবার বাদ। Nusrat এসেছেন দুটি আলাদা পথে — একটি Karima হয়ে, একটি Sabbir হয়ে।
- থামা আর জড়ো করা। সবাই ২ ধাপে, তাই
hops < 2শর্তে কেউ এগোয় না; working table খালি, recursion শেষ। GROUP BY প্রতিটি মানুষকে একবার দেখায়, সবচেয়ে কম ধাপ আর পথের সংখ্যাসহ। “যাঁদের চিনতে পারেন” = ২ ধাপে আছেন কিন্তু ১ ধাপে নেই: Nusrat আর Tanvir. - Graph ডেটাবেসে। Cypher-এ
*1..2লিখলেই পরিবর্তনশীল দৈর্ঘ্যের পথ। ফল একই; পার্থক্য ভেতরে — প্রতি ধাপে node নিজের সম্পর্কের তালিকা ধরে হাঁটে। ধাপ যত বাড়ে (৪, ৫, ৬), আর গ্রাফ যত বড় হয়, এই পার্থক্য তত চোখে পড়ে।
এবার আসল কোয়েরি। friends টেবিলে (a, b) — a-র বন্ধু b; প্রতিটি বন্ধুত্ব দুই দিক থেকে দুটি সারিতে রাখা, যাতে JOIN-এ দিক নিয়ে ভাবতে না হয়। নাম আসছে দোকানের customers টেবিল থেকে:
WITH RECURSIVE reach(person, hops, path) AS (
SELECT 1, 0, ',1,'
UNION ALL
SELECT f.b, r.hops + 1, r.path || f.b || ','
FROM reach r JOIN friends f ON f.a = r.person
WHERE r.hops < 2 AND instr(r.path, ',' || f.b || ',') = 0
)
SELECT c.name, MIN(r.hops) AS hops, COUNT(*) AS paths
FROM reach r JOIN customers c ON c.customer_id = r.person
WHERE r.hops > 0
GROUP BY r.person
ORDER BY hops, r.person;চার সারি: Karima Begum | 1 | 1, Sabbir Hasan | 1 | 1, Nusrat Jahan | 2 | 2, Tanvir Ahmed | 2 | 1 — ঠিক stepper-এর মতো। instr(r.path, ',4,') দেখে path-এ ৪ আগে আছে কি না; দুই পাশে কমা না দিলে ',1,12,'-এর ভেতরে “2” ভুল করে মিলে যেত। r.hops < 2-কে ৩ বা ৪ করে দেখুন — Farhana, Imran, Mitu আসবেন, আর সারির সংখ্যা দ্রুত বাড়বে। বড় সামাজিক গ্রাফে প্রতি ধাপে সারি বহু গুণ বাড়ে; এখানেই graph ডেটাবেস আর relational-এর ব্যবধান সবচেয়ে বেশি।
সুপারিশের প্রশ্ন — “যাঁরা Headphone (102) কিনেছেন, তাঁরা আর কী কিনেছেন” — relational-এ চারটি টেবিলের JOIN. Cypher-এর ছবির মতো pattern-টি এখানে order_items → orders → (একই গ্রাহক) → orders → order_items পথ ধরে হাঁটে:
SELECT p.name, COUNT(DISTINCT o2.customer_id) AS customers
FROM order_items i1
JOIN orders o1 ON o1.order_id = i1.order_id
JOIN orders o2 ON o2.customer_id = o1.customer_id
JOIN order_items i2 ON i2.order_id = o2.order_id
JOIN products p ON p.product_id = i2.product_id
WHERE i1.product_id = 102 AND i2.product_id <> 102
GROUP BY p.product_id
ORDER BY customers DESC, p.name;ছয় সারি: Rice 5kg | 2, তারপর DSA Book, Mobile Phone, Mustard Oil 1L, SQL Book, Smart Watch — প্রত্যেকে 1. Headphone কিনেছেন দুজন: Rahim (অর্ডার 1001) আর Karima (1007). Rahim-এর অন্য পণ্য 101, 108, 109, 110; Karima-র 104, 105, 108 — তাই চাল (108) দুজনেরই। COUNT(DISTINCT …) জরুরি: একজন একই পণ্য কয়েকবার কিনলে সাধারণ COUNT তাঁকে কয়েকবার গুনত। কোয়েরিটি এক ধাপের সম্পর্কের জন্য চমৎকার চলে; “বন্ধুরা যা কিনেছেন, তাঁদের বন্ধুরাও যা কিনেছেন” ধাঁচে ধাপ বাড়লে JOIN-এর শিকল লম্বা হয় — graph মডেলের যুক্তি তখন জোরালো।
৮ · কপি রাখা, BASE বনাম ACID আর tunable consistency
চারটি মডেলের তিনটিতেই একটি জিনিস বারবার এসেছে: একই তথ্যের কপি। relational জগতে denormalization (পর্ব ৩৮) একটি সচেতন ব্যতিক্রম; NoSQL-এ এটিই স্বাভাবিক নিয়ম, কারণ JOIN নেই বা ব্যয়বহুল। কিন্তু কপির দাম সবসময় একই — বদলের সময় সব কপি খুঁজে বদলাতে হয়। Rahim নাম বদলালেন:
UPDATE customers SET name = 'Rahim Uddin Khan' WHERE customer_id = 1;
SELECT o.order_id, o.customer_name AS copy, c.name AS source
FROM orders_by_customer o JOIN customers c ON c.customer_id = o.customer_id
WHERE o.customer_name <> c.name;
UPDATE orders_by_customer
SET customer_name = (SELECT name FROM customers WHERE customer_id = 1)
WHERE customer_id = 1;
SELECT COUNT(*) AS stale_copies FROM orders_by_customer o
JOIN customers c ON c.customer_id = o.customer_id WHERE o.customer_name <> c.name;প্রথম UPDATE-এ ১টি সারি বদলাল, কিন্তু কপিগুলো পুরোনো রয়ে গেল: 1009 | Rahim Uddin | Rahim Uddin Khan, 1003 | …, 1001 | … — তিনটি বাসি কপি। দ্বিতীয় UPDATE-এ ৩টি সারি, তারপর stale_copies = 0. এখানে দুটি টেবিল একই ডেটাবেসে, তাই চাইলে একটি transaction-এ দুটি UPDATE মুড়ে দেওয়া যেত। Cassandra-য় কপিগুলো থাকে আলাদা partition-এ, আলাদা সার্ভারে — orders_by_customer, orders_by_status আর আরও কয়েকটি টেবিল। মাঝপথে অ্যাপ crash করলে কিছু কপি নতুন, কিছু পুরোনো। এই কারণেই কপিওয়ালা ডিজাইনে নাম বা দামের মতো যা প্রায় বদলায় না বা ইতিহাস হিসেবে স্থির থাকে, সেটিই কপি করা হয়; ঘনঘন বদলানো জিনিস (stock) কপি করা বিপজ্জনক।
BASE — ACID-এর উল্টো দিকের নকশা
অনেক সার্ভারে কপি থাকলে আর নেটওয়ার্ক ভাগ হলে (পর্ব ৫৪-এর CAP) সিস্টেমকে বাছতে হয়: সব কপি মিল না হওয়া পর্যন্ত উত্তর দেওয়া বন্ধ, নাকি হাতের কপি থেকে উত্তর দেওয়া। দ্বিতীয় পথের দর্শনের নাম BASE: Basically Available (প্রায় সবসময় সাড়া দেয়), Soft state (অবস্থা কিছুক্ষণ অস্থির থাকতে পারে), Eventually consistent (নতুন লেখা থামলে সব কপি একসময় মিলবে — eventual consistency). শব্দটি রসায়নের অম্ল-ক্ষারের খেলা — ACID-এর বিপরীতে BASE; ধারণাটি ১৯৯০-এর দশকের শেষে Eric Brewer-দের গবেষণায় দেখা যায়, আর ২০০৮ সালে eBay-এর Dan Pritchett-এর একটি প্রবন্ধে জনপ্রিয় হয় বলে সাধারণত বলা হয়।
| বিষয় | ACID | BASE |
|---|---|---|
| অগ্রাধিকার | সঠিকতা: প্রতিটি পড়ায় সর্বশেষ commit | সাড়া: নেটওয়ার্ক ভাগেও লেখা-পড়া চালু |
| অমিল | দেখা যায় না (isolation level অনুযায়ী) | কিছুক্ষণ দেখা যেতে পারে |
| দ্বন্দ্ব | lock বা abort করে আগেই ঠেকানো | পরে মেলানো — যেমন শেষ লেখা জেতে |
| উপযুক্ত | টাকা, মজুদ, টিকিটের আসন | like-এর সংখ্যা, ভিউ, ফিড, cart |
তবে ২০২০-এর দশকে বিভাজনটি আর সাদা-কালো নয়। MongoDB প্রথম থেকেই একটি document-এর লেখা atomic রাখত; ৪.০ সংস্করণ (২০১৮) replica set-এ multi-document transaction আনে, আর ৪.২ (২০১৯) sharded cluster-এও। DynamoDB ২০১৮ সালের শেষে transaction API যোগ করে। আবার উল্টো দিকে Google Spanner, CockroachDB-র মতো distributed SQL ডেটাবেস বহু সার্ভারে ছড়িয়েও ACID দেয়। তাই আজ প্রশ্নটি “NoSQL মানে transaction নেই” নয়, বরং: কোন পরিসরে (একটি document, একটি partition, পুরো cluster) কোন নিশ্চয়তা, কত দামে?
Tunable consistency — প্রতিটি কোয়েরিতে বাছাই
Dynamo-ধাঁচের সিস্টেমে (Cassandra, ScyllaDB, Riak) প্রতিটি key-র N-টি কপি। লেখায় কয়টি কপির “পেয়েছি” লাগবে (W), পড়ায় কয়টি কপির উত্তর লাগবে (R) — দুটিই প্রতিটি কোয়েরিতে বাছা যায়: tunable consistency. মূল নিয়মটি পর্ব ৫২ আর ৫৪-এর quorum-এর আত্মীয়: R + W > N হলে পড়ার কপিগুলো আর লেখার কপিগুলো অন্তত একটিতে মিলবেই (pigeonhole নীতি: দুটি দলের মোট সদস্য N-এর বেশি হলে N জনের মধ্যে অন্তত একজন দুই দলেই)। সেই কপিতে সর্বশেষ লেখা আছে, আর timestamp দেখে নতুনটি বেছে নেওয়া হয়।
CONSISTENCY QUORUM; -- N = 3 হলে ২টি কপি: লেখায় W = 2, পড়ায় R = 2
SELECT * FROM shop.orders_by_customer WHERE customer_id = 1;
CONSISTENCY ONE; -- দ্রুততম, কিন্তু পুরোনো মান ফিরতে পারে
-- একাধিক ডেটা সেন্টারে: LOCAL_QUORUM = শুধু নিজের সেন্টারের majorityনিচের simulation-এ N = ৩. লেখার সময় কপি ৩ নাগালের বাইরে; পড়ার সময় কপি ১ বন্ধ আর ৩ ফিরে এসেছে — সবচেয়ে খারাপ সময়ে সবচেয়ে খারাপ কপি। প্রতিটি (W, R) জোড়ায় পড়া কী ফেরত দেয়?
N = 3 # every key is stored on 3 replicas
def run(W, R):
replicas = [{"cart:1": (1, "old")} for _ in range(N)] # (timestamp, value)
up = [True, True, False] # replica 3 is unreachable during the write
acks = 0
for i in range(N):
if up[i] and acks < W: # simplified: stop after W acks;
replicas[i]["cart:1"] = (2, "new"); acks += 1 # others get it later (repair)
ok = acks >= W
up = [False, True, True] # now replica 1 is down, 3 is back
asked = [i for i in range(N) if up[i]][:R]
answers = [replicas[i]["cart:1"] for i in asked]
ts, value = max(answers) # newest timestamp wins
overlap = "R+W>N" if R + W > N else "R+W<=N"
print(f"W={W} R={R} ({overlap}): write {'OK' if ok else 'FAILED'}, "
f"read from {[i + 1 for i in asked]} saw {[a[1] for a in answers]} -> returns '{value}'")
for W, R in [(1, 1), (2, 1), (1, 2), (2, 2), (3, 1)]:
run(W, R)আউটপুট:
W=1 R=1 (R+W<=N): write OK, read from [2] saw ['old'] -> returns 'old' W=2 R=1 (R+W<=N): write OK, read from [2] saw ['new'] -> returns 'new' W=1 R=2 (R+W<=N): write OK, read from [2, 3] saw ['old', 'old'] -> returns 'old' W=2 R=2 (R+W>N): write OK, read from [2, 3] saw ['new', 'old'] -> returns 'new' W=3 R=1 (R+W>N): write FAILED, read from [2] saw ['new'] -> returns 'new'
তিনটি শিক্ষা। (১) W=1, R=1-এ লেখা “সফল” হলেও পড়া পুরোনো মান দিল — eventual consistency-র চেনা চেহারা। (২) W=2, R=1-এ নতুন মান এল, কিন্তু ভাগ্যে: পড়াটি কপি ২-তে গিয়েছিল; কপি ৩-এ গেলে পুরোনো আসত। R + W ≤ N-এ নিশ্চয়তা নেই, শুধু সম্ভাবনা। (৩) W=2, R=2 (QUORUM/QUORUM): দুই কপির উত্তর ভিন্ন, আর timestamp-এ নতুনটি জিতল — মিলে যাওয়া কপিটিই (২) বাঁচাল। আর W=3 দেখায় দাম: একটি কপি বন্ধ থাকলেই লেখা ব্যর্থ — অথচ কপি ১ আর ২-তে লেখাটি থেকে গেছে! Cassandra-য় ব্যর্থ লেখা rollback হয় না; এজন্য retry-যোগ্য (idempotent) লেখা জরুরি। মনে রাখুন, “শেষ timestamp জেতে” (last write wins) নিজেই একটি আপস: দুটি client প্রায় একসাথে লিখলে একটির লেখা নিঃশব্দে হারায়।
৯ · তুলনা, polyglot persistence আর কীভাবে বাছবেন
| মডেল | ডেটার আকার | কোয়েরির ক্ষমতা | Transaction | Scaling | সাধারণ ব্যবহার |
|---|---|---|---|---|---|
| Relational | টেবিল, normalize করা | সবচেয়ে বেশি: যেকোনো JOIN, aggregate, ad-hoc প্রশ্ন | পূর্ণ ACID | সাধারণত বড় যন্ত্র + read replica; sharding কঠিন কিন্তু সম্ভব | লেনদেন, হিসাব, প্রায় সব ব্যবসায়িক অ্যাপ |
| Key-value | key → অস্বচ্ছ মান | সবচেয়ে কম: key দিয়ে get/put | একটি key-তে atomic; কিছু সিস্টেমে সীমিত multi-key | সবচেয়ে সহজ — key-এর hash ধরে ভাগ | session, cart, cache, rate limit |
| Document | JSON-ধাঁচের গাছ | ভালো: ভেতরের ঘরে filter, index, aggregation; JOIN দুর্বল | একটি document-এ atomic; MongoDB ৪.০+ multi-document | document id বা shard key ধরে | catalog, CMS, প্রোফাইল, নমনীয় ফর্ম |
| Wide-column | partition → sorted সারি | সীমিত: partition key দিয়ে, clustering key-র পরিসর | সারি/partition স্তরে; lightweight transaction (Paxos) সীমিত | বহু সার্ভার, বিপুল লেখা, বহু ডেটা সেন্টার | message, IoT, event log, সময়-ক্রমের ডেটা |
| Graph | node + সম্পর্ক | সম্পর্ক ধরে বহু ধাপ হাঁটায় সেরা; বড় aggregate-এ দুর্বল | অনেকগুলোতে (যেমন Neo4j) ACID | ভাগ করা কঠিন — সম্পর্ক সার্ভার পেরিয়ে যায় | সুপারিশ, প্রতারণা শনাক্ত, সামাজিক নেটওয়ার্ক, knowledge graph |
Polyglot persistence — একটি দোকানে কয়েকটি ডেটাবেস
বড় সিস্টেমে প্রায়ই একটি ডেটাবেস সব কাজ করে না। প্রতিটি কাজে সবচেয়ে উপযুক্ত যন্ত্র — polyglot persistence. ধরুন দেশের একটি বড় অনলাইন দোকান:
ক্রেতা ── খোঁজেন "panjabi" ──► Elasticsearch (পণ্য খোঁজা) │ ├─ cart-এ যোগ ───────────► Redis (cart, session, TTL) │ ├─ checkout ─────────────► PostgreSQL (orders, payments │ ACID transaction, মজুদ কমানো) │ │ CDC / event stream │ ▼ │ ┌─ Elasticsearch-এ stock হালনাগাদ │ └─ graph DB-তে BOUGHT সম্পর্ক └─ "আরও যা পছন্দ হতে পারে" ◄── graph DB (সুপারিশ)
সত্যের উৎস (source of truth) একটিই — অর্ডার আর টাকা PostgreSQL-এ। বাকিরা তার থেকে তৈরি কপি, যা CDC বা event stream দিয়ে হালনাগাদ হয়। অ্যাপ নিজে দুই জায়গায় লিখলে (dual write) মাঝপথের crash-এ কপিগুলো চুপচাপ আলাদা হয়ে যায়। খোঁজার ইঞ্জিন Elasticsearch Lucene-এর inverted index-এর ওপর গড়া — শব্দ থেকে document-এর তালিকা (পর্ব ৫৯)। দাম মনে রাখুন: প্রতিটি নতুন ডেটাবেস মানে নতুন backup, monitoring, নিরাপত্তা, আপগ্রেড আর দক্ষ মানুষ। ছোট দল আর ছোট অ্যাপের জন্য এই বোঝা প্রায়ই সুবিধার চেয়ে বড়।
কীভাবে বাছবেন — চেকলিস্ট
- relational দিয়ে শুরু করুন, যদি নিচের কোনো স্পষ্ট কারণ না থাকে। একটি PostgreSQL বা MySQL সার্ভার বেশিরভাগ অ্যাপের জন্য বহু বছর যথেষ্ট, আর ভুল ডিজাইন পরে সারানোও সহজ।
- আগে access pattern লিখুন: প্রতিটি পাতা কোন প্রশ্ন করে, সেকেন্ডে কতবার, কত লেখা, কত পড়া। প্রশ্নগুলো অজানা বা ঘনঘন বদলায় (রিপোর্ট, analytics) — relational-এর ad-hoc SQL জেতে।
- মাপ সত্যিই কি একটি সার্ভার ছাড়ায়? সেকেন্ডে লাখো লেখা, বহু TB, বহু অঞ্চলে একসাথে লেখা — wide-column বা distributed ডেটাবেস ভাবুন।
- কোন নিশ্চয়তা লাগবে? টাকা, মজুদ, আসন সংরক্ষণে কঠোর transaction; like-এর সংখ্যায় eventual consistency চলে।
- ডেটার আকার কেমন? একসাথে পড়া স্বয়ংসম্পূর্ণ গোছা — document; বহু ধাপের সম্পর্ক — graph; শুধু key দিয়ে অতি দ্রুত — key-value.
- দল আর পরিচালনা: কে চালাবে, backup কীভাবে, managed সেবা (পর্ব ৬১) আছে কি না।
Multi-model — সীমানা মুছে যাচ্ছে
প্রবণতাটি স্পষ্ট: প্রতিটি পক্ষ অন্যের ভালো দিক নিজের ভেতরে নিচ্ছে। PostgreSQL-এ hstore (একটি কলামে key-value জোড়া) অনেক পুরোনো, আর ৯.৪ সংস্করণ (২০১৪) থেকে JSONB, যার ওপর GIN index দিয়ে document-এর ভেতরে দ্রুত খোঁজা যায় (পর্ব ৩০)। SQL:2023 standard-এ SQL/PGQ যোগ হয়েছে — SQL-এর ভেতরে property graph-এর pattern কোয়েরি — আর ২০২৪ সালে graph কোয়েরির আলাদা ISO ভাষা GQL প্রকাশিত হয়েছে। অন্য দিকে Redis-এ module দিয়ে JSON document আর খোঁজার সুবিধা যোগ হয়েছে, Cassandra দিয়েছে SQL-ধাঁচের CQL, আর Azure Cosmos DB বা ArangoDB-র মতো multi-model ডেটাবেস একই ইঞ্জিনে কয়েকটি মডেল দেয়। তাই সিদ্ধান্ত ক্রমশ “কোন পরিবার” থেকে সরে “এই কাজের জন্য কোন আকার আর কোন নিশ্চয়তা”-য় আসছে।
আরও গভীরে: Cassandra, HBase, ScyllaDB আর RocksDB-ভিত্তিক বহু সিস্টেম লেখা রাখে LSM tree-এ: প্রতিটি লেখা আগে commit log-এ যোগ হয় আর RAM-এর একটি sorted টেবিলে (memtable) বসে; ভরে গেলে সেটি ডিস্কে একটি অপরিবর্তনীয় sorted ফাইল (SSTable) হয়ে নামে, আর পেছনে compaction পুরোনো ফাইলগুলো মিলিয়ে নতুন করে লেখে। তাই লেখা প্রায় সবসময় ক্রমিক এবং খুব দ্রুত, কিন্তু একটি পড়ায় কয়েকটি ফাইল দেখতে হতে পারে — Bloom filter এড়িয়ে যাওয়ার কাজটি সহজ করে। মোছাও একটি লেখা (“tombstone”), যা compaction-এর আগে পর্যন্ত জায়গা নেয় — ঘনঘন মোছা হয় এমন Cassandra টেবিলে এটি পরিচিত পারফরম্যান্স-সমস্যা। পর্ব ৪০-এর B+ tree জায়গায় বদলায় (in-place), LSM জমিয়ে পরে মেলায় — লেখা বনাম পড়ার এই দাম-দেওয়া-নেওয়াই দুই ঘরানার মূল পার্থক্য।
১০ · AI-এর যুগে NoSQL ডিজাইন
AI-কে “আমার অ্যাপের জন্য MongoDB schema দাও” বললে সে সুন্দর JSON লিখে দেয়। সমস্যা হলো, NoSQL ডিজাইন নির্ভর করে আপনার প্রশ্ন আর মাপের ওপর — যা AI জানে না, আর না জানলেও আত্মবিশ্বাসী উত্তর দেয়। সাধারণ ভুল:
| AI-এর ভুল | ফল | কীভাবে ধরবেন |
|---|---|---|
| Cassandra-য় relational ধাঁচের normalized টেবিল, তারপর ALLOW FILTERING | ছোট ডেটায় চলে, বড় হলে প্রতিটি কোয়েরি সব সার্ভার ঘোরে | প্রতিটি কোয়েরির WHERE-এ partition key আছে কি না দেখুন |
| সীমাহীন array embed (গ্রাহকের ভেতরে সব অর্ডার, পোস্টের ভেতরে সব comment) | document ফুলতে থাকে, শেষে আকারের সীমা | প্রতিটি array-র সর্বোচ্চ আকার জিজ্ঞেস করুন |
| পুরোনো তথ্য: “MongoDB-তে transaction নেই”, বা উল্টো, “NoSQL মানেই দ্রুত” | ভুল প্রযুক্তি বাছাই | নিজের সংস্করণের ডকুমেন্টেশন দেখুন |
| না-থাকা কমান্ড বা option বানিয়ে লেখা | কোড চলে না, বা চুপচাপ ভুল আচরণ | পরীক্ষামূলক সার্ভারে চালিয়ে দেখুন |
| consistency level বা write concern-এর কথা না বলা | default-এ পুরোনো মান পড়া বা লেখা হারানো | প্রতিটি লেখা-পড়ার নিশ্চয়তা আলাদা করে লিখুন |
| ছোট CRUD অ্যাপে কয়েকটি NoSQL সুপারিশ | পরিচালনার বোঝা, কোনো লাভ ছাড়া | “relational দিয়ে কেন নয়?” জিজ্ঞেস করুন |
AI-কে প্রযুক্তির নাম নয়, প্রশ্ন, মাপ আর নিশ্চয়তা দিন — আর বিকল্পের সাথে তুলনা চান:
AI-এর ডিজাইন মানার আগে — চেকলিস্ট
পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:
- NoSQL-এর চার প্রধান ধরন কী কী? প্রতিটির একটি উদাহরণ? — key-value (Redis), document (MongoDB), wide-column (Cassandra), graph (Neo4j).
- ACID আর BASE-এর পার্থক্য? — ACID: প্রতিটি transaction atomic, consistent, isolated, durable; BASE: প্রায় সবসময় সাড়া, কিছুক্ষণ অস্থির অবস্থা, একসময় সব কপি মেলে।
- Embedding আর referencing কখন? — অল্প, সীমিত আর একসাথে পড়া হলে embed; সীমাহীনভাবে বাড়ে, আলাদা পড়া হয় বা many-to-many হলে reference.
- Cassandra-য় partition key আর clustering key-র কাজ? — partition key ঠিক করে ডেটা কোন সার্ভারে; clustering key partition-এর ভেতরে সারির ক্রম।
- N = ৩ হলে কোন R আর W-তে সর্বশেষ লেখা পড়া নিশ্চিত? — R + W > ৩, যেমন R = ২, W = ২ (QUORUM).
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১Amazon-এর Dynamo প্রবন্ধ (২০০৭) শপিং কার্টকে কেন “সবসময় লেখা যায়” এমন করতে চেয়েছিল? এর দাম কী ছিল, আর আজকের DynamoDB কি একই নকশা?
Dynamo প্রবন্ধের লেখকদের যুক্তি ছিল ব্যবসায়িক: ক্রেতা “Add to cart” চাপলে যদি ত্রুটি দেখায়, বিক্রিটি হয়তো হারায়। তাই সার্ভার মরুক বা নেটওয়ার্ক ভাগ হোক, কার্টে লেখা কখনো প্রত্যাখ্যাত হবে না — consistency-র চেয়ে availability অগ্রাধিকার পেল। প্রতিটি key কয়েকটি সার্ভারে কপি হয় consistent hashing দিয়ে, লেখা আর পড়ায় N, R, W বাছা যায়, আর কোনো কপি বন্ধ থাকলে অন্য একটি সার্ভার সাময়িকভাবে লেখা রেখে দেয় (hinted handoff).
দাম হলো দ্বন্দ্ব। নেটওয়ার্ক ভাগের দুই দিকে একই কার্টে দুটি আলাদা বদল হলে দুটি “সমান্তরাল” সংস্করণ তৈরি হয়। Dynamo এদের চিনত vector clock দিয়ে, আর নিজে না মিলিয়ে দুটিই অ্যাপকে ফেরত দিত। কার্টের ক্ষেত্রে মেলানোর নিয়ম ছিল দুই সংস্করণের পণ্যগুলোর union — যোগ করা কোনো পণ্য কখনো হারায় না। কিন্তু প্রবন্ধটি নিজেই স্বীকার করে, এতে মুছে ফেলা পণ্য কখনো কখনো আবার ফিরে আসতে পারে। ক্রেতার কাছে এটি সামান্য বিরক্তি; ব্যাংকের হিসাবে একই আচরণ হতো বিপর্যয়।
আজকের Amazon DynamoDB নাম আর কিছু ধারণা পেয়েছে, কিন্তু ভেতরের নকশা অনেক আলাদা — ২০২২ সালের USENIX ATC-তে প্রকাশিত প্রবন্ধে বর্ণিত হয়েছে, প্রতিটি partition-এর কপিগুলো Multi-Paxos দিয়ে একটি leader বাছে, আর strongly consistent পড়া ও transaction-ও দেওয়া হয়। অর্থাৎ পরিচালিত সেবা হিসেবে Amazon গ্রাহকদের কাছে vector clock-এর জটিলতা ঠেলে না দিয়ে সরলতর নিশ্চয়তা বেছে নিয়েছে।
মূল উপলব্ধি: eventual consistency একটি ব্যবসায়িক সিদ্ধান্ত, শুধু প্রযুক্তিগত নয়। কার্টে “মোছা জিনিস ফিরে আসা” মেনে নেওয়া যায়; টাকায় নয়। কোন ডেটা কোন ভুল সইতে পারে — এই প্রশ্নই ঠিক করে কোন নিশ্চয়তা কিনবেন।
প্র ০২NoSQL আন্দোলনের পথিকৃৎ Google নিজেই পরে Spanner-এর মতো SQL আর transaction-ওয়ালা ডেটাবেস বানাল, আর MongoDB যোগ করল multi-document transaction. কেন এই “ফিরে আসা”?
Bigtable একটি row-এর ভেতরে atomic লেখা দিত, কিন্তু একাধিক row বা টেবিল জুড়ে transaction দিত না। Google-এর বিজ্ঞাপন ব্যবস্থার মতো সিস্টেমে, যেখানে টাকার হিসাব জড়িত, ডেভেলপারদের নিজেদের কোডে এই নিশ্চয়তা বানাতে হতো — ভুলপ্রবণ আর ধীর কাজ। ২০১২ সালের Spanner প্রবন্ধ (OSDI) এই অভিজ্ঞতা থেকেই লেখা: সারা পৃথিবীতে ছড়ানো ডেটায় SQL-ধাঁচের কোয়েরি, বহু সার্ভার জুড়ে transaction আর সুনির্দিষ্ট সময়ের ধারণা (TrueTime). প্রবন্ধে যুক্তিটি মোটামুটি এরকম: transaction-এর অতিরিক্ত ব্যবহারে পারফরম্যান্স-সমস্যা হলে সেটি ডেভেলপাররা সারাতে পারেন, কিন্তু transaction না থাকলে প্রতিটি অ্যাপকে সারাক্ষণ তার অভাব ঘিরে কোড লিখতে হয় — দ্বিতীয়টি খারাপ।
অন্য দিকেও একই ধারা। Cassandra নিজের ভাষা দিয়েছে SQL-ধাঁচের CQL; Couchbase দিয়েছে SQL++ (আগে N1QL); MongoDB ৪.০ (২০১৮) থেকে replica set-এ আর ৪.২ থেকে sharded cluster-এ multi-document transaction. কারণ ব্যবহারকারীরা দেখলেন, প্রথম অ্যাপটি সহজ হলেও পরের রিপোর্ট, পরের নিয়ম আর পরের সঠিকতার দাবি ঘুরেফিরে সেই পুরোনো চাহিদায় আসে: ad-hoc প্রশ্ন, constraint আর একাধিক রেকর্ডের atomic বদল।
পর্ব ৪-এর আরও গভীরে অংশের Stonebraker আর Pavlo-র ২০২৪ সালের প্রবন্ধও একই সিদ্ধান্তে আসে — নতুন মডেলগুলোর ভালো অংশ শেষে relational ব্যবস্থায় মিশে যায়, আর NoSQL ব্যবস্থাগুলো relational-এর সুবিধা যোগ করে। তবে এর মানে এই নয় যে NoSQL “ব্যর্থ”: বিপুল মাপ, নমনীয় document আর key-value cache-এর জায়গা স্থায়ী হয়েছে।
মূল উপলব্ধি: transaction আর ঘোষণামূলক কোয়েরি ভাষা বিলাসিতা নয়, ডেভেলপারের সময় আর সঠিকতা বাঁচানোর যন্ত্র। মাপের চাপে এগুলো সাময়িক ছাড়া যায়, কিন্তু প্রযুক্তি পরিপক্ব হলে এরা ফিরে আসে।
প্র ০৩“MongoDB schemaless, তাই migration লাগে না” — কথাটি কতটা সত্য? তিন বছর পুরোনো একটি document collection-এ বাস্তবে কী ঘটে?
শুরুতে কথাটি সত্যি মনে হয়। নতুন একটি ঘর দরকার — শুধু নতুন document-এ লিখে দিন; ALTER TABLE নেই, রাতের বেলা migration চালানোর ভয় নেই। কিন্তু schema উধাও হয়নি, শুধু জায়গা বদলেছে। অ্যাপের প্রতিটি পড়ার কোড ধরে নেয় কিছু ঘর আছে আর তাদের টাইপ কী। তিন বছর পর collection-এ একসাথে থাকে কয়েক প্রজন্মের document: প্রথম বছরের phone একটি স্ট্রিং, দ্বিতীয় বছরের phones একটি array, আর কোনো এক bug-এর সময় লেখা কিছু document-এ দাম স্ট্রিং আকারে (“1200”). প্রতিটি পড়ার কোডকে সব রূপ সামলাতে হয়, আর রিপোর্ট লেখা মানুষ জানেন না কোন ঘর কবে থেকে আছে।
এর প্রতিকার হিসেবে কয়েকটি অভ্যাস প্রচলিত। এক, document-এ একটি schema_version ঘর রাখা, আর পড়ার সময় পুরোনো সংস্করণকে নতুনে রূপান্তর করে প্রয়োজনে আবার লিখে দেওয়া (lazy migration). দুই, পটভূমিতে ধীরে ধীরে সব document হালনাগাদের script — অর্থাৎ migration ঠিকই, শুধু অন্য রূপে। তিন, ডেটাবেসেই যাচাই: MongoDB ৩.৬ সংস্করণ থেকে collection-এ $jsonSchema দিয়ে আবশ্যক ঘর আর টাইপ বেঁধে দেওয়া যায়, যা schema-on-write-এর দিকে ফেরা।
relational দিকেও একই আপস উল্টো দিক থেকে দেখা যায়: PostgreSQL-এর JSONB কলামে নমনীয় অংশ রেখে মূল ঘরগুলো সাধারণ কলামে রাখা, আর দরকারে CHECK constraint. অর্থাৎ ভালো নকশায় কিছু অংশ কঠোর, কিছু অংশ নমনীয় — পুরোপুরি schemaless বা পুরোপুরি কঠোর, কোনোটিই নয়।
মূল উপলব্ধি: schema-কে এড়ানো যায় না, শুধু কোথায় রাখবেন আর কে যাচাই করবে তা বাছা যায়। ডেটাবেস যাচাই না করলে সেই দায় পড়ে প্রতিটি পাঠক-কোডের ওপর, চিরকালের জন্য।
প্র ০৪Discord তাদের বার্তা রাখার ব্যবস্থা MongoDB থেকে Cassandra-য়, আর পরে ScyllaDB-তে সরিয়েছে বলে নিজেদের প্রকৌশল ব্লগে লিখেছে। এই গল্পে partition key-র নকশা কেন কেন্দ্রীয়?
কোম্পানির ২০১৭ সালের ব্লগ লেখা অনুযায়ী, বার্তার সংখ্যা বাড়ার সাথে একটি MongoDB সেটআপের index আর RAM-এ আঁটছিল না, আর পড়ার সময় অনিশ্চিত হয়ে পড়ছিল। তাদের মূল প্রশ্ন ছিল একটাই: “এই চ্যানেলের সাম্প্রতিক বার্তাগুলো, নতুন আগে” — পরিষ্কার একটি access pattern. Cassandra-য় স্বাভাবিক নকশা মনে হয় partition key = channel_id, clustering key = message_id (যেটি সময়-ক্রমিক)। কিন্তু খুব সক্রিয় একটি চ্যানেলে বছরের পর বছর বার্তা জমে partition বিশাল হয়ে যেত, আর একটি বিশাল partition একটি সার্ভারেই থাকে — ভাগ করা যায় না।
তাই তাঁরা partition key করেন (channel_id, bucket) — bucket একটি নির্দিষ্ট সময়-জানালা (কয়েক দিনের)। ফলে একটি চ্যানেলের বার্তা কয়েকটি মাঝারি partition-এ ভাগ হয়, আর “সাম্প্রতিক বার্তা” পড়তে সাধারণত শেষ এক-দুটি bucket যথেষ্ট। এটি ঠিক এই পর্বের orders_by_status-এ মাস যোগ করার কৌশল। ২০২৩ সালের আরেকটি লেখায় তাঁরা জানান, ট্রিলিয়ন-মাপের বার্তায় Cassandra-র garbage collection-এর বিরতি আর hot partition-এর সমস্যা মেটাতে ScyllaDB-তে (Cassandra-সামঞ্জস্যপূর্ণ, C++-এ লেখা) সরেছেন, আর সামনে একটি ডেটা-সেবা স্তর বসিয়েছেন যা একই partition-এর একসাথে আসা অনুরোধ এক করে দেয়।
লক্ষ করুন, দুই ধাপের কোনোটিতেই ডেটা মডেল মূলত বদলায়নি — বদলেছে partition-এর আকার আর তার চারপাশের পরিচালনা। wide-column ডিজাইনে partition key বাছাই প্রায় অপরিবর্তনীয় সিদ্ধান্ত: ভুল হলে সব ডেটা নতুন টেবিলে আবার লিখতে হয়।
মূল উপলব্ধি: query-first ডিজাইনে দুটি প্রশ্ন সমান জরুরি — প্রশ্নটি একটি partition-এ উত্তর পায় কি না, আর কোনো partition সময়ের সাথে সীমাহীন বড় বা অতিরিক্ত ব্যস্ত হয়ে উঠবে কি না।
প্র ০৫Facebook-এর সামাজিক গ্রাফ — সম্ভবত পৃথিবীর অন্যতম বড় graph — সংরক্ষণ করা হয় MySQL-এর ওপর গড়া TAO নামের ব্যবস্থায়। তাহলে আলাদা graph ডেটাবেস কখন সত্যিই দরকার?
২০১৩ সালের USENIX ATC-তে Facebook-এর প্রকৌশলীরা TAO প্রবন্ধে বর্ণনা করেন, তাদের সামাজিক গ্রাফের object (ব্যবহারকারী, পোস্ট, ছবি) আর association (বন্ধু, like, comment) শেষ পর্যন্ত থাকে বহু ভাগে ভাগ করা MySQL ডেটাবেসে, আর তার ওপর একটি বিশাল, ভৌগোলিকভাবে ছড়ানো cache স্তর পড়ার চাপ সামলায়। কারণ তাদের প্রধান প্রশ্নগুলো অগভীর: “এই পোস্টে কারা like দিয়েছে”, “এই ব্যবহারকারীর শেষ ৫০টি বন্ধু” — এক ধাপ, বিপুল পরিমাণে, খুব দ্রুত। এর জন্য একটি সরল association তালিকা, ভালো ভাগ আর ভালো cache যথেষ্ট।
আলাদা graph ডেটাবেস ঝলমল করে অন্য ধরনের প্রশ্নে: গভীর আর পরিবর্তনশীল দৈর্ঘ্যের পথ। প্রতারণা শনাক্ত — একই ফোন নম্বর, ডিভাইস আর ঠিকানা ঘুরে কয়েক ধাপে যুক্ত অ্যাকাউন্টের চক্র খোঁজা; সরবরাহ-শৃঙ্খলে কোন যন্ত্রাংশের সমস্যা কোন পণ্যে পৌঁছায়; knowledge graph-এ সম্পর্কের পথ ধরে যুক্তি। এসব প্রশ্নে relational-এ প্রতিটি ধাপ একটি JOIN আর মধ্যবর্তী ফল বহুগুণ বাড়ে; graph ইঞ্জিন সরাসরি সম্পর্কের ঠিকানা ধরে হাঁটে আর Cypher-এ প্রশ্নটি ছবির মতো লেখা যায়।
দুর্বল দিকও আছে: গ্রাফ বহু সার্ভারে ভাগ করা কঠিন, কারণ যেকোনো ভাগে কিছু সম্পর্ক সার্ভার পেরিয়ে যায়; আর “মোট বিক্রি মাসভিত্তিক” ধাঁচের বড় aggregate-এ graph ইঞ্জিন সাধারণত relational বা column store-এর চেয়ে দুর্বল। নতুন মধ্যপথও এসেছে — SQL:2023-এর SQL/PGQ relational টেবিলের ওপরেই graph pattern কোয়েরি দেয়, আর ২০২৪ সালে ISO GQL ভাষা প্রকাশ করেছে।
মূল উপলব্ধি: ডেটা “graph-এর মতো দেখতে” হওয়া যথেষ্ট কারণ নয় — প্রায় সব ডেটাই তাই। প্রশ্নগুলো বহু ধাপের আর অনির্দিষ্ট দৈর্ঘ্যের হলে graph ডেটাবেস; এক-দুই ধাপের বিপুল পড়া হলে ভালো index, ভাগ আর cache-ওয়ালা relational প্রায়ই জেতে।
অনুশীলন
একটি কোয়েরি লেখা, একটি হাতে trace, একটি ডিজাইন, একটি ফল অনুমান।
-
কোয়েরি লিখুন: অংশ ৫-এর
customers_docটেবিল (প্রতিটি গ্রাহকের JSON document, ভেতরেordersarray, প্রতিটি অর্ডারে order_id, date, status) থেকে প্রতিটিpendingঅর্ডারের গ্রাহকের নাম, order_id আর তারিখ বের করুন। (অংশ ৫-এর দ্বিতীয় এডিটরে লিখে চালাতে পারেন।)SELECT json_extract(d.doc, '$.name') AS name, json_extract(o.value, '$.order_id') AS order_id, json_extract(o.value, '$.date') AS date FROM customers_doc d, json_each(d.doc, '$.orders') o WHERE json_extract(o.value, '$.status') = 'pending' ORDER BY d.id;ফল দুই সারি:
Rahim Uddin | 1009 | 2025-10-08আরFarhana Akter | 1006 | 2025-09-20. লাইনের ভেতরে যাওয়ার দরকার নেই, তাই একটিই json_each. MongoDB-তে একই প্রশ্নdb.customers.find({"orders.status": "pending"})— তবে এটি পুরো গ্রাহক document ফেরত দেয়; শুধু মেলা অর্ডারটি চাইলে aggregation-এ$unwindআর$matchলাগে। লক্ষ করুন, গুদামের এই প্রশ্নটি embedding-এ ভালো মেলে না — প্রতিটি গ্রাহক খুলতে হয়; referencing-এরorderscollection-এstatus-এর index অনেক স্বাভাবিক। -
হাতে trace করুন: অংশ ৭-এর বন্ধুত্বের গ্রাফে (1–2, 1–3, 2–4, 3–4, 3–5, 4–6, 5–7, 6–8) recursive CTE-টি Mitu (8) থেকে শুরু করুন আর শর্ত করুন
hops < 3. প্রতিটি পাকে কোন সারি তৈরি হবে, আর শেষ ফল কী?anchor: (8, 0,
,8,)। পাক ১: 8-এর বন্ধু শুধু 6 → (6, 1,,8,6,)। পাক ২: 6-এর বন্ধু 4 আর 8; 8 path-এ আছে → শুধু (4, 2,,8,6,4,)। পাক ৩: 4-এর বন্ধু 2, 3, 6; 6 বাদ → (2, 3,,8,6,4,2,), (3, 3,,8,6,4,3,)। পাক ৪: hops = ৩, শর্ত মিথ্যা → থামে। ফল:Farhana Akter | 1 | 1,Nusrat Jahan | 2 | 1,Karima Begum | 3 | 1,Sabbir Hasan | 3 | 1. Rahim (1) ৪ ধাপ দূরে, তাই আসেননি। প্রান্তের মানুষের কাছে পৌঁছাতে অনেক ধাপ লাগে, কিন্তু প্রতি ধাপে অল্প সারি — একটি “সরু” গ্রাফ। -
ডিজাইন করুন: পণ্যের পাতায় দেখাতে হবে “এই পণ্যের সাম্প্রতিক রিভিউ, নতুন আগে, প্রতি পাতায় ২০টি”। রিভিউতে আছে review_id, product_id, customer_id, rating, text আর সময়। Cassandra টেবিলটি CQL-এ লিখুন। কোনো পণ্যে লাখো রিভিউ জমতে পারলে কী বদলাবেন?
CREATE TABLE shop.reviews_by_product ( product_id int, reviewed_at timestamp, review_id uuid, customer_id int, rating int, text text, PRIMARY KEY ((product_id), reviewed_at, review_id) ) WITH CLUSTERING ORDER BY (reviewed_at DESC, review_id ASC); SELECT * FROM shop.reviews_by_product WHERE product_id = 101 LIMIT 20;প্রশ্নের WHERE থেকে partition key (product_id), ORDER BY থেকে clustering key (reviewed_at DESC); একই মুহূর্তের দুটি রিভিউ যেন একে অপরের ওপর না লেখে, তাই review_id. পরের পাতার জন্য Cassandra-র paging state বা
reviewed_at < শেষ দেখা সময়শর্ত। লাখো রিভিউয়ের জনপ্রিয় পণ্যে partition সীমাহীন বাড়ে — partition key করুন(product_id, month): সাম্প্রতিক পাতার জন্য চলতি মাস, দরকারে আগের মাস। “গ্রাহকের নিজের সব রিভিউ” চাইলে আলাদা টেবিলreviews_by_customer. -
ফল অনুমান করুন: একটি Cassandra cluster-এ প্রতিটি সারির N = ৫টি কপি। নিচের কোন (W, R) জোড়ায় পড়া নিশ্চিতভাবে সর্বশেষ সফল লেখাটি দেখবে? আর প্রতিটির দাম কী? (ক) W = ৩, R = ৩ (খ) W = ২, R = ৩ (গ) W = ৫, R = ১ (ঘ) W = ১, R = ৪
নিয়ম R + W > N, অর্থাৎ যোগফল ৬ বা বেশি। (ক) ৩ + ৩ = ৬ — নিশ্চিত; দুটি কপি বন্ধ থাকলেও লেখা আর পড়া দুটোই চলে (QUORUM/QUORUM). (খ) ২ + ৩ = ৫ — নিশ্চিত নয়; লেখা দুটিতে আর পড়া অন্য তিনটিতে যেতে পারে। (গ) ৫ + ১ = ৬ — নিশ্চিত, পড়া খুব দ্রুত; কিন্তু একটি কপি বন্ধ হলেই সব লেখা ব্যর্থ — availability খুব কম। (ঘ) ১ + ৪ = ৫ — নিশ্চিত নয়। সাধারণ পছন্দ (ক), কারণ এটি নিশ্চয়তা আর ব্যর্থতা-সহনশীলতা দুটোর ভারসাম্য রাখে। তবু মনে রাখুন: একসাথে আসা দুটি লেখার মধ্যে কোনটি টিকবে, তা ঠিক করে timestamp — quorum শুধু “সর্বশেষটি দেখা” নিশ্চিত করে, দ্বন্দ্ব ঠেকায় না।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমCassandra-র মতো wide-column ডেটাবেসে query-first টেবিল ডিজাইনের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- অ্যাপের প্রতিটি পাতা কোন প্রশ্ন করে আর কত ঘনঘন — তালিকা করা
- প্রতিটি প্রশ্নের WHERE থেকে partition key বাছা
- প্রশ্নের ক্রম (ORDER BY) আর অনন্যতা থেকে clustering key বাছা
- partition-এর সম্ভাব্য আকার আর hot partition যাচাই, দরকারে bucket যোগ
- প্রতিটি পরিবর্তনে কোন কোন টেবিলের কপি লিখতে হবে তা ঠিক করা
একটি ওয়েবসাইটে লগইনের session রাখতে হবে: প্রতিটি session একটি এলোমেলো id দিয়ে পড়া-লেখা হয়, ৩০ মিনিট পর নিজে মুছে যাওয়া উচিত, আর অন্য কোনো প্রশ্ন করা হয় না। কোন ধরনের ডেটাবেস সবচেয়ে স্বাভাবিক?
- Graph ডেটাবেস
- Key-value store, TTL-সহ
- Wide-column store
- Data warehouse
একটি Cassandra টেবিলের primary key: PRIMARY KEY ((customer_id), order_date, order_id). কোন কোয়েরিটি ALLOW FILTERING ছাড়াই দক্ষভাবে চলবে?
WHERE status = 'pending'WHERE order_date = '2025-10-08'WHERE customer_id = 1 AND order_date >= '2025-10-01'WHERE order_id = 1009
একটি জনপ্রিয় ব্লগ পোস্টে লাখো comment আসতে পারে। MongoDB-তে comment রাখার সবচেয়ে উপযুক্ত নকশা কোনটি?
- আলাদা comments collection, প্রতিটিতে post_id, আর (post_id, সময়)-এ index
- পোস্টের document-এর ভেতরে সব comment-এর একটি array
- প্রতিটি comment document-এ পুরো পোস্টের একটি কপি
- comment-এর জন্য graph ডেটাবেস বাধ্যতামূলক
একটি Dynamo-ধাঁচের ডেটাবেসে প্রতিটি key-র ৩টি কপি (N = 3). লেখায় W আর পড়ায় R-টি কপির উত্তর লাগে। কোন জোড়ায় পড়া নিশ্চিতভাবে সর্বশেষ সফল লেখাটি দেখবে?
- W = 1, R = 1
- W = 2, R = 1
- W = 1, R = 2
- W = 2, R = 2
“Schema-on-read” কথাটির সবচেয়ে সঠিক অর্থ কোনটি?
- ডেটার কোনো গঠনই থাকে না, তাই কোনো নিয়ম লাগে না
- লেখার সময় যাচাই হয় না; পড়ার সময় প্রোগ্রাম ডেটার আকার ব্যাখ্যা করে
- লেখার আগে প্রতিটি রেকর্ড টেবিলের কলাম আর টাইপের সাথে মেলানো হয়
- পড়া সবসময় লেখার চেয়ে দ্রুত হয়
একটি অনলাইন দোকান অর্ডার রাখে PostgreSQL-এ, cart রাখে Redis-এ, পণ্য খোঁজে Elasticsearch দিয়ে। এই polyglot persistence-এর সবচেয়ে বড় বাস্তব দাম কোনটি?
- প্রতিটি কাজ ধীর হয়ে যায়
- অর্ডারের transaction আর ACID থাকে না
- কপিগুলো মিলিয়ে রাখা আর কয়েকটি আলাদা ব্যবস্থা চালানোর পরিচালনা-বোঝা
- SQL আর ব্যবহার করা যায় না
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- NoSQL = Not only SQL, একটি পরিবার: key-value (Redis), document (MongoDB), wide-column (Cassandra), graph (Neo4j) — প্রত্যেকে আলাদা প্রশ্নের জন্য। উৎস: Bigtable (২০০৬) আর Dynamo (২০০৭)-এর ধারণা, ২০০৯-এ নামটির প্রসার।
- প্রেরণা আর দাম: scale-out, নমনীয় গঠন, নির্দিষ্ট access pattern, ডেভেলপারের সুবিধা; বিনিময়ে দুর্বল transaction (অন্তত শুরুতে), কপি আর কোয়েরির সীমা। schema-on-read মানে schema কোডে চলে যাওয়া।
- Document-এ: অল্প, সীমিত আর একসাথে পড়া — embed; সীমাহীন, আলাদা পড়া বা many-to-many — reference; ইতিহাস (কেনার দিনের দাম) — snapshot কপি।
- Wide-column-এ: আগে প্রশ্ন, পরে টেবিল। WHERE থেকে partition key, ORDER BY থেকে clustering key; নতুন প্রশ্ন = নতুন টেবিল; partition-এর আকার সীমিত রাখুন। Graph-এ বহু ধাপের traversal সস্তা।
- নিশ্চয়তা আর বাছাই: BASE বনাম ACID এখন বর্ণালি — R + W > N, MongoDB ৪.০-এর transaction. বাস্তবে polyglot persistence, কিন্তু নিয়ম: স্পষ্ট প্রয়োজন না থাকলে relational দিয়ে শুরু।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ৫৭ · MongoDB — document ডেটাবেস হাতে-কলমেপরবর্তী পর্বdocument মডেলের CRUD আর aggregation.
- পর্ব ৫৪ · Distributed database — partitioning, sharding আর CAPফিরে দেখুনভাগ, কপি আর নেটওয়ার্ক ভাগের তত্ত্ব।
- পর্ব ০৪ · ডেটা মডেল — hierarchical থেকে relational আর NoSQLফিরে দেখুনডেটা মডেলের ইতিহাস আর বড় ছবি।
- পর্ব ৫৮ · Redis — in-memory key-value আর cacheসামনেkey-value store-এর গভীরে।
- ডেটাবেস (DBMS) ও SQL · কোর্স হোম৬৭টি পর্বপুরো কোর্সের রোডম্যাপ — ১০টি মডিউল।
- DSA · Graphসম্পর্কিত কোর্সBFS, DFS আর adjacency list — graph ডেটাবেসের ভিত্তি।
- DSA · Hash tableসম্পর্কিত কোর্সkey-value store-এর ভেতরের তথ্য-কাঠামো।