এই পর্বে যা শিখবেন
- Relational database কী নিশ্চয়তা দেয়, আর তার scaling-এর পথ — live SQL-এ join আর transaction চালিয়ে
- NoSQL কেন এল, আর key-value, document, wide-column ও graph-এর data model, শক্তি আর দুর্বলতা
- Embed বনাম reference আর partition key-clustering key — দুটি seeded simulation-এর সংখ্যায়
- Query ধরে data modelling, denormalisation-এর দাম, distributed SQL আর tunable consistency
- বাছাইয়ের চেকলিস্ট, polyglot persistence-এর খরচ, আর একটি অনলাইন বাজারের ডেটা কোথায় যাবে
১ · Relational database যা দেয় — schema, join, transaction আর SQL
পাড়ার মুদি দোকানের খাতার কথা ভাবুন। ভালো দোকানির খাতায় আলাদা পাতা: এক পাতায় ক্রেতার তালিকা (ক্রমিক নম্বর, নাম, ফোন), এক পাতায় দাম, আর বাকির পাতায় শুধু “ক্রেতা ৭, চাল ২ বস্তা, ১২ তারিখ”. নাম-ফোন বারবার লেখা হয় না, তাই ফোন বদলালে এক জায়গায় কাটলেই চলে। “রহিম সাহেবের মোট বাকি?” জানতে দোকানি দুই পাতা মিলিয়ে দেখেন।
এটিই relational database-এর মূল চিন্তা। ডেটা থাকে টেবিলের সারি আর কলামে; প্রতিটি তথ্য এক জায়গায় (normalization), সম্পর্ক বোঝানো হয় id দিয়ে। প্রতিটি সারিকে আলাদা করে চেনায় primary key; অন্য টেবিলের সারির দিকে নির্দেশ করে foreign key. প্রশ্ন করার সময় পাতাগুলো জোড়া দেওয়ার নাম join. সিস্টেম ডিজাইনে relational database বলতে সাধারণত PostgreSQL, MySQL বা ছোট কাজে SQLite-এর কথা ভাবা হয়।
১৯৭০ সালে IBM-এর Edgar F. Codd “A Relational Model of Data for Large Shared Data Banks” প্রবন্ধে প্রস্তাব করেন: ডেটা disk-এ কীভাবে সাজানো, প্রোগ্রাম তা জানবে না; শুধু বলবে কী চাই। IBM-এর System R-এ এর ভাষা তৈরি হয় (প্রথমে SEQUEL, পরে SQL), ১৯৭৯ সালে আসে বাণিজ্যিকভাবে বিক্রি হওয়া প্রথম দিকের SQL database Oracle, ১৯৮৬ সালে SQL-এর প্রথম ANSI মানদণ্ড। পরের প্রায় তিন দশক “database” মানেই relational.
পাঁচটি জিনিস, যা relational database বিনা পরিশ্রমে দেয়
১ · Schema আর constraint
গঠন আগেই ঘোষিত (schema), লেখার মুহূর্তে যাচাই — schema-on-write. NOT NULL, CHECK (stock >= 0), foreign key ভুল ডেটা ঢোকার আগেই আটকায়।
২ · Join
তথ্য একবার রেখে যেকোনো সংমিশ্রণে জোড়া দেওয়া যায়। কপি নেই বলে কপি মেলানোর ঝামেলাও নেই।
৩ · ACID transaction
কয়েকটি লেখা একসাথে — অর্ডার লেখা আর stock কমানো — হয় দুটোই, নয় কোনোটিই না (transaction, ACID). টাকা, আসন আর মজুদের হিসাবে এটি প্রায় অপরিহার্য।
৪ · SQL আর query optimizer
আপনি বলেন কী চাই; কোন index, কোন join-এর ক্রম — ঠিক করে query optimizer. তাই নতুন প্রশ্নও (“কোন শহরে বিক্রি বেশি?”) গঠন না বদলেই চলে।
৫ · পরিণত টুল আর মানুষ: backup, replication, monitoring, migration টুল, ORM আর অভিজ্ঞ প্রকৌশলী — কয়েক দশকের জমা অভিজ্ঞতা। রাত ৩টার সমস্যায় পড়া মানুষ আগেও ছিলেন; এই “বিরক্তিকর” সুবিধার মূল্য প্রায়ই সবচেয়ে বেশি।
নিচের এডিটরে একটি কাল্পনিক অনলাইন দোকানের চারটি টেবিল। “চালান” চাপুন: একটি অর্ডারের পাতা join দিয়ে, একটি transaction, আর টেবিল বানানোর সময় কেউ ভাবেনি এমন একটি প্রশ্ন:
-- একটি কাল্পনিক অনলাইন দোকান: প্রতিটি তথ্য এক জায়গায়, সম্পর্ক id দিয়ে
CREATE TABLE customers (id INTEGER PRIMARY KEY, name TEXT NOT NULL, city TEXT NOT NULL, phone TEXT NOT NULL);
CREATE TABLE products (id INTEGER PRIMARY KEY, name TEXT NOT NULL, price INTEGER NOT NULL,
stock INTEGER NOT NULL CHECK (stock >= 0));
CREATE TABLE orders (id INTEGER PRIMARY KEY, customer_id INTEGER NOT NULL REFERENCES customers(id),
placed_at TEXT NOT NULL);
CREATE TABLE order_items (order_id INTEGER NOT NULL REFERENCES orders(id),
product_id INTEGER NOT NULL REFERENCES products(id),
qty INTEGER NOT NULL, unit_price INTEGER NOT NULL,
PRIMARY KEY (order_id, product_id));
INSERT INTO customers VALUES (1, 'Rina', 'Dhaka', '01700-000001'), (2, 'Arif', 'Rajshahi', '01800-000002'),
(3, 'Mitu', 'Khulna', '01900-000003');
INSERT INTO products VALUES (10, 'Rice 5kg', 450, 40), (11, 'Lentils 1kg', 140, 25), (12, 'Tea 400g', 260, 15);
INSERT INTO orders VALUES (1001, 1, '2026-10-01'), (1002, 2, '2026-10-02'), (1003, 1, '2026-10-05');
INSERT INTO order_items VALUES (1001, 10, 1, 450), (1001, 11, 2, 140), (1002, 12, 1, 260),
(1003, 10, 2, 450), (1003, 12, 1, 260);
-- 1) অর্ডার 1003-এর পাতা: চারটি টেবিল join করে
SELECT o.id, c.name, c.phone, p.name AS product, i.qty, i.qty * i.unit_price AS line_total
FROM orders o JOIN customers c ON c.id = o.customer_id
JOIN order_items i ON i.order_id = o.id JOIN products p ON p.id = i.product_id
WHERE o.id = 1003;
-- 2) নতুন অর্ডার আর stock কমানো একটি transaction-এ: হয় সব, নয় কিছুই না
BEGIN;
INSERT INTO orders VALUES (1004, 3, '2026-10-06');
INSERT INTO order_items VALUES (1004, 12, 2, 260);
UPDATE products SET stock = stock - 2 WHERE id = 12;
COMMIT;
-- 3) আগে না ভাবা একটি প্রশ্ন: শহরভিত্তিক বিক্রি, schema না বদলেই
SELECT c.city, COUNT(DISTINCT o.id) AS orders, SUM(i.qty * i.unit_price) AS revenue
FROM customers c JOIN orders o ON o.customer_id = c.id JOIN order_items i ON i.order_id = o.id
GROUP BY c.city ORDER BY revenue DESC;
SELECT id, name, stock FROM products;টেবিল আর ডেটা বানানোর পর প্রথম ফল: 1003 | Rina | 01700-000001 | Rice 5kg | 2 | 900 আর 1003 | Rina | 01700-000001 | Tea 400g | 1 | 260 — চার টেবিলের তথ্য এক পাতায়, অথচ Rina-র নাম-ফোন লেখা মাত্র একবার। “✓ BEGIN” থেকে “✓ COMMIT”: অর্ডার ১০০৪ আর stock কমানো একসাথে স্থায়ী হলো; মাঝপথে ব্যর্থ হলে ROLLBACK-এ দুটোই বাতিল হতো। শহরভিত্তিক ফল Dhaka | 2 | 1890, Khulna | 1 | 520, Rajshahi | 1 | 260 — নতুন অর্ডারটিও গোনা, আর এই প্রশ্নের জন্য কোনো টেবিল বদলাতে হয়নি। শেষে Tea-র stock 13 (১৫ − ২)।
Relational মডেলের আসল শক্তি অজানা প্রশ্নের প্রতি নমনীয়তা: ডেটা থাকে তার নিজের গঠনে, কোন পাতা কীভাবে পড়া হবে তা না জেনেই; পরে যে প্রশ্নই আসুক, join আর index দিয়ে উত্তর মেলে। NoSQL-এর বেশিরভাগ নকশা উল্টো পথে হাঁটে: আগে প্রশ্ন, তারপর গঠন (অংশ ৬)।
২ · Relational-এর scaling — বড় যন্ত্র, read replica, তারপর হাতে-করা sharding
রেস্তোরাঁর রান্নাঘর ভাবুন: ভিড় বাড়লে আগে বড় চুলা আর বেশি বাবুর্চি; তারপর বাড়তি ওয়েটার, রান্না তবু এক রান্নাঘরে; শেষে দ্বিতীয় শাখা — আর তখনই ঝামেলা, অর্ডারের অর্ধেক যদি অন্য শাখার ভাঁড়ারে থাকে? Relational database-এর বেড়ে ওঠাও এমন, সাধারণত এই ক্রমে:
| ধাপ | কী করেন | কী পান | দাম |
|---|---|---|---|
| ১ · Query আর index ঠিক করা | ধীর query-র EXPLAIN, ঠিক index (পর্ব ২১) | প্রায়ই সবচেয়ে বড় লাভ | প্রকৌশলীর সময় |
| ২ · Vertical scaling | বেশি CPU, RAM, দ্রুত SSD (পর্ব ১৩) | কোড বদলাতে হয় না | সীমা আছে, দাম লাফিয়ে বাড়ে, যন্ত্র একটিই |
| ৩ · Connection pool আর cache | বারবার পড়া ডেটা memory-তে (পর্ব ২৮, ৩০) | পড়ার চাপ কমে | পুরোনো মান, invalidation |
| ৪ · Read replica | primary-র কপি থেকে পড়া (পর্ব ২২) | পড়া অনেক যন্ত্রে ছড়ায় | Replication lag; সব লেখা তবু এক primary-তে |
| ৫ · আলাদা ডেটা আলাদা database-এ | অর্ডার, ব্যবহারকারী, লগ — ভিন্ন database | প্রতিটির চাপ ছোট | database পেরিয়ে join নেই |
| ৬ · Sharding | একই টেবিলের সারি key ধরে কয়েকটি database-এ (পর্ব ২৩) | লেখাও ছড়ায় | নিচের পাঁচটি সমস্যা |
সাধারণ PostgreSQL বা MySQL নিজে থেকে sharding করে না; অ্যাপকে, অথবা মাঝের একটি স্তরকে, কাজগুলো হাতে করতে হয়। তখন পাঁচটি সমস্যা প্রায় নিশ্চিতভাবে আসে:
- Routing: প্রতিটি query কোন shard-এ যাবে — সাধারণত customer_id-এর মতো key-এর hash থেকে।
- Shard পেরিয়ে join: অ্যাপের কোডে — সব shard-এ প্রশ্ন পাঠিয়ে ফল মেলানো।
- Shard পেরিয়ে transaction: এক shard-এর টাকা কমিয়ে অন্য shard-এ বাড়ানো আর একটি সাধারণ
BEGIN … COMMITনয় — লাগে distributed transaction বা saga (পর্ব ২৫)। - Resharding: shard ভরে গেলে চালু অবস্থায় ডেটা সরানো (পর্ব ১৬ আর ২৩)।
- বাকি সব N বার: schema migration, backup, monitoring, unique ID — প্রতিটি shard-এ।
এই কাজ সহজ করতে MySQL-এর জন্য Vitess বা PostgreSQL-এর জন্য Citus-এর মতো open-source স্তর আছে (Vitess-এর শুরু YouTube-এ, প্রকল্পের প্রকাশিত ইতিহাস অনুযায়ী)। তবু shard key-এর সিদ্ধান্ত আর shard পেরোনো প্রশ্নের খরচ আপনার নকশারই অংশ থাকে।
বেশিরভাগ সিস্টেম ধাপ ৬-এ পৌঁছায় না। পর্ব ৩-এর ধাঁচে — ধরা যাক দিনে ১০ লাখ অর্ডার, প্রতিটি ২ KB. গড়ে সেকেন্ডে ১০,০০,০০০ ÷ ৮৬,৪০০ ≈ ১১.৬টি লেখা, পিকে দশ গুণ ধরলে প্রায় ১১৬টি; বছরে ডেটা ১০ লাখ × ২ KB × ৩৬৫ ≈ ৭৩০ GB. ঠিক index-সহ একটি ভালো primary আর কয়েকটি replica সাধারণত এটুকু সামলায় — তবে নিজের hardware আর query-তে load test করে মাপুন। “ভবিষ্যতে লাগবে” ভেবে আগেভাগে sharding নেওয়া প্রায়ই trade-off-এর ভুল দিক।
৩ · NoSQL কেন এল — web scale, নমনীয় schema আর ভাগ করা যায় এমন data model
২০০০-এর দশকে সার্চ, বিজ্ঞাপন আর অনলাইন কেনাকাটার কয়েকটি সেবার কাছে একটি যন্ত্র যথেষ্ট ছিল না, আর হাতে-করা sharding ছিল যন্ত্রণার। তারা প্রশ্নটি উল্টে দেয়: join আর বহু-সারির transaction ছেড়ে দিলে কি ডেটা সহজে হাজারো যন্ত্রে ছড়ানো যায়?
দুটি প্রকাশিত প্রবন্ধ এই আন্দোলনের ভিত্তি। Google-এর Bigtable (OSDI, ২০০৬) — একটি বিশাল, সাজানো map: (row key, column, timestamp) → মান, row key-এর ক্রমে টুকরো হয়ে অনেক সার্ভারে। Amazon-এর Dynamo (SOSP, ২০০৭) — শপিং কার্টের key-value store, যা consistent hashing (পর্ব ১৬) দিয়ে key ছড়ায়, যন্ত্র বিকল হলেও লেখা নেয়, পরে কপিগুলো মেলায়। এদের ধারণা থেকে open-source জগতে আসে HBase (Bigtable-ধাঁচ), Cassandra (Facebook-এ শুরু, ২০০৮ সালে open source; Bigtable-এর data model আর Dynamo-র বিতরণ-কৌশলের মিশ্রণ), আর ২০০৯ সালের দিকে MongoDB ও Redis. “NoSQL” নামটি ছড়ায় ২০০৯ সালে San Francisco-র একটি meetup-এর hashtag থেকে; পরে অনেকে একে পড়েন “Not only SQL”.
চারটি প্রেরণা
- ভাগ করা সহজ এমন data model: যে ডেটা সবসময় একটি key দিয়ে একসাথে পড়া হয় — একটি কার্ট, একজন ব্যবহারকারীর timeline — সেটি পুরোটা এক partition-এ রাখলে কোনো প্রশ্নেই যন্ত্র পেরোতে হয় না। তখন যন্ত্র যোগ করলে ক্ষমতা প্রায় সমানুপাতে বাড়ে (horizontal scaling). এই একসাথে-থাকা এককের নাম “aggregate” (শব্দটি Domain-Driven Design থেকে নেওয়া; NoSQL-এর আলোচনায় জনপ্রিয় করেন Sadalage ও Fowler, ২০১২)।
- বিকল হলেও চালু থাকা: কয়েকটি data center-এ কপি, কোনো কপি না পৌঁছালেও লেখা গ্রহণ — বিনিময়ে কিছুক্ষণের অমিল (eventual consistency).
- নমনীয় schema: বিচিত্র গঠনের ডেটায় (মোবাইল ফোন আর শাড়ির spec আলাদা) প্রতিটি বদলে
ALTER TABLEছাড়াই নতুন field. - কোডের সাথে মিল: নেস্টেড object আর সমতল টেবিলের অমিল — impedance mismatch — document-এ প্রায় নেই।
বিনিময়ে যা ছাড়তে হয়েছিল: join, কয়েকটি সারি বা document জুড়ে transaction (অন্তত শুরুর দিকে), যেকোনো নতুন প্রশ্ন করার স্বাধীনতা আর ডেটাবেসের নিজের constraint-যাচাই। এগুলো উধাও হয়নি — কাজগুলো সরে গেছে অ্যাপের কোডে আর নকশাকারীর মাথায়। NoSQL কোনো একটি প্রযুক্তি নয়; ভিন্ন ভিন্ন trade-off-এর একটি পরিবার।
NoSQL-এর চার পরিবার — ডেটার চার আকার
Key-value · Document · Wide-column (partition + clustering key) · Graph (node + edge)
৪ · Key-value আর document — key দিয়ে আনা, document-এ গুছিয়ে রাখা
Key-value store
রেলস্টেশনের মালপত্রের লকারের মতো: টোকেন দিলে বাক্স ফেরত, কিন্তু “লাল ব্যাগ কোন বাক্সে?” — উত্তর কর্মচারীর কাছে নেই। Key-value store-এ প্রতিটি মান একটি key-এর নিচে; কাজ মূলত তিনটি — GET, SET, DEL, সাথে প্রায়ই মেয়াদ (TTL).
- শক্তি: দ্রুত আর অনুমেয় latency (memory-তে এক মিলিসেকেন্ডেরও কম); key-এর hash দিয়ে সহজে ভাগ।
- দুর্বলতা: মান দিয়ে খোঁজা, সম্পর্ক বা (key সাজানো না থাকলে) পরিসর ধরে প্রশ্ন — কোনোটিই নেই।
- উদাহরণ: Redis আর Memcached (cache, session, counter), etcd (ছোট config), RocksDB (অ্যাপের ভেতরে বসানো engine), আর DynamoDB-ধাঁচের store — partition key-এর সাথে sort key, তাই partition-এর ভেতরে সাজানো প্রশ্নও চলে।
- সিস্টেম ডিজাইনে কোথায়: session (পর্ব ১৫), cache (পর্ব ২৮ আর ৩০), rate limit-এর counter (পর্ব ৪৯), আসনের ১০ মিনিটের hold. Memory-ভিত্তিক store-কে টাকার হিসাবের একমাত্র ঠিকানা বানাবেন না (DBMS কোর্সের Redis পর্ব)।
Document database
Document database-এ প্রতিটি রেকর্ড একটি স্বয়ংসম্পূর্ণ JSON-ধাঁচের document (MongoDB-তে binary রূপ BSON), গঠন একেকটির একেক রকম হতে পারে। Key-value-এর সাথে বড় পার্থক্য: ডেটাবেস ভেতরটা দেখে — field ধরে secondary index, আর একটি document-এর ভেতরের বদল atomic. উদাহরণ: MongoDB, Couchbase, CouchDB; আর relational-এর ভেতরেই document রাখার উপায় PostgreSQL-এর JSONB.
নকশার সবচেয়ে বড় সিদ্ধান্ত: সম্পর্কিত ডেটা ভেতরে রাখবেন (embedding), নাকি আলাদা document-এ রেখে id দিয়ে নির্দেশ করবেন (referencing)?
| প্রশ্ন | Embed করুন, যদি… | Reference করুন, যদি… |
|---|---|---|
| একসাথে পড়া হয়? | প্রায় সবসময় (অর্ডার আর items) | আলাদা আলাদা |
| আকার | সীমিত (কয়েকটি item, একটি ঠিকানা) | সীমাহীন (মন্তব্য, লগ, অনুসারী) |
| মালিকানা | শুধু এই document-এর | অনেকের ভাগ করা (পণ্য, লেখক) |
| বদল | মূল document-এর সাথে | নিজে নিজে, ঘনঘন |
এবার একই অর্ডার দুই মডেলে, ধাপে ধাপে। খেয়াল করুন কোন প্রশ্ন কোথায় সস্তা, আর কোথায় দামি:
customers 1 │ Rina │ Dhaka │ 01700-000001
orders 1003 │ customer_id 1 │ 2026-10-05
order_items 1003 │ product 10 │ qty 2 │ 450
1003 │ product 12 │ qty 1 │ 260
products 10 │ Rice 5kg │ 450
12 │ Tea 400g │ 260
SEARCH o USING INTEGER PRIMARY KEY (rowid=?) → 1003 SEARCH c USING INTEGER PRIMARY KEY (rowid=?) → Rina SEARCH i USING INDEX sqlite_autoindex_order_items_1 (order_id=?) SEARCH p USING INTEGER PRIMARY KEY (rowid=?) → Rice, Tea
{ _id: 1003, placed_at: "2026-10-05",
customer: { id: 1, name: "Rina", phone: "01700-000001" },
items: [ { product_id: 10, name: "Rice 5kg", qty: 2, unit_price: 450 },
{ product_id: 12, name: "Tea 400g", qty: 1, unit_price: 260 } ],
total: 1160 }
db.orders.findOne({ _id: 1003 }) // shard key _id → এক node-এ পুরো অর্ডার
ক) পণ্য 10 মোট কতটি বিক্রি হলো? relational: SELECT SUM(qty) FROM order_items WHERE product_id = 10 document : সব অর্ডারের items খুলে aggregation, নয়তো "items.product_id"-এ নতুন index খ) Rina ফোন নম্বর বদলালেন relational: customers-এ একটি সারি document : তাঁর প্রতিটি অর্ডারে কপি — বদলাবেন, না ইতিহাস হিসেবে রাখবেন?
| অর্ডারের পাতা পড়তে | |
| কপি করা তথ্য | |
| আগে না ভাবা প্রশ্ন |
- Relational মডেল। চারটি টেবিল, প্রতিটি তথ্য একবার। Rina-র ফোন আছে শুধু
customers-এ; অর্ডার রাখে শুধুcustomer_id, আর item রাখে শুধুproduct_id. - পাতা পড়তে join. SQLite-এর plan: চারটি টেবিলে চারটি index-খোঁজ। এক যন্ত্রে খুব সস্তা; কিন্তু টেবিলগুলো আলাদা shard-এ থাকলে প্রতিটি খোঁজ একটি network hop — অংশ ২-এর সমস্যা।
- Document মডেল। পুরো অর্ডার একটি document, customer-এর দরকারি অংশ আর items ভেতরে embed করা। Item সীমিত, একসাথে পড়া হয়, অর্ডারেরই মালিকানা — embedding-এর আদর্শ ক্ষেত্র, অর্ডারের “aggregate”.
- পড়া একটি খোঁজে।
_idদিয়ে একটি document; shard key_idহলে পুরো অর্ডার এক node-এ — join নেই, যন্ত্র পেরোনো নেই। অর্ডার লেখাও একটি document-এ, তাই নিজে থেকেই atomic. - দাম আসে নতুন প্রশ্নে আর কপিতে। “পণ্য ১০ কতটি বিক্রি?” relational-এ একটি query; document-এ সব অর্ডার খুলে গোনা, নয়তো নতুন index. Rina-র ফোন বদলালে relational-এ একটি সারি, document-এ প্রতিটি অর্ডার — যদি না সেটি “ডেলিভারির সময়ের” ইতিহাস হিসেবেই রাখার কথা। কোনটি কপি আর কোনটি ইতিহাস, সেটিই নকশার প্রশ্ন।
Embed না reference — সংখ্যায় দেখুন
একটি কাল্পনিক বাস-যাত্রীদের ফোরাম: ২০০টি পোস্ট, ৫০ জন ব্যবহারকারী, মন্তব্য ভারী-লেজের বণ্টনে, পোস্ট ০ ভাইরাল — ৩,০০০টি মন্তব্য (সব ধরা যাক)। তিন মডেল: সব মন্তব্য পোস্টের ভেতরে (embed), আলাদা collection-এ (reference), আর পোস্টে শুধু সর্বশেষ ১০টি, পুরোটা আলাদা (subset). প্রশ্ন দুটি: Q1 পোস্ট ০ আর তার সর্বশেষ ১০টি মন্তব্য; Q2 u7-এর সব মন্তব্য (user field-এ index ধরে)। আকার = JSON লেখার দৈর্ঘ্য:
// Document store: embed a post's comments, reference them, or embed only the latest 10 (seeded)
function mulberry32(a) { return () => { a = (a + 0x6D2B79F5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; }
const rand = mulberry32(20), POSTS = 200, USERS = 50, W = "bus seat route trip fare late ac good bad".split(" ");
const words = n => Array.from({ length: n }, () => W[Math.floor(rand() * W.length)]).join(" ");
const size = doc => JSON.stringify(doc).length; // ASCII only, so characters = bytes
// ধরা যাক: comments per post are heavy-tailed (Pareto), and post 0 went viral with 3000
const posts = [], comments = [];
for (let p = 0; p < POSTS; p++) {
posts.push({ _id: p, title: words(6), author: "u" + (p % USERS), body: words(120) });
const k = p === 0 ? 3000 : Math.floor(6 / (1 - rand()) ** (1 / 1.6)) - 6;
for (let j = 0; j < k; j++)
comments.push({ _id: comments.length, post: p, user: "u" + Math.floor(rand() * USERS), t: j, text: words(5 + Math.floor(rand() * 20)) });
}
const byPost = p => comments.filter(c => c.post === p);
const strip = ({ post, ...c }) => c; // an embedded comment needs no post id
const embed = posts.map(p => ({ ...p, comments: byPost(p._id).map(strip) }));
const subset = posts.map(p => ({ ...p, latest: byPost(p._id).slice(-10).map(strip) }));
const sum = a => a.reduce((s, x) => s + x, 0), kb = b => (b / 1024).toFixed(1) + " KB";
const sizes = embed.map(size).sort((a, b) => b - a);
console.log("comments " + comments.length + " | embedded post sizes: viral " + kb(size(embed[0])) +
", next " + kb(sizes[1]) + ", median " + kb(sizes[POSTS / 2]));
// Q1: post 0 with its latest 10 comments. Q2: every comment written by user u7 (index on user)
const mine = comments.filter(c => c.user === "u7"), hit = [...new Set(mine.map(c => c.post))];
const rows = [
["embed", 1, 1, size(embed[0]), hit.length, sum(hit.map(p => size(embed[p]))), 1],
["reference", 2, 11, size(posts[0]) + sum(byPost(0).slice(-10).map(size)), mine.length, sum(mine.map(size)), 1],
["subset", 1, 1, size(subset[0]), mine.length, sum(mine.map(size)), 2]];
console.log("model Q1 queries docs bytes | Q2 docs bytes | writes per comment");
for (const [m, q, d1, b1, d2, b2, w] of rows)
console.log(m.padEnd(10) + String(q).padStart(11) + String(d1).padStart(6) + kb(b1).padStart(11) + " |" +
String(d2).padStart(8) + kb(b2).padStart(11) + " |" + String(w).padStart(8));
const avg = sum(comments.map(c => size(strip(c)))) / comments.length;
console.log("avg embedded comment " + avg.toFixed(0) + " B: a 16 MiB document is full after ~" + Math.floor(16 * 1024 * 1024 / avg) + " comments");comments 4523 | embedded post sizes: viral 323.5 KB, next 16.7 KB, median 1.0 KB
model Q1 queries docs bytes | Q2 docs bytes | writes per comment
embed 1 1 323.5 KB | 27 410.2 KB | 1
reference 2 11 1.8 KB | 102 11.9 KB | 1
subset 1 1 1.7 KB | 102 11.9 KB | 2
avg embedded comment 109 B: a 16 MiB document is full after ~154126 comments
ভারী লেজ স্পষ্ট: ভাইরাল পোস্টের document 323.5 KB, পরেরটি 16.7 KB, মাঝারি 1.0 KB. Q1-এ embed এক query-তে, কিন্তু ১০টি মন্তব্যের জন্য সার্ভারকে পুরো 323.5 KB পড়তে হয় (projection দিয়ে network-এ কম পাঠানো যায়, পড়া কমে না); reference-এ দুটি query, 1.8 KB; subset-এ একটি query, 1.7 KB. Q2-তে embed-এর দুর্বলতা: u7-এর মন্তব্য 27টি পোস্টে, প্রতিটি পুরো পোস্ট — 410.2 KB; বাকি দুটিতে 102টি ছোট document, 11.9 KB. Subset-এর দাম শেষ কলামে — প্রতি মন্তব্যে 2টি লেখা। আর গড় 109 B-এ MongoDB-র ১৬ MiB সীমা (লেখার সময়ে) প্রায় ১,৫৪,১২৬টি মন্তব্যে ভরবে — তার অনেক আগেই পড়া ধীর।
৫ · Wide-column আর graph — আগে প্রশ্ন, পরে টেবিল; আর যেখানে সম্পর্কই তথ্য
Wide-column store
ডাকঘরে চিঠি বাছাইয়ের কথা ভাবুন: আগে জেলা ধরে বস্তায় ভাগ, প্রতিটি বস্তা নির্দিষ্ট ট্রাকে; বস্তার ভেতরে চিঠি থানা আর বাড়ির নম্বরের ক্রমে, তাই একটি এলাকার চিঠি এক টানে ওঠে। কিন্তু “লাল খামের সব চিঠি” খুঁজতে সব ট্রাকের সব বস্তা খুলতে হবে।
Wide-column store-এ বস্তাটি partition, জেলাটি partition key — এর hash ঠিক করে সারি কোন node-এ যাবে (Cassandra-তে consistent hashing-এর ring, পর্ব ১৬); বস্তার ভেতরের ক্রম clustering key. লেখা সস্তা, কারণ ভেতরে সাধারণত LSM-tree: লেখা memory-তে জমে, পরে disk-এ একটানা (পর্ব ১৯)। উদাহরণ: Apache Cassandra, ScyllaDB, HBase, আর managed সেবা হিসেবে Google-এর Bigtable.
নামের ফাঁদ: “wide-column” আর “column store” এক নয়। Column store (পর্ব ১৯) ডেটা কলাম ধরে রাখে — বড় বিশ্লেষণের (OLAP) জন্য; wide-column store সারি রাখে partition ধরে, প্রতিটি সারিতে ভিন্ন ভিন্ন কলাম — ছোট, দ্রুত পড়া-লেখার (OLTP) জন্য।
নিয়মটি কঠোর: একটি প্রশ্ন যেন একটি partition-এ শেষ হয়. তাই নকশা শুরু হয় প্রশ্নের তালিকা থেকে, আর প্রতিটি বড় প্রশ্নের জন্য আলাদা টেবিল। একটি কাল্পনিক বাস-টিকিট সেবায় ধাপে ধাপে দেখুন:
Q1 একটি রুটে একটি দিনের trip, ছাড়ার সময়ের ক্রমে — খুব ঘনঘন Q2 একটি trip-এর সব আসনের অবস্থা — খুব ঘনঘন Q3 একজন যাত্রীর সাম্প্রতিক টিকিট, নতুনটি আগে — মাঝারি
CREATE TABLE trips_by_route_date ( route text, travel_date date, dep_time text, trip_id uuid, operator text, bus_type text, fare int, PRIMARY KEY ((route, travel_date), dep_time, trip_id) ); -- ( partition key ), clustering key
SELECT dep_time, operator, fare FROM trips_by_route_date WHERE route = 'DHK-RAJ' AND travel_date = '2026-12-10' AND dep_time >= '08:00' AND dep_time < '12:00';
CREATE TABLE seats_by_trip ( trip_id uuid, seat_no text, status text, held_until timestamp, PRIMARY KEY ((trip_id), seat_no) );
CREATE TABLE tickets_by_passenger ( passenger_id uuid, booked_at timestamp, ticket_id uuid, route text, travel_date date, dep_time text, seat_no text, -- কপি PRIMARY KEY ((passenger_id), booked_at, ticket_id) ) WITH CLUSTERING ORDER BY (booked_at DESC, ticket_id ASC); -- একটি বুকিং = দুই টেবিলে লেখা UPDATE seats_by_trip SET status = 'booked' WHERE trip_id = ? AND seat_no = 'B4' IF status = 'free'; INSERT INTO tickets_by_passenger (...) VALUES (...);
SELECT * FROM trips_by_route_date WHERE operator = 'OP-7'; -- Cassandra চালায় না: ALLOW FILTERING চায়, মানে সব node-এর সব partition পড়া CREATE TABLE trips_by_operator_date ( operator text, travel_date date, dep_time text, trip_id uuid, route text, PRIMARY KEY ((operator, travel_date), dep_time, trip_id) ); -- আর পুরোনো সব trip এখানে ভরাট (backfill)
| টেবিলের সংখ্যা | |
| একটি partition-এ সারি | |
| এক বুকিংয়ে লেখা | |
| নতুন trip যোগে লেখা |
- আগে প্রশ্নের তালিকা। Relational-এ আগে সত্তা, পরে প্রশ্ন; wide-column-এ উল্টো — প্রতিটি প্রশ্ন, তার ঘনত্ব আর latency-লক্ষ্য লিখুন (পর্ব ৬)। তিনটি প্রশ্ন, তাই অন্তত তিনটি টেবিল।
- Q1-এর টেবিল। Partition key দুই কলাম —
(route, travel_date). শুধুrouteনিলে partition-এ বছরের পর বছরের trip জমত, আর জনপ্রিয় রুট একটি node-কে গরম রাখত (hot key). তারিখ যোগে প্রতিটি partition এক দিনের ২০–৪০টি trip. - Clustering key-তে পরিসর। Partition-এর ভেতরে সারি
dep_time-এর ক্রমে সাজানো; একই সময়ে দুটি trip থাকলেtrip_idতাদের আলাদা করে। তাই “সকাল ৮টা থেকে ১২টা” মানে একটি partition-এর একটি টানা অংশ — নিচের simulation-এর query A. - Q2-এর জন্য আলাদা টেবিল। Partition key
trip_id, clustering keyseat_no: আসন-ম্যাপ এক পড়ায়। নতুন trip যোগ এখন দুই টেবিলে লেখা। - Q3 আর কপির দাম। যাত্রীর টিকিট-তালিকায় রুট, তারিখ আর সময় কপি করা — join নেই বলে; একটি বুকিং এখন দুই টেবিলে লেখা। একই আসন দুজনকে বিক্রি ঠেকাতে শর্তযুক্ত লেখা (
IF status = 'free') — Cassandra-তে Paxos-ভিত্তিক lightweight transaction, সাধারণ লেখার চেয়ে কয়েক গুণ ধীর। তাই অনেক নকশায় আসন আর টাকা relational-এই থাকে (পর্ব ৬০)। - নতুন প্রশ্ন মানে নতুন টেবিল। “OP-7-এর সব trip” কোনো partition key-তে নেই; Cassandra
ALLOW FILTERINGচায় — সব node-এর সব partition পড়া (simulation-এর query B). ঠিক পথ: নতুন টেবিল আর backfill; নতুন trip এখন তিন টেবিলে। Relational-এ লাগত শুধু একটি index.
এবার সংখ্যায়: আটটি রুট, ডিসেম্বরের ৩০ দিন, দিনে ২০–৪০টি trip (ধরা যাক), চারটি node; partition-এর node ঠিক হয় partition key-এর FNV-1a hash থেকে। প্রতিটি প্রশ্নে গোনা হলো ছোঁয়া partition আর node, পড়া সারি আর ফেরত সারি:
// Wide-column model: partition key (route, date), clustering key dep — which partitions does a query touch?
function mulberry32(a) { return () => { a = (a + 0x6D2B79F5) | 0;
let t = Math.imul(a ^ (a >>> 15), 1 | a); t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
return ((t ^ (t >>> 14)) >>> 0) / 4294967296; }; }
function fnv1a(s) { let h = 0x811c9dc5;
for (let i = 0; i < s.length; i++) { h ^= s.charCodeAt(i); h = Math.imul(h, 0x01000193) >>> 0; } return h; }
const rand = mulberry32(23), NODES = 4, pad = n => String(n).padStart(2, "0"), home = pk => fnv1a(pk) % NODES;
const routes = ["DHK-RAJ", "DHK-CTG", "DHK-SYL", "DHK-KHL", "DHK-BSL", "DHK-RNG", "CTG-SYL", "DHK-CXB"];
const nodes = Array.from({ length: NODES }, () => new Map()); // node -> (partition key -> sorted rows)
for (const r of routes) for (let d = 1; d <= 30; d++) { // ধরা যাক: 20-40 trips per route per day
const pk = r + "|2026-12-" + pad(d), rows = [];
for (let k = 20 + Math.floor(rand() * 21); k > 0; k--)
rows.push({ dep: pad(5 + Math.floor(rand() * 19)) + ":" + pad(5 * Math.floor(rand() * 12)), op: "OP-" + (1 + Math.floor(rand() * 12)) });
rows.sort((a, b) => a.dep < b.dep ? -1 : a.dep > b.dep ? 1 : 0); // clustering key keeps rows in order
nodes[home(pk)].set(pk, rows);
}
const total = nodes.reduce((s, m) => s + [...m.values()].reduce((t, r) => t + r.length, 0), 0), A = "DHK-RAJ|2026-12-10";
console.log("partitions per node: " + nodes.map(m => m.size).join(" / ") + " rows in total: " + total);
console.log("partition " + A + " lives on node " + home(A) + " with " + nodes[home(A)].get(A).length + " rows, sorted by dep");
function query(label, plan) {
const touched = new Set(), s = { parts: 0, read: 0, out: 0 };
plan((pk, n) => { touched.add(n); s.parts++; return nodes[n].get(pk) || []; }, s);
console.log(label.padEnd(38) + String(s.parts).padStart(6) + String(touched.size).padStart(7) + String(s.read).padStart(7) + String(s.out).padStart(6));
}
console.log("query".padEnd(38) + " parts nodes read out");
query("A: route+date, 08:00 <= dep < 12:00", (get, s) => { // full partition key known: one partition, a slice
const rows = get(A, home(A)); let i = 0;
while (i < rows.length && rows[i].dep < "08:00") i++; // a real store binary-searches the sorted rows
for (; i < rows.length && rows[i].dep < "12:00"; i++) { s.read++; s.out++; }
});
query("B: op = 'OP-7' (not in the key)", (get, s) => // ALLOW FILTERING: every partition on every node
nodes.forEach((m, n) => m.forEach((_, pk) => get(pk, n).forEach(r => { s.read++; if (r.op === "OP-7") s.out++; }))));
query("C: route only, IN (30 dates)", (get, s) => { // a partition is found only from its full key
for (let d = 1; d <= 30; d++) { const pk = "DHK-RAJ|2026-12-" + pad(d); get(pk, home(pk)).forEach(() => { s.read++; s.out++; }); }
});partitions per node: 60 / 62 / 64 / 54 rows in total: 7178
partition DHK-RAJ|2026-12-10 lives on node 2 with 34 rows, sorted by dep
query parts nodes read out
A: route+date, 08:00 <= dep < 12:00 1 1 8 8
B: op = 'OP-7' (not in the key) 240 4 7178 566
C: route only, IN (30 dates) 30 4 878 878
Hash ২৪০টি partition চার node-এ মোটামুটি সমান ছড়িয়েছে (60 / 62 / 64 / 54). Query A পুরো partition key জানে: 1টি partition, 1টি node, ৩৪টি সারির মধ্যে পড়া শুধু দরকারি 8টি। Query B-তে operator key-তে নেই: 240টি partition, 4টি node, 7178টি সারি পড়ে ফেরত 566টি — প্রায় ৯২% পড়া বৃথা, আর ডেটা দশ গুণ হলে খরচও দশ গুণ। Query C: partition key-এর অর্ধেক দিয়ে partition চেনা যায় না, তাই ৩০টি তারিখের তালিকা — 30টি partition, চারটি node-ই। সমাধান প্রশ্নপিছু টেবিল।
Graph database
কিছু প্রশ্নে সম্পর্কই মূল তথ্য: “বন্ধুর বন্ধুদের মধ্যে কে এই এলাকায়?”, “এই পাঁচটি অ্যাকাউন্ট কি একই ডিভাইসে জোড়া?” Graph database-এ ডেটা node (সত্তা) আর edge (সম্পর্ক), দুটোতেই property; প্রশ্ন মানে graph traversal — edge ধরে হাঁটা। Neo4j-ধাঁচের ডেটাবেসে প্রতিটি node নিজের edge-গুলোর সরাসরি ঠিকানা রাখে (index-free adjacency), তাই এক ধাপ হাঁটার খরচ নির্ভর করে প্রতিবেশীর সংখ্যায়, পুরো টেবিলের আকারে নয়। Relational-এ প্রতি ধাপে একটি join, আর ধাপ বাড়লে খরচ দ্রুত বাড়ে।
দুর্বলতা: graph ভাগ করা কঠিন — যন্ত্র পেরোনো প্রতিটি edge এক network hop; বিশাল সমষ্টিতেও সুবিধা নেই। তাই অনেক দল edge টেবিল relational-এই রাখে। উদাহরণ: Neo4j (Cypher), JanusGraph; গভীরে DBMS কোর্সের Graph, search আর time-series পর্ব।
আরও দুটি বিশেষ store — time-series আর search engine
- Time-series database: CPU-র মাপ, sensor-এর রিডিং — সময়-চিহ্নিত, প্রায় শুধু নতুন লেখা, প্রশ্ন সময়ের সীমা ধরে; compression, পুরোনো ডেটার গড় (downsampling) আর মেয়াদে মোছা। উদাহরণ: InfluxDB, TimescaleDB (PostgreSQL-এর extension), আর metric-এর জন্য Prometheus-এর নিজস্ব storage (পর্ব ৫০)।
- Search engine: শব্দ থেকে নথির তালিকা — search index — দিয়ে পূর্ণ লেখায় খোঁজা, প্রাসঙ্গিকতা আর বানান-ভুল সহ্য করা; Lucene-এর ওপর Elasticsearch, OpenSearch, Solr. সাধারণত মূল ডেটার দ্বিতীয় কপি, একটু দেরিতে হালনাগাদ। বিস্তারিত পর্ব ২৭-এ।
| পরিবার | Data model | ভালো প্রশ্ন | শক্তি | দুর্বলতা | উদাহরণ |
|---|---|---|---|---|---|
| Relational | টেবিল, foreign key | যেকোনো join, নতুন প্রশ্ন | ACID, constraint | লেখা ছড়াতে হাতে sharding | PostgreSQL, MySQL |
| Key-value | key → অস্বচ্ছ মান | key দিয়ে GET/SET | সবচেয়ে দ্রুত, ভাগ সহজ | মান দিয়ে খোঁজা নেই | Redis, etcd |
| Document | নেস্টেড JSON | id বা field দিয়ে পুরো aggregate | নমনীয় গঠন | document পেরিয়ে join দুর্বল | MongoDB, Couchbase |
| Wide-column | partition → সাজানো সারি | partition key আর পরিসর | বিপুল লেখা, বহু data center | প্রশ্ন বদলালে নতুন টেবিল | Cassandra, HBase |
| Graph | node, edge, property | কয়েক ধাপের সম্পর্ক | traversal সস্তা | ভাগ করা কঠিন, বড় সমষ্টি | Neo4j, JanusGraph |
| Time-series | সময় → মাপ | সময়ের সীমা, গড় | compression, retention | সাধারণ CRUD | InfluxDB, TimescaleDB |
| Search | শব্দ → নথি | পূর্ণ লেখায় খোঁজা | ranking, facet | মূল ঠিকানা নয় | Elasticsearch, OpenSearch |
৬ · NoSQL-এ data modelling — প্রশ্ন ধরে নকশা, পড়ার জন্য কপি আর তার দাম
Relational-এ আপনি আগে ডেটাকে normalise করেন, তারপর যেকোনো প্রশ্ন করেন। NoSQL-এ, বিশেষ করে key-value আর wide-column-এ, ক্রম উল্টো — যে প্রশ্ন, সেই আকার. অভিজ্ঞ দলগুলো মোটামুটি এই পাঁচ ধাপ মানে:
- Access pattern-এর তালিকা: প্রশ্ন, সেকেন্ডে কতবার, latency-লক্ষ্য (পর্ব ২)।
- প্রশ্নপিছু একটি partition: উত্তর যেন এক জায়গায় থাকে।
- Denormalise আর কপি: denormalization — পড়ার পথে join না রেখে তথ্য কপি।
- কপি কে হালনাগাদ করবে: একই অনুরোধে, নাকি পরে event বা CDC দিয়ে — তখন কিছুক্ষণ কপিগুলো আলাদা।
- Partition-এর আকার আর hot key: কোনো partition সীমাহীন বাড়বে না তো? (অনুশীলন ৩)
Single-table design — এক টেবিলে কয়েক রকম সত্তা
DynamoDB-কে ঘিরে একটি প্রচলিত নকশা-রীতি আছে — single-table design. ভিন্ন ধরনের সত্তা একই টেবিলে থাকে, সাধারণ নামের দুটি key দিয়ে: PK (partition key) আর SK (sort key). যেগুলো একসাথে লাগে, সেগুলোর PK এক, তাই একটি query-তেই সব আসে — যেন আগে থেকে join করা:
| PK | SK | বাকি তথ্য |
|---|---|---|
CUST#1 | PROFILE | name Rina, phone 01700-000001 |
CUST#1 | ORDER#2026-10-01#1001 | total 730 |
CUST#1 | ORDER#2026-10-05#1003 | total 1160 |
ORDER#1003 | ITEM#10 | Rice 5kg × 2, 450 |
ORDER#1003 | ITEM#12 | Tea 400g × 1, 260 |
PK = CUST#1-এর একটি query-তে Rina-র profile আর সব অর্ডার, তারিখের ক্রমে; SK-এর শুরু ORDER#2026-10 হলে শুধু অক্টোবরের। দাম — টেবিলটি পড়া কঠিন, নতুন access pattern-এ বদলানো কঠিন, বিশ্লেষণে ডেটা রপ্তানি লাগে। অনেকে বেশিরভাগ দলকে প্রশ্নপিছু সহজ আলাদা টেবিলের পরামর্শ দেন — এটি কৌশল, সবার জন্য নিয়ম নয়।
কপির দাম — হাতে-কলমে
Denormalisation শুধু NoSQL-এর নয়; relational-এও আগে থেকে join করা টেবিল বা materialized view রাখা হয়। নিচের এডিটরে অংশ ১-এর চারটি টেবিল আছে; বানানো হবে join-করা orders_view, তারপর দেখা হবে পড়া কতটা সহজ হয় — আর একটি ফোন নম্বর বদলালে কী ঘটে:
-- denormalise: অর্ডারের পাতার জন্য আগে থেকে join করে রাখা একটি টেবিল
CREATE TABLE orders_view AS
SELECT o.id AS order_id, c.id AS customer_id, c.name AS customer_name, c.phone AS customer_phone,
p.name AS product, i.qty, i.qty * i.unit_price AS line_total
FROM orders o JOIN customers c ON c.id = o.customer_id
JOIN order_items i ON i.order_id = o.id JOIN products p ON p.id = i.product_id;
CREATE INDEX idx_view_order ON orders_view(order_id);
-- পড়ার পথ: join-এ চারটি টেবিলে খোঁজ, view-তে একটিতে
EXPLAIN QUERY PLAN
SELECT c.name, c.phone, p.name, i.qty FROM orders o JOIN customers c ON c.id = o.customer_id
JOIN order_items i ON i.order_id = o.id JOIN products p ON p.id = i.product_id WHERE o.id = 1003;
EXPLAIN QUERY PLAN
SELECT customer_name, customer_phone, product, qty FROM orders_view WHERE order_id = 1003;
-- দাম ১: একই নাম আর ফোন নম্বর কয়েক জায়গায় কপি
SELECT customer_name, COUNT(*) AS copies FROM orders_view GROUP BY customer_id;
-- দাম ২: Rina ফোন বদলালেন, কিন্তু লেখা হলো শুধু customers-এ (update anomaly)
UPDATE customers SET phone = '01700-999999' WHERE id = 1;
SELECT v.order_id, v.product, v.customer_phone AS copy, c.phone AS truth
FROM orders_view v JOIN customers c ON c.id = v.customer_id
WHERE v.customer_phone <> c.phone;
-- সারাতে প্রতিটি কপিতে লেখা: একটি বদল, অনেক লেখা (write fan-out)
UPDATE orders_view SET customer_phone = '01700-999999' WHERE customer_id = 1;প্রথম plan-এ চারটি সারি — SEARCH o, SEARCH c, SEARCH i USING INDEX sqlite_, SEARCH p: চার টেবিলে চারটি খোঁজ। orders_view-এ একটিই: SEARCH orders_view USING INDEX idx_view_order (order_id=?). দাম ১: Rina | 4, Arif | 1 — Rina-র নাম-ফোন চার জায়গায়। দাম ২: ফোন বদলের পর অমিলের query 4টি সারি দেখায়, প্রতিটিতে 01700-000001 বনাম 01700-999999 — update anomaly. শেষে “✓ UPDATE — ৪টি সারি বদলেছে”: এক তথ্যের বদলে চারটি লেখা। SQLite এক যন্ত্রে চলে; distributed store-এ এই চারটি লেখা হয়তো চারটি partition-এ, আর সবগুলোর একসাথে সফল হওয়ার নিশ্চয়তা আলাদা করে আনতে হয়।
| দাম | কী ঘটে | প্রচলিত প্রতিকার |
|---|---|---|
| Update anomaly | একটি কপি বদলায়, বাকিগুলো পুরোনো | সব কপির তালিকা; একই transaction বা নির্ভরযোগ্য event-এ হালনাগাদ |
| Write fan-out | একটি বদলে অনেক লেখা — জনপ্রিয় পণ্যের নাম বদলালে লাখো সারি | ঘনঘন বদলায় এমন তথ্য কপি না করে শুধু id রাখা |
| সাময়িক অমিল | event-এ হালনাগাদ হলে কিছুক্ষণ দুই কপি দুই রকম | কোন পাতায় কতটা পুরোনো চলে, আগে ঠিক করা |
| Migration আর backfill | নতুন প্রশ্নে নতুন টেবিল, পুরোনো ডেটা ভরাট | backfill-এর সময় আর খরচ হিসাবে রাখা |
| Storage | একই তথ্য কয়েকবার | সাধারণত সবচেয়ে সস্তা দাম |
কপি না ইতিহাস? সব কপি “ভুল” নয়। অর্ডারের unit_price হলো কেনার মুহূর্তের দাম — পণ্যের দাম পরে বদলালেও পুরোনো অর্ডার বদলাবে না। ডেলিভারির ঠিকানাও তেমনি। এগুলো ইতিহাসের ছবি (snapshot), হালনাগাদ করলেই বরং ভুল হবে (অনুশীলন ৪)। বিস্তারিত DBMS কোর্সের Denormalization পর্বে।
৭ · Distributed SQL, consistency আর প্রচলিত ভুল ধারণা
NewSQL আর distributed SQL — SQL রেখেই ছড়ানো
SQL, join আর ACID রেখেও কি ডেটাবেস নিজেই ডেটা ভাগ আর কপি করতে পারে? এর উত্তর NewSQL বা distributed SQL: ডেটা নিজে থেকে খণ্ডে (range) ভাগ হয়, প্রতিটি খণ্ডের কপিগুলো consensus-এ (Raft বা Paxos, পর্ব ৪২) একমত থাকে, আর খণ্ড পেরোনো transaction ডেটাবেস নিজেই সমন্বয় করে। অ্যাপের কাছে এটি প্রায় সাধারণ SQL database.
Google-এর Spanner প্রবন্ধ (OSDI, ২০১২) দেখায় পৃথিবীজুড়ে ছড়ানো ডেটায় transaction; ঘড়ির অনিশ্চয়তা সীমিত রাখে GPS আর atomic clock-ভিত্তিক TrueTime (ঘড়ির সমস্যা পর্ব ৩৯-এ)। পরের বছরের F1 প্রবন্ধ (VLDB, ২০১৩) অনুযায়ী Google-এর বিজ্ঞাপন ব্যবস্থার ডেটা আগে হাতে-sharding করা MySQL-এ ছিল, আর resharding-এর যন্ত্রণা থেকেই Spanner-এর ওপর SQL স্তর F1. এই ধারণা থেকে open-source জগতে আসে CockroachDB আর YugabyteDB-এর মতো Spanner-ধাঁচের ডেটাবেস, আর MySQL-এর সাথে মানানসই TiDB.
দাম? প্রতিটি commit-এ কপিগুলোর সংখ্যাগরিষ্ঠের সম্মতি — এক data center-এ সামান্য, region পেরোলে দূরত্বভেদে কয়েক দশ থেকে কয়েকশো মিলিসেকেন্ড; খণ্ড পেরোনো transaction আরও ধীর; কিছু PostgreSQL বা MySQL বৈশিষ্ট্য ভিন্নভাবে চলে; চালানোও জটিল। এটি বিনা মূল্যের scaling নয় — ভিন্ন একটি trade-off.
উল্টো দিক থেকেও দুই জগৎ কাছে এসেছে: PostgreSQL ৯.৪ (২০১৪)-এ JSONB, MongoDB ৪.০ (২০১৮)-তে কয়েকটি document জুড়ে transaction, DynamoDB-তেও ২০১৮ সালে transaction-এর API — প্রতিটির নিজের সীমাসহ। রেখাটি আজ অনেক ঝাপসা।
“NoSQL মানে consistency নেই” — কথাটি ভুল
অনেক NoSQL ডেটাবেসে প্রতিটি অনুরোধে ঠিক করা যায় কতগুলো কপির উত্তর লাগবে — tunable consistency. Cassandra-ধাঁচের store-এ consistency level ONE, QUORUM বা ALL; ৩টি কপির মধ্যে লেখা আর পড়া দুটোই quorum-এ (২টি করে) হলে ২ + ২ > ৩, তাই প্রতিটি পড়া অন্তত একটি সর্বশেষ লেখা পাওয়া কপি ছোঁয় (প্রমাণ আর ব্যতিক্রম পর্ব ৪১-এ)। MongoDB-তে write concern majority; DynamoDB-তে “strongly consistent read”. দাম একই — বেশি কপির অপেক্ষা মানে বেশি latency, কপি বিকল হলে কম availability.
দুই “consistency” গুলিয়ে ফেলবেন না। ACID-এর C মানে ডেটার নিয়ম রক্ষা — stock কখনো ঋণাত্মক হবে না, প্রতিটি অর্ডারের ক্রেতা আছে (পর্ব ২৪)। CAP-এর C মানে সব কপি থেকে সর্বশেষ মান দেখা, অর্থাৎ linearizability (পর্ব ৪০)। একটি ডেটাবেস একটিতে শক্ত আর অন্যটিতে দুর্বল হতে পারে; নকশার আলোচনায় কোনটি বলছেন, স্পষ্ট করুন (consistency).
ভুল ধারণা আর বাস্তব
| ভুল ধারণা | বাস্তব | কীভাবে যাচাই করবেন |
|---|---|---|
| “NoSQL সবসময় দ্রুত” | নকশা-করা প্রশ্নে দ্রুত, অন্য প্রশ্নে প্রায়ই পুরো scan (অংশ ৫-এর query B); index-সহ relational-এ key দিয়ে খোঁজাও কয়েকটি page মাত্র | নিজের access pattern আর ডেটায় load test, p99 মেপে (পর্ব ৪) |
| “SQL scale করে না” | Read replica, sharding-এর স্তর, distributed SQL; প্রকাশিত TAO প্রবন্ধ (২০১৩) অনুযায়ী Facebook-এর social graph-এর স্থায়ী ঠিকানাও ছিল MySQL, ওপরে cache | Estimation (পর্ব ৩): সংখ্যা কি সত্যিই একটি primary-র সীমা ছাড়ায়? |
| “Schemaless মানে schema নেই” | Schema সরে যায় কোডে — schema-on-read; প্রতিটি পাঠক কোডকে পুরোনো সব গঠন সামলাতে হয় (পর্ব ১২) | পুরোনো document-এ field না থাকলে কোড কী করে? JSON Schema-ভিত্তিক যাচাই আছে কি? |
| “NoSQL-এ transaction নেই” | একটি item-এ প্রায় সবখানে atomic লেখা; অনেকগুলোতে কয়েক item-এর transaction-ও, সীমাসহ | অফিসিয়াল ডকুমেন্টেশনে সীমা পড়ুন |
| “NoSQL মানেই eventual consistency” | প্রায়ই tunable, latency-র দামে | Staging-এ একটি কপি থামিয়ে দেখুন |
৮ · কীভাবে বাছবেন — চেকলিস্ট, polyglot persistence আর একটি পূর্ণ উদাহরণ
ঈদের আগে বাড়ি ফেরার পথ বাছাইয়ের মতো: বাস, ট্রেন, লঞ্চ — কোনোটিই “সেরা” নয়; কতজন, কত মালপত্র, কত সময়, কত ভাড়া — এসব দেখে ঠিক হয়। Database বাছাইয়ে সাতটি প্রশ্ন:
| প্রশ্ন | উত্তর যদি “হ্যাঁ” বা “বেশি” | ঝোঁক |
|---|---|---|
| ১ · সম্পর্ক কি অনেক, নানা সংমিশ্রণে join লাগে? | অর্ডার-ক্রেতা-পণ্য-পেমেন্ট জড়ানো | Relational; বহু ধাপের সম্পর্কে graph |
| ২ · কয়েকটি সত্তা জুড়ে transaction লাগে? | টাকা, মজুদ, আসন | Relational বা distributed SQL |
| ৩ · প্রশ্নগুলো কি জানা আর অল্প কয়েকটি? | key দিয়ে বিপুল পড়া | Key-value বা wide-column; অজানা প্রশ্নে relational বা warehouse |
| ৪ · লেখার হার আর আকার কি এক যন্ত্রের সীমা ছাড়ায়? | হিসাব করে, অনুমানে নয় | Wide-column, sharding বা distributed SQL |
| ৫ · কতটা consistency লাগে? | নিজের লেখা নিজে সঙ্গে সঙ্গে দেখা, বা linearizable | Strong consistency দেয় এমন store বা সেই মোড |
| ৬ · দলের কারা এটি জানেন? | কেউ চালাননি | পরিচিত প্রযুক্তি — রাতের ডাকে কে সারাবেন? |
| ৭ · চালানোর খরচ কত? | backup, upgrade, monitoring, দাম | কম সিস্টেম; vendor lock-in-এর হিসাব |
সন্দেহ থাকলে একটি relational database দিয়ে শুরু করুন — PostgreSQL বা MySQL, সাথে একটি cache. বিশেষ store আনুন তখনই, যখন প্রয়োজনটি সংখ্যায় দেখাতে পারেন: “এই প্রশ্নের p99 লক্ষ্য মিলছে না”, “লেখার হার মাপা সীমার তিন গুণ”. যুক্তি “বড় কোম্পানিরা ব্যবহার করে” হলে থামুন।
Polyglot persistence — আর তার খরচ
একই সিস্টেমে কাজভেদে ভিন্ন database — polyglot persistence. প্রতিটি ডেটা পায় মানানসই store, কিন্তু বিল আসে তিন জায়গা থেকে:
- মিলিয়ে রাখা: অ্যাপ নিজে দুই জায়গায় লিখলে (dual write) একটি ব্যর্থ হলেই দুই store চিরকাল আলাদা। নির্ভরযোগ্য পথ — মূল database-এর log থেকে CDC, বা একই transaction-এ outbox (পর্ব ২৫), তারপর event stream (পর্ব ৩৩)।
- সাময়িক অমিল: search-এ পুরোনো দাম — কতক্ষণ চলবে, আগে ঠিক করুন।
- চালানো আর মানুষ: প্রতিটি store-এর নিজের backup, upgrade, monitoring, failure mode (পর্ব ৫) আর দক্ষতা।
পূর্ণ উদাহরণ — একটি কাল্পনিক অনলাইন বাজারের ডেটা কোথায় যাবে
ধরা যাক একটি অনলাইন বাজার: চাল-ডাল থেকে মোবাইল ফোন আর শাড়ি, পিকে সেকেন্ডে কয়েকশো অর্ডার, লাখো পণ্য, session, সুপারিশ আর খোঁজার বাক্স। পাঁচ ধরনের ডেটা, পাঁচটি সিদ্ধান্ত, তারপর মেলানোর দাম:
- অর্ডার আর পেমেন্ট → relational. অর্ডারের সারি, item, stock কমানো আর পেমেন্টের রেকর্ড — হয় সব, নয় কিছুই না (অংশ ১-এর transaction); ফেরত আর মাসিক রিপোর্টের মতো অজানা প্রশ্নও আসবে। টাকার জন্য double-entry ledger (পর্ব ৬০)। এটিই মূল ঠিকানা।
- Catalogue → document. চাল, মোবাইল ফোন আর শাড়ির বৈশিষ্ট্য একেবারে আলাদা; একটি পণ্যের পাতা = একটি document, ছবি object storage-এ (পর্ব ২৬)। সমান ভালো বিকল্প: PostgreSQL-এর
JSONBকলাম, আলাদা সিস্টেম ছাড়াই। - Session আর cart → key-value. প্রতিটি পাতায় পড়া, key জানা, মেয়াদ আছে — Redis-ধাঁচের store, TTL-সহ (পর্ব ১৫)। Cart দিনের পর দিন টিকিয়ে রাখতে হলে user ID ধরে টেকসই store-এ।
- সুপারিশ → graph. “যাঁরা এটি কিনেছেন, তাঁরা আর কী কিনেছেন” — দুই-তিন ধাপের traversal. শুরুতে ক্রয়-ইতিহাস থেকে রাতে offline হিসাব করে key-value-এ রাখাও চলে; graph database তখনই, যখন ধাপ বেশি আর উত্তর তাজা চাই।
- খোঁজা → search engine. “চা পাতা ৪০০ গ্রাম” লিখলে বানান-ভুলসহ মিল, দাম আর ব্র্যান্ডের filter, প্রাসঙ্গিকতার ক্রম; index মূল catalogue-এর কপি (পর্ব ২৭)।
- মিলিয়ে রাখা — polyglot-এর আসল দাম। মূল ঠিকানা দুটি (relational, document); search আর graph তাদের কপি, CDC বা event stream দিয়ে হালনাগাদ (নিচের ছবি)। পাঁচটি সিস্টেম মানে পাঁচ রকম backup আর রাতের ডাক; ছোট দল শুরু করতে পারে শুধু PostgreSQL (JSONB আর full-text-সহ) আর Redis দিয়ে।
অনলাইন বাজার — polyglot persistence-এর স্থাপত্য
Clients → API gateway → services → their stores · CDC stream keeps search and graph in sync
বাস-টিকিট সেবা হলে? আসন, বুকিং আর পেমেন্ট → relational (শর্তযুক্ত update বা row lock, পর্ব ২৪ আর ৬০); পেমেন্টের ১০ মিনিটের আসন-hold → key-value, TTL-সহ; বাসের বিবরণ → document; রুট খোঁজা → search engine.
আরও গভীরে: Amazon-এর DynamoDB প্রবন্ধ (USENIX ATC, ২০২২) দেখায়, নামের মিল সত্ত্বেও DynamoDB ২০০৭ সালের Dynamo-র leaderless নকশা নয়: প্রতিটি partition-এর একটি leader আর Multi-Paxos. Facebook-এর TAO প্রবন্ধ (USENIX ATC, ২০১৩) দেখায়, বিশাল graph-ও relational store আর তার ওপরের cache দিয়ে চলে, যদি access pattern জানা থাকে। আর Stonebraker ও Çetintemel-এর “One Size Fits All” (ICDE, ২০০৫) polyglot জগতের তাত্ত্বিক পটভূমি (ভাবনার প্রশ্ন ১)।
পরীক্ষা ও ইন্টারভিউতে এই বিষয়ে প্রায়ই যা জিজ্ঞেস করা হয়:
- কখন NoSQL, কখন SQL? — সম্পর্ক, কয়েক সত্তার transaction আর অজানা প্রশ্ন → relational; জানা অল্প access pattern, বিপুল লেখা বা বিচিত্র গঠন → উপযুক্ত NoSQL.
- Embed না reference? — একসাথে পড়া, সীমিত আকার আর একই মালিকানা হলে embed; সীমাহীন, ভাগ করা বা আলাদা বদলালে reference.
- Cassandra-তে partition key কীভাবে বাছবেন? — প্রশ্নের পুরো উত্তর এক partition-এ, partition-এর আকার সীমিত (দরকারে তারিখের bucket), আর ভার সমানভাবে ছড়ানো।
- NoSQL কি ACID দেয় না? — একটি item-এ প্রায় সবাই atomic; অনেকেই এখন সীমাসহ কয়েক item-এর transaction দেয়; আর ACID-এর C ও CAP-এর C আলাদা।
৯ · AI-এর যুগে database বাছাই যাচাই
অনলাইন বাজারের database নকশা চাইলে AI প্রায়ই বলবে: “scale-এর জন্য MongoDB, cache-এ Redis, event-এ Kafka, analytics-এ Cassandra.” নামগুলো বাস্তব, কিন্তু access pattern, সংখ্যা বা failure mode ছাড়া। খুঁজুন এই ভুলগুলো:
| AI-এর ভুল | কেন ভুল | কীভাবে ধরবেন |
|---|---|---|
| “Scale-এর জন্য NoSQL নিন” — প্রশ্ন, key বা QPS ছাড়া | গতি শুধু নকশা-করা প্রশ্নে; বাকিগুলো scan | প্রতিটি store-এর তিনটি প্রধান প্রশ্ন আর key চান |
| টাকা বা আসন eventual-consistent store-এ, শর্তযুক্ত লেখা বা transaction ছাড়া | দুই কপিতে দুই বুকিং গৃহীত হলে পরে মেলানো যায় না | জিজ্ঞেস করুন: একই আসনে দুজন একসাথে চাপলে কী হয়? |
| ACID-এর consistency আর CAP-এর consistency গুলিয়ে ফেলা | একটি ডেটার নিয়ম রক্ষা, অন্যটি কপির সর্বশেষ মান | প্রতিটি “consistent” শব্দের পাশে কোন অর্থে, লিখতে বলুন |
| বানানো সীমা আর দাম — “node-পিছু সেকেন্ডে দশ লাখ লেখা” | সীমা সংস্করণ আর hardware-ভেদে আলাদা | অফিসিয়াল ডকুমেন্টেশন; ছোট benchmark |
| Storage বা partition-এর হিসাবে একক বা গুণের ভুল (দিন আর বছর, KB আর MB) | Partition দশ গুণ বড় হলে নকশাই বদলায় | অনুশীলন ৩-এর মতো নিজে আবার হিসাব |
| শুরুর দিনেই পাঁচটি database, আর dual write দিয়ে মেলানো | Over-engineering; dual write-এ চিরস্থায়ী অমিল | প্রতিটি store-এর জন্য একটি মাপা প্রয়োজন; CDC বা outbox আছে কি না |
AI-কে বিকল্প তুলনা আর হিসাব যাচাইয়ে ব্যবহার করুন — অনুমান আপনি দিন, উত্তরে অনুমান আর সিদ্ধান্ত আলাদা চান:
AI-এর দেওয়া database নকশা হাতে পেলে — যাচাইয়ের চেকলিস্ট
ভাবনার প্রশ্ন
প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন।
প্র ০১Stonebraker ২০০৫ সালে লিখেছিলেন, “এক মাপে সবার হয়” যুগ শেষ। তাহলে আজও PostgreSQL বা MySQL কেন বহু নতুন সিস্টেমের প্রথম পছন্দ?
১৯৮০ থেকে ২০০০-এর দশক পর্যন্ত একটি সাধারণ relational database-ই সব কাজ করত — লেনদেন, বিশ্লেষণী রিপোর্ট, এমনকি লেখা খোঁজা। Michael Stonebraker আর Uğur Çetintemel ২০০৫ সালে (ICDE) যুক্তি দেন যে বিশেষ কাজের জন্য বানানো ইঞ্জিন — বিশ্লেষণের জন্য column store, অবিরাম ডেটার জন্য stream processor, খোঁজার জন্য search engine — সাধারণ ইঞ্জিনকে বহু গুণে হারাবে। পরের দুই দশক তাঁকে অনেকটাই ঠিক প্রমাণ করেছে: data warehouse প্রায় সবই column store, খোঁজা চলে Lucene-ভিত্তিক ইঞ্জিনে, আর বিপুল লেখার জন্য এসেছে wide-column store.
তবু দৈনন্দিন লেনদেনের (OLTP) কাজে সাধারণ relational database টিকে আছে, আর প্রায়ই প্রথম পছন্দ। তিনটি কারণ:
- বাড়ানোর ক্ষমতা: PostgreSQL-এ
JSONB-এ document, নিজস্ব full-text search, PostGIS-এ ভৌগোলিক ডেটা, TimescaleDB-এর মতো extension-এ time-series — একটি ইঞ্জিনের ভেতরেই কয়েকটি “মডেল”. - বেশিরভাগ সিস্টেম মাঝারি আকারের: অংশ ২-এর হিসাবের মতো, দিনে লাখো অর্ডারও একটি ভালো primary আর replica-য় সাধারণত আঁটে।
- চালানোর খরচ: একটি পরিচিত সিস্টেম মানে একটি backup-পদ্ধতি, একটি monitoring, একটি দক্ষতা। প্রতিটি নতুন store polyglot-এর বিল বাড়ায়।
অর্থাৎ Stonebraker-এর যুক্তি ঠিক সেখানে খাটে, যেখানে একটি কাজের চাপ বাকি সবকে ছাপিয়ে যায় — বিশাল বিশ্লেষণ, বিশাল খোঁজা, বিশাল লেখা। বাকি জায়গায় একটি সাধারণ ইঞ্জিন আর তার extension “যথেষ্ট ভালো”, আর যথেষ্ট ভালোর সাথে কম জটিলতা প্রায়ই জেতে।
একটি বাস্তব কৌশল: প্রতিটি বিশেষ কাজের জন্য একটি সংখ্যা ঠিক রাখুন — খোঁজার p99, মাসিক রিপোর্ট চলার সময়, পিকের লেখার হার — আর সংখ্যাটি ভাঙলে তবেই সেই একটি কাজ বিশেষ ইঞ্জিনে সরান, CDC দিয়ে কপি পাঠিয়ে। পুরো সিস্টেম নয়, শুধু সেই কাজটি।
মূল উপলব্ধি: বিশেষ ইঞ্জিন আনুন যখন একটি নির্দিষ্ট কাজ সংখ্যায় সাধারণ ইঞ্জিনকে ছাড়িয়ে যায়; তার আগে extension আর ভালো নকশাই সস্তা পথ।
প্র ০২NoSQL-এর জন্মে Google-এর Bigtable-এর বড় ভূমিকা। তাহলে Google নিজেই পরে Spanner আর F1 দিয়ে SQL আর transaction-এ ফিরল কেন?
Bigtable (২০০৬) একটি row-এর ভেতরে atomic লেখা দিত, কিন্তু কয়েকটি row বা টেবিল জুড়ে transaction দিত না। বিশাল ক্রল-ডেটা বা লগের জন্য এটি যথেষ্ট ছিল; কিন্তু যেসব অ্যাপে কয়েকটি জিনিস একসাথে বদলাতে হয় — বিজ্ঞাপনের বাজেট আর তার প্রচারণা, ব্যবহারকারী আর তাঁর অনুমতি — সেখানে প্রতিটি দলকে নিজের কোডে transaction-এর বিকল্প বানাতে হতো, আর তাতে সূক্ষ্ম বাগ ঢুকত।
প্রথম সংশোধন ছিল Megastore (CIDR, ২০১১): Bigtable-এর ওপর ছোট ছোট “entity group”-এর ভেতরে ACID, Paxos দিয়ে data center-জুড়ে কপি — কিন্তু লেখার গতি কম। তারপর Spanner (OSDI, ২০১২): পৃথিবীজুড়ে ছড়ানো ডেটায় সাধারণ transaction. প্রবন্ধের লেখকেরা খোলাখুলি যুক্তি দেন — transaction-এর অতিরিক্ত ব্যবহারে performance-এর সমস্যা হলে প্রোগ্রামাররা তা সারাবেন, সেটি বরং ভালো, সবসময় transaction-এর অভাবের চারপাশ দিয়ে কোড লেখার চেয়ে। আর F1 প্রবন্ধ (VLDB, ২০১৩) অনুযায়ী বিজ্ঞাপন ব্যবস্থা হাতে-sharding করা MySQL ছেড়ে Spanner-এর ওপর SQL-এ আসে, কারণ resharding ছিল দীর্ঘ আর ঝুঁকিপূর্ণ প্রকল্প।
দাম শূন্য নয়। Spanner প্রতিটি commit-এ consensus চালায়, আর ঘড়ির অনিশ্চয়তা পেরোনো পর্যন্ত অপেক্ষা করে (commit wait) — প্রবন্ধে উল্লিখিত মাপে সাধারণত কয়েক মিলিসেকেন্ড, তবে region পেরোলে নেটওয়ার্কের দূরত্বই বড় খরচ। তাই এটি সস্তা হয়নি, শুধু সম্ভব হয়েছে।
এখান থেকে NoSQL সম্পর্কে একটি গুরুত্বপূর্ণ শিক্ষা মেলে: প্রথম দিকের NoSQL-এর ত্যাগগুলো — join নেই, transaction নেই — প্রকৃতির নিয়ম ছিল না; সেই সময়ের প্রকৌশলের সীমা ছিল। Consensus আর ঘড়ির ভালো ব্যবস্থাপনা যখন সম্ভব করল, transaction ফিরে এল।
মূল উপলব্ধি: প্রোগ্রামারের সময় আর ভুলের খরচও নকশার হিসাবের অংশ। যে নিশ্চয়তা ডেটাবেস দেয় না, তা প্রতিটি অ্যাপকে নিজে দিতে হয় — আর সেটিই সাধারণত সবচেয়ে দামি।
প্র ০৩“Schemaless” database-এ schema বদলানো কি সত্যিই সহজ? নাকি ঝামেলা শুধু জায়গা বদলায়?
দুই পথের তুলনা করি। ধরা যাক ক্রেতার একটি ফোন নম্বরের জায়গায় এখন কয়েকটি নম্বর রাখতে হবে।
- Schema-on-write (relational): একবার migration — নতুন টেবিল বা কলাম, পুরোনো ডেটা রূপান্তর — তারপর সব সারি একই গঠনে। কষ্টটা আগেভাগে: বড় টেবিলে migration সময় নেয়, lock-এর ঝুঁকি থাকে, তাই ধাপে ধাপে করতে হয় (expand, তারপর contract; পর্ব ১২ আর পর্ব ৫৩)।
- Schema-on-read (document): নতুন কোড সঙ্গে সঙ্গে
phones: [...]লিখতে শুরু করে; পুরোনো document-এphoneথেকেই যায়। এখন প্রতিটি পাঠক কোডকে দুই গঠনই বুঝতে হবে, চিরকাল — অথবা যতদিন না কেউ সব পুরোনো document রূপান্তর করেন। বাগ ধরা পড়ে মাস ছয়েক পরে, যখন কোনো পুরোনো document প্রথমবার পড়া হয়।
অর্থাৎ schema উধাও হয় না; সেটি ডেটাবেস থেকে সরে ছড়িয়ে পড়ে প্রতিটি সার্ভিসের কোডে, প্রতিটি report-এর script-এ আর প্রতিটি data pipeline-এ। অভিজ্ঞ দলগুলো তাই document-এ একটি schema_version field রাখে, পড়ার সময় পুরোনো গঠন নতুনে বদলে আবার লেখে (lazy migration), পেছনে ধীরে ধীরে backfill চালায়, আর ডেটাবেসের validation দিয়ে অন্তত বাধ্যতামূলক field আর type আটকায়। বিশ্লেষণের দলের কাছে সমস্যাটি আরও বড়: পাঁচ বছরের document-এ পাঁচ রকম গঠন থাকলে প্রতিটি রিপোর্টের query-কেও পাঁচ রকম গঠন বুঝতে হয়।
দুই জগৎ এখানেও কাছাকাছি এসেছে: document database-এ JSON Schema-ভিত্তিক validation, আর relational-এ JSONB কলামে নমনীয় অংশ — কঠিন নিয়মের জন্য কলাম, ঘনঘন বদলানো বিচিত্র বৈশিষ্ট্যের জন্য JSON. নমনীয়তার আসল লাভ শুরুর দ্রুত পরীক্ষা-নিরীক্ষায় আর সত্যিকারের বিচিত্র ডেটায় (প্রতিটি পণ্য-বিভাগের আলাদা বৈশিষ্ট্য)।
মূল উপলব্ধি: প্রশ্নটি “schema থাকবে কি না” নয়, বরং “কে, কখন তা প্রয়োগ করবে” — লেখার মুহূর্তে ডেটাবেস, নাকি পড়ার মুহূর্তে প্রতিটি কোড।
প্র ০৪একই সিস্টেমে কোন ডেটায় eventual consistency চলে, আর কোনটায় চলে না — এটি কীভাবে ঠিক করবেন?
একটি সহজ পরীক্ষা: দুটি পরস্পরবিরোধী লেখা যদি দুই কপিতে একসাথে গৃহীত হয়, পরে কি সেগুলো অর্থপূর্ণভাবে মেলানো যায়?
- মেলানো যায় — eventual চলে: একটি পোস্টের like-সংখ্যা (দুই কপির বাড়তি যোগ করলেই হয়), দেখার সংখ্যা, “শেষ কবে online”. শপিং কার্টও এই দলে: Dynamo প্রবন্ধ (২০০৭) জানায়, অমিল হলে দুই কার্ট মিলিয়ে নেওয়া হতো — তাতে কখনো মুছে ফেলা জিনিস আবার ফিরে আসত, আর ব্যবসা সেটি মেনে নিয়েছিল, কারণ “কার্টে যোগ করা যাচ্ছে না” তার চেয়ে খারাপ। এমন মেলানোর নিয়মকে গাণিতিকভাবে নিরাপদ করার কৌশল CRDT (পর্ব ৪৬)।
- মেলানো যায় না — strong লাগে: একটি বাসের আসন B4 দুই কপিতে দুই যাত্রীকে বিক্রি হলে পরে “মিলিয়ে” একজনকে আসন দেওয়া যায় না — একজন যাত্রী স্টেশনে গিয়ে বিপদে পড়বেন। টাকার balance, একটি username-এর মালিকানা, শেষ পিসের stock — সবখানে একটি নিয়ম (invariant) আছে, যা দুই আলাদা সিদ্ধান্তে ভাঙে। এখানে লাগে একটি জায়গায় সিদ্ধান্ত: relational-এর row lock, শর্তযুক্ত লেখা (lightweight transaction), বা একটি leader.
দ্বিতীয় প্রশ্ন ব্যবহারকারীর চোখ থেকে: নিজের লেখা নিজে সঙ্গে সঙ্গে দেখা (read-your-writes) কি জরুরি? প্রোফাইলের ছবি বদলে আবার পুরোনো ছবি দেখলে মানুষ বিভ্রান্ত হন — যদিও অন্যরা কয়েক সেকেন্ড পরে দেখলেও ক্ষতি নেই। এটি সাধারণত একই কপি থেকে পড়া বা লেখার পর কিছুক্ষণ primary থেকে পড়া দিয়ে সমাধান হয় (পর্ব ২২)।
Tunable consistency ঠিক এই কারণেই কাজের: একই database-এ like-এর জন্য ONE, আসনের জন্য শর্তযুক্ত লেখা বা QUORUM-এর চেয়েও কড়া পথ। নকশায় তাই প্রতিটি ডেটার পাশে লিখে রাখুন — “অমিল হলে কী ক্ষতি, আর কতক্ষণ সহ্য হয়”.
মূল উপলব্ধি: consistency পুরো সিস্টেমের একটি সুইচ নয়; প্রতিটি ডেটা আর প্রতিটি প্রশ্নের আলাদা সিদ্ধান্ত, যা ঠিক করে অমিলের ব্যবসায়িক ক্ষতি।
প্র ০৫পাঁচটি database-এর polyglot নকশা দেখতে সুন্দর। তাহলে একটি ছোট দল প্রায়ই একটি database-এ শুরু করে কেন, আর কখন সেটি ভাঙে?
Polyglot-এর সুবিধা স্পষ্ট, কিন্তু খরচগুলো লুকিয়ে থাকে। চারটি বড় খরচ:
- Availability-র গুণফল: একটি অনুরোধ যদি তিনটি আলাদা store-এর ওপর নির্ভর করে, আর প্রতিটির availability ৯৯.৯%, তবে মোট প্রায় ০.৯৯৯³ ≈ ৯৯.৭% (পর্ব ৫)। বছরে বন্ধ থাকার সময় প্রায় ৮.৮ ঘণ্টা থেকে বেড়ে প্রায় ২৬ ঘণ্টা।
- Dual write-এর ফাঁদ: অ্যাপ প্রথমে মূল database-এ, তারপর search-এ লেখে; মাঝে crash হলে দুই store চিরকাল আলাদা। নির্ভরযোগ্য পথ হলো মূল database-এর log পড়ে CDC, বা একই transaction-এ outbox (পর্ব ২৫) — কিন্তু সেটি আরেকটি pipeline, যা চালাতে আর পাহারা দিতে হয়।
- চালানো আর দক্ষতা: প্রতিটি store-এর upgrade, backup থেকে ফেরানোর মহড়া, নিরাপত্তা patch আর নিজের অদ্ভুত failure mode. ছয়জনের দলে পাঁচটি database মানে প্রায় প্রত্যেককে একটি করে বিশেষজ্ঞ হতে হবে।
- ভাঙা সত্য: “এই অর্ডারের আসল অবস্থা কী?” — উত্তর কোন store দেবে, তা স্পষ্ট না থাকলে প্রতিটি ঘটনা-তদন্ত কঠিন।
তাই অনেক দল “প্রথমে একটি PostgreSQL” পথে হাঁটে: লেনদেন সারিতে, বিচিত্র বৈশিষ্ট্য JSONB-এ, সাধারণ খোঁজা full-text index-এ, ছোট queue SKIP LOCKED-এ, আর পাশে একটি Redis. এটি ভাঙে তখন, যখন একটি মাপা প্রয়োজন পূরণ হয় না — খোঁজার প্রাসঙ্গিকতা, facet আর বানান-ভুল সহ্য করা ব্যবসার মূল বিষয় হলে search engine; লেখার হার একটি primary-র মাপা সীমা ছাড়ালে wide-column বা sharding; বহু ধাপের সম্পর্ক তাজা চাইলে graph.
লক্ষ করুন, এই পথটি “সব কিছু এক জায়গায়, চিরকাল” নয়; এটি “প্রমাণ পেলে তবেই ভাগ”. প্রথম দিন থেকে মূল ঠিকানা স্পষ্ট রাখলে আর পরিবর্তনগুলো event হিসেবে প্রকাশ করলে পরে নতুন store যোগ করা অনেক সহজ হয়।
মূল উপলব্ধি: প্রতিটি বাড়তি database একটি সুবিধা কেনে আর একটি স্থায়ী দায় নেয়। দায়টি সংখ্যায় লিখুন, তারপর সিদ্ধান্ত নিন।
অনুশীলন
খাতা-কলম পাশে রাখুন। আগে নিজে করুন, তারপর উত্তর মিলিয়ে নিন।
-
চারটি পরিস্থিতি, চারটি database: যুক্তিসহ store বাছুন — (ক) একটি কাল্পনিক mobile banking সেবার লেনদেন আর balance; (খ) একটি ride-sharing অ্যাপে প্রতি ৪ সেকেন্ডে প্রতিটি চালকের অবস্থান — “কাছের চালক” খুঁজতে লাগে শুধু সর্বশেষটি, তবে ৯০ দিনের ইতিহাসও রাখতে হবে (ধরা যাক একসাথে ৫০,০০০ চালক সক্রিয়); (গ) একটি সংবাদ-সাইটের লেখা, যার ধরন আলাদা আলাদা (ভিডিও, ছবির গ্যালারি, লাইভ ব্লগ), আর পাঠক শিরোনাম ও বিষয়ে খোঁজেন; (ঘ) একটি পেশাজীবী নেটওয়ার্কে “আপনি যাঁদের চিনতে পারেন” — দুই-তিন ধাপের সংযোগ।
(ক) Relational, ACID transaction-সহ double-entry ledger: প্রতিটি লেনদেন দুটি বিপরীত সারি, balance একই transaction-এ হালনাগাদ। Constraint, নিরীক্ষা আর অজানা রিপোর্ট — relational-এর শক্তি। কয়েক region-এ একসাথে লেখা লাগলে distributed SQL, commit latency মেনে। Eventual-consistent store চলে না (ভাবনার প্রশ্ন ৪)।
(খ) দুই store. সর্বশেষ অবস্থান: memory-ভিত্তিক key-value store, ভৌগোলিক index-সহ (Redis-ধাঁচের GEO কমান্ড), TTL-এ অফলাইন চালক নিজে থেকে মুছে যান (পর্ব ৫৮)। লেখা ৫০,০০০ ÷ ৪ = ১২,৫০০/সেকেন্ড, প্রতিটি একটি key-এর ওপর লেখা, হারালে ক্ষতি সামান্য। ইতিহাস: wide-column বা time-series, partition key
(driver_id, day), ৯০ দিনের TTL — দিনে প্রায় ১০৮ কোটি সারি (১২,৫০০ × ৮৬,৪০০)।(গ) Document database (বা relational-এ
JSONB) মূল ঠিকানা — প্রতিটি ধরনের নিজের field; খোঁজায় search engine, CDC দিয়ে হালনাগাদ; পাতা CDN-এ (পর্ব ১৭), ছবি-ভিডিও object storage-এ।(ঘ) Graph database, যদি উত্তর তাজা আর ধাপ বেশি চাই। প্রায়ই সস্তা বিকল্প: রাতে batch job-এ (পর্ব ৩৬) সুপারিশ হিসাব করে key-value-এ রাখা — পাতা খুললে একটি GET.
-
Relational থেকে document: একটি বাস-টিকিট সেবার relational টেবিল:
trips(trip_id, route, travel_date, dep_time, operator, bus_type, fare),seats(trip_id, seat_no, status)— প্রতি trip-এ ৪০টি আসন,bookings(booking_id, passenger_id, trip_id, seat_no, fare, booked_at),passengers(passenger_id, name, phone). Access pattern: (১) trip-এর পাতা আর আসন-ম্যাপ — প্রতি একটি বুকিংয়ের বিপরীতে প্রায় ২০০ বার পড়া হয়; (২) যাত্রীর “আমার টিকিট”, নতুনটি আগে; (৩) আসন বুক করা — একটি আসন কখনো দুজনকে নয়। একটি document মডেল নকশা করুন, trip document-এর আকার আন্দাজ করুন, আর বলুন কোন ঝুঁকি থাকে।trips collection: প্রতি trip একটি document, ৪০টি আসন ভেতরে embed করা — আসন সীমিত, সবসময় trip-এর সাথে পড়া হয়, trip-এরই মালিকানা। আকার: trip-এর field প্রায় ২৫০ byte + ৪০ × প্রায় ৩০ byte (
{no:"B4", status:"free"}) ≈ ১,৪৫০ byte, অর্থাৎ দেড় KB-এর মতো — পাতা এক পড়ায়।বুক করা: একটি document-এর ভেতরের বদল atomic, তাই শর্তযুক্ত update:
db.trips.updateOne( { _id: tripId, seats: { $elemMatch: { no: "B4", status: "free" } } }, { $set: { "seats.$.status": "held" } }) // modifiedCount 0 = আসনটি আগেই নেওয়াbookings collection: আলাদা document,
trip_idআরpassenger_id-এর reference, সাথে রুট, তারিখ, সময়, আসন আর ভাড়ার snapshot (ইতিহাস)। “আমার টিকিট” ={passenger_id: 1, booked_at: -1}index-এ একটি query. যাত্রীর document-এ বুকিং embed করবেন না — তালিকা সীমাহীন।passengers collection: আলাদা — অনেক বুকিং একে নির্দেশ করে, আর এটি নিজে বদলায়।
ঝুঁকি: আসন “held” করা আর booking লেখা দুটি document — মাঝে ব্যর্থ হলে আসন আটকে থাকে। প্রতিকার: hold-এর মেয়াদ (
held_until), পেমেন্ট সফল হলে “booked”, নয়তো ছেড়ে দেওয়া — একটি saga (পর্ব ২৫)। ঈদের আগে জনপ্রিয় trip-এর document hot হয়ে যায় — সামনে virtual waiting room (পর্ব ১৮)। অনেক দল তাই আসন আর পেমেন্ট relational-এ রাখে। -
Anti-pattern খুঁজুন: একটি কাল্পনিক cold-storage কোম্পানির ৫,০০০টি sensor প্রতি ১০ সেকেন্ডে তাপমাত্রা পাঠায়; disk-এ প্রতিটি সারি প্রায় ৮০ byte (ধরা যাক)। নকশা:
PRIMARY KEY ((sensor_id), ts), আর ড্যাশবোর্ডের সতর্কবার্তা আসেSELECT * FROM readings WHERE temp > 40 ALLOW FILTERINGথেকে। সমস্যাগুলো কী? এক বছর পর একটি partition কত বড়? ঠিক নকশা কী?সমস্যা ১ — সীমাহীন partition: একটি sensor-এর দিনে ৮৬,৪০০ ÷ ১০ = ৮,৬৪০টি সারি, বছরে ৮,৬৪০ × ৩৬৫ = ৩১,৫৩,৬০০টি; × ৮০ byte = ২৫,২২,৮৮,০০০ byte ≈ ২৫২ MB, তিন বছরে প্রায় ৭৫৭ MB, বাড়তেই থাকবে। এত বড় partition পড়া, compaction আর মেরামতে ধীর।
সমস্যা ২ — ALLOW FILTERING: প্রতিবার সব node-এর সব partition পড়া — এক বছরে ৫,০০০ × ৩১,৫৩,৬০০ ≈ ১,৫৭৭ কোটি সারি, কয়েকটি গরম রিডিং খুঁজতে। অংশ ৫-এর query B-এর বড় সংস্করণ।
ঠিক নকশা: দিনের bucket —
PRIMARY KEY ((sensor_id, day), ts), সাথেCLUSTERING ORDER BY (ts DESC): প্রতিটি partition ৮,৬৪০ × ৮০ = ৬,৯১,২০০ byte ≈ ০.৬৯ MB; “শেষ ২৪ ঘণ্টা” এক বা দুটি partition. সতর্কবার্তার আলাদা টেবিলalerts_by_day(partition key(day), বেশি হলে(day, hour)), যেখানে লেখার সময়েই stream processor (পর্ব ৩৬) ৪০-এর বেশি রিডিং লেখে। কাঁচা ডেটায় TTL (ধরা যাক ৯০ দিন), দীর্ঘমেয়াদে ঘণ্টাভিত্তিক গড়।বাড়তি পর্যবেক্ষণ: মোট লেখা মাত্র ৫,০০০ ÷ ১০ = ৫০০/সেকেন্ড। এই হারে একটি time-series database, এমনকি PostgreSQL-এর ওপর TimescaleDB-ও সম্ভবত যথেষ্ট — wide-column বাছার আগে সংখ্যাটি দেখুন।
-
Output অনুমান করুন: অংশ ৬-এর চারটি টেবিল আর
orders_view(ফোন বদলের আগের অবস্থা) ধরুন। চালানো হলোUPDATE products SET price = 500 WHERE id = 10;আরUPDATE customers SET city = 'Gazipur' WHERE id = 1;. তারপর: (১) Rina-রorders_view-এরorder_id, product, line_total; (২) অর্ডার ১০০৩-এর বর্তমান দামে মোট বনাম কেনার দামে মোট; (৩) শহরভিত্তিক বিক্রি। ফলগুলো আগে খাতায় লিখুন।নিজের অনুমান মিলিয়ে দেখতে চালান:
UPDATE products SET price = 500 WHERE id = 10; UPDATE customers SET city = 'Gazipur' WHERE id = 1; SELECT order_id, product, line_total FROM orders_view WHERE customer_id = 1 ORDER BY order_id, product; SELECT SUM(i.qty * p.price) AS at_new_price, SUM(i.qty * i.unit_price) AS charged FROM order_items i JOIN products p ON p.id = i.product_id WHERE i.order_id = 1003; SELECT c.city, SUM(i.qty * i.unit_price) AS revenue FROM customers c JOIN orders o ON o.customer_id = c.id JOIN order_items i ON i.order_id = o.id GROUP BY c.city ORDER BY revenue DESC;(১)
orders_viewবদলায়নি:1001 | Lentils 1kg | 280,1001 | Rice 5kg | 450,1003 | Rice 5kg | 900,1003 | Tea 400g | 260— কপি টেবিল মূল টেবিলের বদল জানে না; তবে এখানে সেটিই ঠিক, কারণ line total কেনার মুহূর্তের ইতিহাস। (২)1260 | 1160— নতুন দামে ২ × ৫০০ + ২৬০ = ১,২৬০, কিন্তু ক্রেতা দিয়েছেন ২ × ৪৫০ + ২৬০ = ১,১৬০; এজন্যইunit_priceঅর্ডারে কপি থাকে। (৩)Gazipur | 1890,Rajshahi | 260— Rina-র পুরোনো ঢাকার অর্ডারও এখন Gazipur-এর বিক্রি! শহর ছিল ক্রেতার বর্তমান ঠিকানা; ডেলিভারির ঠিকানা অর্ডারে snapshot না রাখলে পুরোনো রিপোর্ট বদলে যায়। Normalization মানে “কখনো কপি নয়” নয় — ইতিহাস আলাদা তথ্য।
নিজেকে যাচাই করুন
ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।
ক্রমএকটি wide-column store-এ query-driven নকশার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)
- 📝 প্রতিটি প্রশ্ন, তার ঘনত্ব আর latency-র লক্ষ্য লেখা
- 🔑 প্রতিটি প্রশ্নের জন্য partition key বাছা, যাতে উত্তর এক partition-এ থাকে
- ↕️ Partition-এর ভেতরের ক্রমের জন্য clustering key বাছা
- 📋 দরকারি তথ্য কপি করে প্রশ্নপিছু টেবিল বানানো
- 🔄 প্রতিটি কপি কে, কখন হালনাগাদ করবে — ঠিক করা
- 📏 Partition-এর আকার আর hot key হিসাব করে load test
একটি সেবায় প্রতিটি অর্ডারে তিনটি কাজ — অর্ডার লেখা, stock কমানো আর পেমেন্টের রেকর্ড — একসাথে সফল বা একসাথে ব্যর্থ হতে হবে, আর হিসাব বিভাগ প্রতি মাসে নতুন নতুন ধরনের রিপোর্ট চায়। প্রথম পছন্দ হিসেবে কোনটি সবচেয়ে মানানসই?
- একটি in-memory key-value store
- Eventual consistency-র একটি wide-column store
- ACID transaction-সহ একটি relational database
- একটি graph database
একটি Cassandra-ধাঁচের টেবিলের key: PRIMARY KEY ((route, travel_date), dep_time). নিচের কোন query একটিমাত্র partition পড়েই শেষ হয়?
- WHERE route = 'DHK-RAJ'
- WHERE dep_time >= '08:00'
- WHERE operator = 'OP-7'
- WHERE route = 'DHK-RAJ' AND travel_date = '2026-12-10' AND dep_time >= '08:00'
একটি ফোরামে একটি পোস্টের মন্তব্য কয়েকটি থেকে হাজার হাজার পর্যন্ত হয়, আর প্রায়ই “একজন ব্যবহারকারীর সব মন্তব্য” দেখানো হয়। Document database-এ মন্তব্য রাখার সবচেয়ে ভালো উপায় কোনটি?
- সব মন্তব্য পোস্টের document-এর ভেতরে embed করা
- একটি key-value store-এ, key = post id, মান = সব মন্তব্যের তালিকা
- মন্তব্য আলাদা collection-এ, post আর user field-এ index-সহ
- সব পোস্টের সব মন্তব্য একটি বড় document-এ
একটি store-এ প্রতিটি ডেটার ৩টি কপি (N = 3), আর প্রতিটি লেখা ২টি কপিতে নিশ্চিত হলে সফল (W = 2). প্রতিটি পড়া যেন অন্তত একটি সর্বশেষ লেখা পাওয়া কপি ছোঁয়, সেজন্য প্রতিটি পড়ায় অন্তত কয়টি কপির (R) উত্তর লাগবে?
- ১টি
- ২টি
- ৩টি — সবগুলো, এর কম হলে কখনো চলে না
- W = 2 হলে R-এর কোনো শর্ত লাগে না
“Schemaless” document database সম্পর্কে কোন কথাটি সঠিক?
- এতে ডেটার কোনো গঠন থাকে না, তাই schema নিয়ে ভাবতেই হয় না
- নতুন field যোগ করলে পুরোনো সব document নিজে থেকেই নতুন গঠনে বদলে যায়
- এতে কখনো কোনো ধরনের যাচাই (validation) রাখা যায় না
- Schema সরে যায় কোডে; প্রতিটি পাঠককে পুরোনো আর নতুন গঠন দুটোই সামলাতে হয়
একটি denormalised orders_view টেবিলে প্রতিটি item-এর সারিতে ক্রেতার ফোন নম্বর কপি করা আছে; একজন ক্রেতার এমন ১২টি সারি। তিনি ফোন নম্বর বদলালেন, কিন্তু শুধু customers টেবিল হালনাগাদ করা হলো। কী ঘটবে?
- ১২টি সারিতে পুরোনো নম্বর থেকে যাবে — update anomaly; সারাতে ১২টি লেখা লাগবে
- ডেটাবেস নিজে থেকেই ১২টি কপি হালনাগাদ করবে
- UPDATE-টি foreign key-এর ত্রুটিতে ব্যর্থ হবে
- কিছুই হবে না, কারণ orders_view সবসময় customers থেকে পড়ে
শব্দকোষ
লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:
মনে রাখুন
- প্রশ্নটি “SQL না NoSQL” নয় — কোন ডেটা, কোন access pattern, কোন নিশ্চয়তা; বড় সিস্টেমে প্রায়ই দুটোই।
- Relational: schema, join, ACID, SQL আর অজানা প্রশ্নে নমনীয়তা; scaling — index, বড় যন্ত্র, cache, read replica, শেষে হাতে-করা sharding, যেখানে join আর transaction কঠিন।
- চার পরিবার: key-value (key দিয়ে), document (embed না reference), wide-column (partition key + clustering key, প্রশ্নপিছু টেবিল), graph (traversal); পাশে time-series আর search.
- NoSQL-এ নকশা প্রশ্ন ধরে: denormalise আর কপি — দাম update anomaly আর write fan-out; কোনটি কপি আর কোনটি ইতিহাস, আলাদা করুন।
- Distributed SQL আর tunable consistency রেখা ঝাপসা করেছে; সন্দেহ থাকলে একটি relational database আর একটি cache, বিশেষ store আসবে মাপা প্রয়োজনে — polyglot-এর খরচ হিসাব করে।
আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ
- পর্ব ২১ · Index আর query performance — composite, covering index আর EXPLAINপরবর্তী পর্ববাছাই করা database-কে দ্রুত করা, plan পড়ে।
- সিস্টেম ডিজাইন · কোর্স হোম৬৬টি পর্বপুরো কোর্সের রোডম্যাপ — কোন পর্ব কখন।
- NoSQL — key-value, document, wide-column আর graphগভীরে যানচার পরিবারের কমান্ড, query-first নকশা আর BASE, হাতে-কলমে।
- MongoDB — document ডেটাবেস হাতে-কলমেপাশের ট্র্যাকEmbed না reference, aggregation pipeline, index আর transaction.
- Distributed database — partitioning, sharding আর CAPপাশের ট্র্যাকShard পেরোনো query, two-phase commit আর NewSQL-এর ভেতরের কথা।