সরাসরি পর্বে যান
পর্ব ১৯ · ৬৬-এর মধ্যে · মডিউল ৪

Storage engine — B-tree বনাম LSM-tree, WAL, row বনাম column store

Storage Engines — B-trees vs LSM-trees, Write-Ahead Logs & Row vs Column Stores

Taught in Bangla, this lesson explains how storage engines store, find and protect data given disk and memory latency. It measures read, write and space amplification with the RUM conjecture, shows write-ahead logging, fsync and group commit, computes B+-tree height from fan-out, builds a tiny LSM-tree with memtable, SSTables, compaction, tombstones and Bloom filters, compares B-trees and LSM-trees, row and column stores, and durability options for in-memory databases.

এই পর্বে যা শিখবেন

  • Storage engine-এর কাজ, আর HDD, SSD ও RAM-এর latency কেন engine-এর নকশা ঠিক করে দেয়
  • Read, write আর space amplification মাপা, আর RUM conjecture-এর ভাষায় যেকোনো engine-কে বিচার করা
  • WAL কীভাবে durability আর crash recovery দেয়, fsync-এর দাম আর group commit-এর হিসাব
  • B+-tree-এর page, fan-out আর উচ্চতা হিসাব; LSM-tree-এর memtable, SSTable, compaction, tombstone আর Bloom filter
  • কোন কাজে B-tree, কোন কাজে LSM, hash index বা column store — আর RAM-এ রাখা database-এর durability-র বিকল্প

১ · Storage engine কী — আর কেন এর নকশাই গতি ঠিক করে

পাড়ার মুদি দোকানির তিনটি কাজ: মাল তাকে রাখা, খদ্দের চাইলে দ্রুত খুঁজে দেওয়া, আর হিসাব যেন বিদ্যুৎ গেলেও হারিয়ে না যায়। মাল কীভাবে সাজানো, তা-ই ঠিক করে কাজ কত দ্রুত হবে।

Database-এর ভেতরে এই দোকানির নাম storage engine. SQL বোঝা আর query-র পরিকল্পনা ওপরের স্তরের কাজ; কিন্তু ডেটা ডিস্কের কোথায়, কোন আকারে বসবে, কীভাবে খোঁজা হবে আর crash-এর পরে কীভাবে ফিরবে — সে সিদ্ধান্ত engine-এর। একই MySQL-এ আলাদা engine বসানো যায় (InnoDB, বা RocksDB-ভিত্তিক MyRocks); MongoDB-র ডিফল্ট engine হলো WiredTiger. Engine বদলালে SQL একই থাকে, অথচ লেখার গতি, ডিস্কের খরচ আর p99 latency কয়েক গুণ বদলে যেতে পারে।

একটি query কোন স্তর পেরিয়ে ডিস্কে পৌঁছায়

Application → query layer → storage engine → OS / file system → device

  1. অ্যাপSQL / get(key)কী চাই
  2. Query স্তরparser, optimizerকীভাবে আনব
  3. Storage engineWAL · B-tree/LSM · cacheকোথায়, কোন আকারে
  4. OS আর ডিস্কfsync · SSD/HDDআসল byte
ছবি ১৯.১ — এই পর্বের পুরো কাজ কমলা বাক্সটির ভেতরে। Query স্তরের index-বাছাই আর EXPLAIN পাবেন পর্ব ২১-এ।

যন্ত্রের সত্য — random বনাম sequential

Engine-এর নকশা বুঝতে তিনটি সংখ্যার মাত্রা মনে রাখুন (পর্ব ৩-এর latency-র মই; বিস্তারিত Computer Architecture · মেমরি হায়ারার্কি-তে)। সংখ্যাগুলো মোটামুটি; যন্ত্র আর প্রজন্মভেদে কয়েক গুণ আলাদা হয়।

যন্ত্রএকটি ছোট random পড়াএকটানা (sequential) পড়ামূল কথা
RAMপ্রায় ১০০ nsসেকেন্ডে কয়েক দশ GBবিদ্যুৎ গেলে মুছে যায়
SSD (NVMe)কয়েক দশ থেকে প্রায় ১০০ µsসেকেন্ডে কয়েক GBপড়া সস্তা; জায়গায় নতুন করে লেখা যায় না
HDDপ্রায় ৫–১০ ms (seek + ঘূর্ণন)সেকেন্ডে প্রায় ১০০–২৫০ MBrandom-sequential ফারাক কয়েকশো গুণ

একটি হিসাব: ধরা যাক ১ GiB (২৩০ byte) ডেটা ৪ KiB-র টুকরোয় পড়তে হবে — ২,৬২,১৪৪টি টুকরো। HDD-তে প্রতিটি random I/O-তে ১০ ms ধরলে প্রায় ২,৬২১ সেকেন্ড (৪৪ মিনিট); টানা পড়লে (sequential I/O, সেকেন্ডে ২০০ MB) মাত্র ৫.৪ সেকেন্ড — প্রায় ৪৯০ গুণ কম। SSD-তে (random ১০০ µs, টানা ৩ GB/s) ২৬ সেকেন্ড বনাম ০.৩৬ সেকেন্ড — ফারাক কমে প্রায় ৭০ গুণে, তবু থাকে।

মূল ধারণা

তিনটি চিরন্তন নিয়ম: (১) যা বারবার লাগে, RAM-এ রাখুন; (২) ডিস্কে লিখুন বড় টুকরোয়, পরপর; (৩) পুরোনো জায়গায় ছোট ছোট বদল সবচেয়ে দামি। B-tree, LSM-tree, WAL, column store — প্রতিটি এই নিয়মগুলোর একেকটি আপস। ডিস্ক, page আর buffer pool-এর খুঁটিনাটি: DBMS · স্টোরেজ — ডিস্ক, page, record আর buffer pool.

ডেটাবেস ডিস্কের সাথে লেনদেন করে নির্দিষ্ট মাপের ব্লকে — page, সাধারণত ৪ থেকে ১৬ KB (ডিফল্টে SQLite-এ ৪ KB, PostgreSQL-এ ৮ KB, InnoDB-তে ১৬ KB). সারি ১০০ byte হলেও ডিস্ক থেকে আসে পুরো page; RAM-এ তার কপি থাকে buffer pool-এ, database-এর নিজস্ব cache.

২ · তিন রকম খরচ — read, write আর space amplification

Engine তুলনার সবচেয়ে কাজের ভাষা হলো amplification — আপনি যতটুকু চাইলেন, engine-কে তার কত গুণ কাজ করতে হলো।

📖 Read amplification

একটি key পড়তে আসলে কত page বা byte পড়তে হলো। ১০০ byte-এর সারির জন্য ১৬ KB-র ৪টি page পড়লে read amplification byte-এ প্রায় ৬৫০ গুণ, গোনায় ৪টি I/O.

✍️ Write amplification

ডিস্কে আসলে যত byte লেখা হলো ÷ অ্যাপ যত byte লিখতে বলেছিল। ১০০ byte বদলাতে ১৬ KB-র পুরো page লিখলে write amplification প্রায় ১৬৪ গুণ; সাথে WAL-এ আরেকবার।

💾 Space amplification

ডিস্কে মোট জায়গা ÷ আসল জীবিত ডেটা। অর্ধেক-ফাঁকা page, মোছার চিহ্ন আর পুরোনো কপি space amplification বাড়ায়।

বাস্তবে buffer pool অনেকটা বাঁচায়: একই page-এ ৫০টি বদল জমিয়ে একবার লিখলে ১৬৪ গুণ নেমে আসে প্রায় ৩.৩ গুণে, যদিও এলোমেলো key-তে জমানোর সুযোগ কম। SSD-র ভেতরেও আলাদা একটি write amplification আছে (ভাবনার প্রশ্ন ৩) — তাই এটি গতি, খরচ আর যন্ত্রের আয়ু, তিনটিরই প্রশ্ন।

RUM conjecture — তিনটি একসাথে কমানো যায় না

২০১৬ সালে Manos Athanassoulis ও সহকর্মীরা (EDBT সম্মেলনের প্রবন্ধ) একটি পর্যবেক্ষণকে নাম দেন RUM conjecture — Read, Update, Memory (জায়গা): কোনো access method যদি তিনটির দুটির ঊর্ধ্বসীমা বেঁধে দেয়, তাহলে তৃতীয়টির একটি নিম্নসীমা অনিবার্য। এটি গাণিতিক প্রমাণিত উপপাদ্য নয়, “conjecture” — কিন্তু বাস্তবের প্রতিটি নকশায় খাটে। দুই চরম উদাহরণে দেখুন:

কাঠামোReadWrite (update)Space
শুধু log-এর শেষে যোগ, কোনো index নেই❌ পুরো log scan✅ ১ গুণ, ক্রমিক❌ সব পুরোনো কপি থেকে যায়
পুরোপুরি সাজানো, ফাঁকহীন array✅ binary search❌ মাঝে ঢোকাতে অর্ধেক array সরানো✅ প্রায় ১ গুণ
B+-tree✅ ৩–৪ page, স্থির🟡 প্রতি লেখায় একটি page বদল🟡 page প্রায় ৭০% ভরা
LSM-tree🟡 কয়েক জায়গায় খোঁজ✅ ক্রমিক; compaction-এ বারবার নতুন করে লেখা🟡 compaction-এর নীতির ওপর নির্ভর

পর্বের বাকিটা এই টেবিলের শেষ দুই সারির বিস্তার। কোনো engine “সেরা” নয়; প্রশ্ন হলো আপনার access pattern-এ কোন খরচ সবচেয়ে দামি — read:write ratio, key এলোমেলো না ক্রমিক, range query লাগে কি না, আর টানাটানি ডিস্কে না RAM-এ।

৩ · Write-ahead log — আগে খাতায়, পরে তাকে

মুদি দোকানি বাকির খাতায় আগে লেখেন “রহিম — ২ কেজি চাল, ১৫০ টাকা”, তারপর ধীরেসুস্থে তাকের হিসাব মেলান। মাঝপথে বিদ্যুৎ গেলেও খাতা দেখে বাকি কাজ শেষ করা যায়; আর খাতায় লেখা সহজ, কারণ সবসময় শেষ লাইনে।

Database-এর খাতার নাম write-ahead log (WAL), আর নিয়মটি এক লাইনের: কোনো বদল ডেটা-ফাইলে পৌঁছানোর আগে তার বিবরণ log-এ টেকসইভাবে লেখা থাকতে হবে, আর commit-এর log record ডিস্কে স্থায়ী হলে তবেই commit সফল। লাভ তিনটি:

  • Durability সস্তায়: log-এ লেখা শুধু ফাইলের শেষে যোগ (append-only) — sequential I/O; আসল page পরে, অনেক বদল জমিয়ে লেখা যায়।
  • Crash recovery: চালু হওয়ার সময় engine শেষ checkpoint থেকে log পড়ে, ডেটা-ফাইলে না-পৌঁছানো বদল আবার করে (redo) আর অসম্পূর্ণ transaction উল্টে দেয় (undo); checkpoint তাই recovery-র সময়ের সীমা টানে।
  • বাড়তি উপহার: একই log পাঠিয়ে replica তৈরি করা যায় (পর্ব ২২), log পড়ে অন্য সিস্টেমে বদলের ধারা পাঠানো যায় (পর্ব ৩৩, log-ভিত্তিক streaming), আর log-কেই মূল সত্য ধরার নকশা আসবে পর্ব ৩৫-এ (event sourcing).

PostgreSQL-এর WAL, InnoDB-র redo log, SQLite-এর WAL মোড, আর LSM-tree-এর “commit log” — নাম আলাদা, ধারণা এক। File system-ও একই কৌশলে metadata রক্ষা করে: Operating Systems · crash consistency আর journaling. Log record, LSN আর ARIES-এর তিন ধাপের পুরো বিবরণ পাবেন DBMS · Recovery — log, WAL, checkpoint আর ARIES-এ; এখানে আমরা দেখব নকশার সংখ্যাগুলো।

fsync-এর দাম আর group commit

অ্যাপ write() করলে OS ডেটা প্রথমে নিজের page cache-এ রাখে; ডিস্কে পৌঁছানোর নিশ্চয়তা দেয় fsync (বা fdatasync). fsync ধীর: সাধারণ SSD বা HDD-তে প্রায় ০.১ থেকে ১০ ms; বিদ্যুৎ-বিভ্রাটে সুরক্ষিত cache-ওয়ালা enterprise SSD-তে অনেক কম। ধরা যাক একটি fsync-এ ২ ms লাগে।

Group commit — একটি হিসাব

প্রতিটি commit নিজের fsync করলে একটি log-এ সেকেন্ডে সর্বোচ্চ ১ ÷ ০.০০২ = ৫০০ commit. কিন্তু একই সময়ে অনেক connection commit করলে engine তাদের log record একসাথে জমিয়ে একটি fsync করে — group commit. প্রতি fsync-এ গড়ে ২০টি commit গেলে সেকেন্ডে ২০ ÷ ০.০০২ = ১০,০০০ commit. দাম: একটি commit-কে দলের বাকিদের জন্য গড়ে fsync-এর সময়ের প্রায় অর্ধেক বাড়তি অপেক্ষা করতে হতে পারে — এটি পর্ব ৪-এর batching-এর চেনা throughput-latency আপস।

“লেখা ধীর? fsync বন্ধ করুন” — এতে durability-র প্রতিশ্রুতিই ভাঙে: crash-এ শেষ কয়েক সেকেন্ডের commit হারাতে পারে, ডেটা-ফাইল অসংগতও হতে পারে। PostgreSQL-এর synchronous_commit বা InnoDB-র innodb_flush_log_at_trx_commit-এর মতো সেটিং নিয়ন্ত্রিত ঝুঁকি নিতে দেয়; ঠিক কী হারায় তা সংস্করণভেদে আলাদা, তাই অফিসিয়াল documentation দেখে ঠিক করুন।

১৯৯২ সালে IBM-এর C. Mohan ও সহকর্মীদের ARIES প্রবন্ধ WAL-ভিত্তিক recovery-কে আধুনিক রূপ দেয়; আজকের প্রায় সব বড় relational database তার কোনো রূপ মানে। SQLite ২০১০ সালে (৩.৭.০ সংস্করণে) WAL মোড যোগ করে, যাতে লেখকের পাশাপাশি পাঠকরাও একসাথে চলতে পারেন।

৪ · B-tree পরিবার — page, fan-out আর উচ্চতা

একটি ঠিকানা খোঁজার কথা ভাবুন: আগে বিভাগ (৮টির একটি), তারপর জেলা, তারপর উপজেলা, তারপর রাস্তা — চার-পাঁচ ধাপেই দেশের যেকোনো বাড়ি। B-tree ঠিক এই কৌশল: প্রতিটি ধাপে দুটির একটি নয়, কয়েকশো শাখার একটি।

ডেটাবেসে যা চলে তা মূলত B+-tree: প্রতিটি node একটি page; internal node-এ শুধু পথ দেখানোর key আর child-এর ঠিকানা; ডেটা বা সারির ঠিকানা থাকে leaf-এ, আর leaf-গুলো শিকলে বাঁধা — range query মানে একবার নেমে শিকল ধরে হাঁটা। একটি page-এ কতগুলো child ধরে, তার নাম fan-out.

উচ্চতার হিসাব — কেন ৩–৪ স্তরেই কোটি key
  1. Fan-out f ≈ page-এ ব্যবহারযোগ্য byte ÷ (key + pointer). ১৬ KB page, ৮ byte key, ৮ byte pointer, ৭০% ভরা ধরলে f ≈ ১৬,৩৮৪ × ০.৭ ÷ ১৬ ≈ ৭১৬।
  2. উচ্চতা h হলে গাছে সর্বোচ্চ প্রায় fhটি key ধরে (leaf-সহ প্রতিটি স্তর f গুণ বাড়ায়)। তাই N key-এর জন্য h = ⌈logf N⌉.
  3. f = ৫০০ হলে: ২ স্তরে ২,৫০,০০০; ৩ স্তরে ৫০০৩ = ১২,৫০,০০,০০০ (১২.৫ কোটি); ৪ স্তরে ৬,২৫০ কোটি key.

তুলনায় binary search tree-তে ১০ কোটি key-এর উচ্চতা প্রায় ২৭ (log₂ ১০৮ ≈ ২৬.৬)। প্রতিটি স্তর একটি সম্ভাব্য ডিস্ক-পড়া — fan-out-ই ২৭-কে ৩-এ নামায়; আর ওপরের ছোট স্তরগুলো প্রায় সবসময় RAM-এ, তাই আসল ডিস্ক-পড়া প্রায়ই ১টি।

কেন ৭০%? এলোমেলো insert-এ ভরা page দুই ভাগ হয়, নতুন page শুরু করে অর্ধেক ভরা অবস্থায়; Andrew Yao-র ১৯৭৮ সালের বিশ্লেষণে গড় ভরাট প্রায় ln 2 ≈ ৬৯%. নিচের হিসাবযন্ত্রে page size, key আর fill বদলে চালাতে পারেন:

// B+-tree: how tall is the tree, and how many page reads does one lookup cost?
// Assumptions (change them): key 8 bytes (BIGINT), pointer 8 bytes, pages ~70% full.
const KEY = 8, PTR = 8, FILL = 0.7;
const fanout = (page) => Math.floor((page * FILL) / (KEY + PTR));
function height(n, f) {                      // levels, leaf included: smallest h with f^h >= n
  let h = 1, cap = f;
  while (cap < n) { cap *= f; h++; }
  return h;
}
function levelBytes(n, f, page) {            // [leaf level, all levels above it] in bytes
  let pages = Math.ceil(n / f), upper = 0;
  const leaf = pages;
  while (pages > 1) { pages = Math.ceil(pages / f); upper += pages; }
  return [leaf * page, upper * page];
}
const size = (b) => b >= 2 ** 30 ? (b / 2 ** 30).toFixed(1) + " GB" : (b / 2 ** 20).toFixed(2) + " MB";
console.log("check: fan-out 500, 3 levels -> 500^3 = " + (500 ** 3).toLocaleString("en-US") + " keys");
const F = [500, fanout(4096), fanout(8192), fanout(16384)];
console.log("fan-out at 70% fill: 4 KB page " + F[1] + ", 8 KB " + F[2] + ", 16 KB " + F[3]);
console.log("\n     N  h(f=500)  h(4 KB)  h(8 KB)  h(16 KB)   16 KB: leaf level  levels above");
for (let e = 3; e <= 10; e++) {
  const n = 10 ** e, [leaf, upper] = levelBytes(n, F[3], 16384);
  console.log(("10^" + e).padStart(6) + F.map((f, i) => String(height(n, f)).padStart(i ? 9 : 10)).join("") +
    size(leaf).padStart(19) + size(upper).padStart(14));
}
console.log("\nI/O per lookup: h page reads if nothing is cached; 1 (the leaf) if the levels above sit in RAM");

আউটপুট:

check: fan-out 500, 3 levels -> 500^3 = 125,000,000 keys
fan-out at 70% fill: 4 KB page 179, 8 KB 358, 16 KB 716

     N  h(f=500)  h(4 KB)  h(8 KB)  h(16 KB)   16 KB: leaf level  levels above
  10^3         2        2        2        2            0.03 MB       0.02 MB
  10^4         2        2        2        2            0.22 MB       0.02 MB
  10^5         2        3        2        2            2.19 MB       0.02 MB
  10^6         3        3        3        3           21.83 MB       0.05 MB
  10^7         3        4        3        3          218.23 MB       0.33 MB
  10^8         3        4        4        3             2.1 GB       3.08 MB
  10^9         4        4        4        4            21.3 GB      30.55 MB
 10^10         4        5        4        4           213.1 GB     305.25 MB

I/O per lookup: h page reads if nothing is cached; 1 (the leaf) if the levels above sit in RAM

প্রথম লাইন মূল দাবিটি যাচাই করে: fan-out ৫০০-র ৩ স্তরে ১২,৫০,০০,০০০টি key. Page বড় হলে গাছ নিচু: ১০৮ key-এ ৪ KB-র (f = ১৭৯) আর ৮ KB-র (f = ৩৫৮) গাছ ৪ স্তর, কারণ ৩৫৮৩ ≈ ৪.৬ কোটি; ১৬ KB-র (f = ৭১৬) ৩ স্তর। শেষ দুই কলাম আসল শিক্ষা: ১০১০ (১,০০০ কোটি) key-এ leaf স্তর প্রায় ২১৩ GB, অথচ ওপরের সব স্তর মিলে মাত্র ৩০৫ MB — এটুকু RAM-এ রাখলে প্রতিটি খোঁজ বড়জোর ১টি ডিস্ক-পড়া (এখানে MB = ২২০ আর GB = ২৩০ byte).

এবার একটি খোঁজ ধাপে ধাপে — একটি কাল্পনিক mobile wallet সেবার ১০ কোটি account, primary key দিয়ে:

  1. Queryid = 73456789১০ কোটি সারি
  2. Root১টি pageসবসময় RAM-এ
  3. Internal৪০০টির ১টিসাধারণত RAM-এ
  4. Leaf২ লাখের ১টিহয়তো ডিস্কে
  5. ফল৩টি pageবড়জোর ১টি ডিস্ক-পড়া
accounts: ১০ কোটি সারি · ১৬ KB page · root-এর ৪০০ child, বাকি node-এর ৫০০
এখন যে page
বাকি key-এর পরিসর
তুলনা (binary search, মোট)
ডিস্ক-পড়া (মোট)
  1. প্রশ্ন। SELECT balance FROM accounts WHERE id = 73456789. গাছে ১০ কোটি সারি: root-এর ৪০০টি child, প্রতিটি internal page-এর ৫০০টি, প্রতিটি leaf-এ ৫০০টি key — ৪০০ × ৫০০ × ৫০০ = ১০ কোটি। উচ্চতা ৩।
  2. Root. Page-এর ভেতরে ৩৯৯টি পথনির্দেশক key-এ binary search — প্রায় ৯টি তুলনা (log₂ ৪০০ ≈ ৮.৬)। 73456789 পড়ে ২৯৪তম child-এর পরিসরে, 73250001 থেকে 73500000. Root সবসময় RAM-এ, তাই ডিস্ক-পড়া নেই।
  3. Internal page. আবার প্রায় ৯টি তুলনা; key পড়ে ৪১৪তম child-এর পরিসরে, 73456501 থেকে 73457000. পুরো internal স্তর মাত্র ৪০০টি page, প্রায় ৬.৫ MB — buffer pool-এ সহজেই থাকে।
  4. Leaf. Leaf স্তরে ২ লাখ page, প্রায় ৩.৩ GB — সবটা RAM-এ না-ও থাকতে পারে, তাই এখানেই সম্ভাব্য একমাত্র ডিস্ক-পড়া। Binary search-এ key মিলল ২৮৯তম entry-তে। Clustered গাছে (InnoDB-র primary key, SQLite-এর rowid টেবিল) পুরো সারি এখানেই; PostgreSQL-এ heap-এর আরও একটি page.
  5. হিসাব। ৩টি page, প্রায় ২৭টি তুলনা (সব RAM-এ), বড়জোর ১টি ডিস্ক-পড়া। Binary search tree-তেও প্রায় ২৭টি তুলনা লাগত, কিন্তু ২৭টি আলাদা node মানে ২৭টি পর্যন্ত random পড়া। B-tree তুলনা কমায় না — ডিস্ক-পড়া কমায়।

লেখা — জায়গায় বদল আর page split

B-tree-তে একটি সারি বদলাতে engine ওপরের পথেই leaf-এ নামে, page-টি buffer pool-এ এনে জায়গায় বদলায় (in-place update), বিবরণ WAL-এ লেখে, আর পরে পুরো page ডিস্কে ফেরত লেখে। Leaf ভরে গেলে page split: অর্ধেক entry নতুন page-এ, বাবার node-এ একটি নতুন key; বাবাও ভরা থাকলে split ওপরে ছড়ায়, আর root ভাঙলে গাছ এক স্তর উঁচু হয়।

  • ক্রমিক key (auto-increment, সময়-ক্রমের ID): সব insert ডানতম leaf-এ — split সস্তা, cache-এ গরম একটি-দুটি page.
  • এলোমেলো key (যেমন random UUID): insert ছড়ায় সব leaf-এ — বেশি split, অর্ধেক-ভরা page, বড় টেবিলে প্রায় প্রতিটি insert-এ একটি cache miss. তাই সময়-ক্রমের ID (২০২৪ সালের RFC 9562-এর UUIDv7, বা Snowflake-ধাঁচের ID — পর্ব ২৩) জনপ্রিয় হচ্ছে।
  • প্রতিটি secondary index একটি আলাদা B-tree: ৫টি index-ওয়ালা টেবিলে একটি insert মানে ৬টি গাছে লেখা। InnoDB-র clustered নকশায় সারি থাকে primary key-এর গাছের leaf-এ, আর secondary index রাখে primary key; PostgreSQL-এ সারি আলাদা heap ফাইলে, index রাখে তার ঠিকানা (ভাবনার প্রশ্ন ২)।

Insert-delete-এর অ্যালগরিদম আর উচ্চতার সীমার প্রমাণ পাবেন DBMS · Index — B+ tree, hash আর bitmap আর DSA · B-tree আর B+ tree-এ; composite আর covering index বাছাই পর্ব ২১-এ।

নিজের চোখে — SQLite, একটি B-tree storage engine

এই পাতার SQLite চলে ব্রাউজারের memory-তে; এতে প্রতিটি টেবিল আর প্রতিটি index একটি করে B-tree. নিচের কোড WAL-এর সেটিং দেখে, ৫০,০০০টি account বানায়, তারপর দেখে কোন খোঁজ কোন গাছ ধরে যায়:

PRAGMA journal_mode;
PRAGMA journal_mode = WAL;
PRAGMA synchronous;
PRAGMA page_size;
CREATE TABLE accounts (
  id      INTEGER PRIMARY KEY,
  wallet  TEXT NOT NULL,
  balance INTEGER NOT NULL
);
WITH RECURSIVE seq(n) AS (
  SELECT 1 UNION ALL SELECT n + 1 FROM seq WHERE n < 50000
)
INSERT INTO accounts
SELECT n, 'W' || printf('%07d', (n * 7919) % 10000000), (n * 37) % 100000
FROM seq;
PRAGMA page_count;
EXPLAIN QUERY PLAN SELECT balance FROM accounts WHERE id = 31337;
EXPLAIN QUERY PLAN SELECT id FROM accounts WHERE wallet = 'W8157703';
CREATE UNIQUE INDEX idx_accounts_wallet ON accounts(wallet);
EXPLAIN QUERY PLAN SELECT id FROM accounts WHERE wallet = 'W8157703';
SELECT id, wallet, balance FROM accounts WHERE wallet = 'W8157703';
PRAGMA page_count;

WAL-এর সেটিং: journal_mode প্রথমে delete — SQLite-এর ডিফল্ট rollback journal, যেখানে বদলের আগে পুরোনো page-এর কপি আলাদা ফাইলে রাখা হয় আর commit-এ সেটি মুছে যায়। journal_mode = WAL-এর উত্তর wal: এখন থেকে বদল যাবে আলাদা একটি WAL ফাইলে, মূল ফাইলে পৌঁছাবে checkpoint-এ। (এই পাতার sql.js database থাকে ব্রাউজারের memory-র একটি কাল্পনিক ফাইলে, তাই আচরণ ফাইলের মতো, যদিও কিছুই ডিস্কে যায় না।) synchronous = 2 মানে FULL, প্রতিটি commit-এ fsync; WAL মোডে অনেকে NORMAL (1) বেছে নেন — বিদ্যুৎ গেলে শেষ কয়েকটি commit হারাতে পারে, কিন্তু database অসংগত হয় না (খুঁটিনাটি SQLite-এর documentation-এ)। page_size = 4096, অর্থাৎ ৪ KB-র page.

দুটি গাছ: ৫০,০০০ সারির টেবিল নিল ২৫২টি page (প্রায় ১ MB). id দিয়ে খোঁজা SEARCH … USING INTEGER PRIMARY KEY (rowid=?) — টেবিলটি নিজেই rowid-এর B-tree. wallet দিয়ে খোঁজা প্রথমে SCAN accounts, index-এর পরে SEARCH … USING COVERING INDEX idx_accounts_wallet (wallet=?): index-এর leaf-এ wallet আর rowid দুটোই আছে, তাই টেবিলের গাছে যেতেই হয়নি। ফল 31337 │ W8157703 │ 59469. শেষ page_count ৪৫৩ — index নিজেই প্রায় ২০০ page-এর আলাদা একটি গাছ, যা প্রতিটি লেখায় হালনাগাদ হয়।

যা এই demo দেখায় না: fsync-এর আসল দাম, crash recovery, অনেক লেখকের ভিড় বা একাধিক যন্ত্র — এটি এক যন্ত্রের, memory-তে চলা SQLite.

৫ · LSM-tree — জমিয়ে রাখা, সাজিয়ে নামানো, পরে মেলানো

ডাকঘরে প্রতিটি চিঠি আসামাত্র গুদামের ঠিক তাকে রাখতে দৌড়ালে কর্মীর দিন শেষ। তার বদলে চিঠি জমে টেবিলের ট্রেতে; ট্রে ভরলে এলাকা ধরে সাজিয়ে একটি বস্তায় বাঁধা হয়, আর রাতে কয়েকটি ছোট বস্তা মিলিয়ে একটি বড় সাজানো বস্তা। চিঠি খুঁজতে হলে আগে ট্রে, তারপর নতুন বস্তা থেকে পুরোনো বস্তা।

১৯৯৬ সালে Patrick O'Neil, Edward Cheng, Dieter Gawlick আর Elizabeth O'Neil-এর প্রবন্ধ “The Log-Structured Merge-Tree” (Acta Informatica) এই কৌশলটি দাঁড় করায়: LSM-tree. মূল চিন্তা — ডিস্কে পুরোনো জায়গায় কখনো বদল নয়; লেখা জমে RAM-এ, বড় সাজানো টুকরো হয়ে ক্রমিকভাবে ডিস্কে নামে, আর পেছনে টুকরোগুলো মিলিয়ে গোছানো হয়। Google-এর Bigtable (২০০৬) memtable আর SSTable নাম দুটি জনপ্রিয় করে; আজ LevelDB, RocksDB, Cassandra, HBase আর ScyllaDB এই পরিবারের।

চারটি অংশ

  • WAL: প্রতিটি লেখা আগে log-এর শেষে (অংশ ৩), যাতে crash-এ RAM-এর ডেটা ফেরানো যায়।
  • Memtable: RAM-এ key-এর ক্রমে সাজানো কাঠামো (LevelDB আর RocksDB-র ডিফল্ট একটি skip list — DSA · treap আর skip list). সীমা ছুঁলে এটি immutable memtable হয়, নতুন খালি memtable লেখা নেয়।
  • SSTable (sorted string table): immutable memtable এক টানে ডিস্কে নামে একটি অপরিবর্তনীয়, সাজানো ফাইল হয়ে — flush; সাথে ছোট একটি index আর একটি Bloom filter.
  • Compaction: পেছনে কয়েকটি SSTable merge sort-এর মতো মিলিয়ে (DSA · merge sort) নতুন SSTable লেখা — প্রতিটি key-এর নতুনতম মান রেখে; তারপর পুরোনো ফাইল মোছা।

পুরোনো ফাইল বদলানোই যায় না, তাহলে delete? একটি বিশেষ লেখা দিয়ে — tombstone, “এই key মৃত” চিহ্ন। পড়ার সময় tombstone আগে মিললে পুরোনো মান উপেক্ষা হয়। Tombstone ফেলা যায় কেবল তখন, যখন compaction নিশ্চিত যে নিচের কোনো স্তরে ওই key-এর পুরোনো কপি নেই — নইলে মুছে ফেলা ডেটা আবার “জেগে” ওঠে। Replica থাকলে আরও সাবধানতা: Cassandra tombstone একটি grace সময় রাখে (লেখার সময়ের ডিফল্ট ১০ দিন), যাতে ফিরে আসা কোনো replica পুরোনো মান আবার না ছড়ায়।

নিচে একটি লেখার পুরো যাত্রা। Memtable-এর সীমা ইচ্ছা করে মাত্র ৪টি key; আসল engine-এ memtable সাধারণত কয়েক দশ MB.

  1. লেখাput(u1, Rahim2)ক্লায়েন্ট
  2. WALappend + fsyncডিস্ক, ক্রমিক
  3. Memtableসাজানো, RAMসীমা ৪টি key
  4. Immutableশুধু পড়ানতুন memtable চালু
  5. Flushনতুন SSTableএক টানে লেখা
  6. Compactionmergeপুরোনো কপি বাদ
Memtable-এর সীমা ৪টি key · ডিস্কে আগে থেকে SSTable A
WAL (ডিস্ক)
Memtable (RAM)
Immutable memtable
SSTable (ডিস্ক)
ফল আর খরচ
  1. লেখা আসে। put(u1, "Rahim2") — u1-এর নতুন মান। Memtable-এ ৩টি key: u2, u5 আর মুছে ফেলা u3-এর tombstone (tomb) চিহ্নে। ডিস্কের SSTable A-তে u1 থেকে u4-এর পুরোনো মান।
  2. WAL-এর শেষে যোগ, তারপর fsync. শুধু ফাইলের শেষ প্রান্তে লেখা, কোনো পুরোনো page খোঁজা নেই। fsync ফেরার পরে লেখাটি টেকসই — এখন বিদ্যুৎ গেলেও লেখাটি WAL থেকে ফেরানো যাবে।
  3. Memtable-এ বসানো। RAM-এর সাজানো কাঠামোয়, মাইক্রোসেকেন্ডে; এরপরই ক্লায়েন্ট OK পায়, আর পরের পড়াতেই নতুন মান দেখা যায়। u1 যোগ হয়ে key ৪টি — সীমা ছুঁয়েছে।
  4. Memtable জমে যায়। ভরা memtable immutable হয়; নতুন খালি memtable সঙ্গে সঙ্গে চালু, তাই পরের লেখা u6=Tania থামে না। এ মুহূর্তে পড়লে দুটি memtable-ই দেখা হয়।
  5. Flush. Immutable memtable এক টানে ডিস্কে নতুন SSTable B হয়, সাথে index আর Bloom filter. লেখাগুলো এখন SSTable-এ টেকসই, তাই WAL-এর সেই অংশ আর লাগে না।
  6. Compaction. পেছনে B আর A মিলে নতুন SSTable C, প্রতিটি key-এর নতুনতম মান নিয়ে। u3-এর tombstone আর পুরোনো u3=Salma দুটোই বাদ, কারণ নিচে আর কোনো পুরোনো ফাইল নেই। ৮টি entry পড়া, ৪টি লেখা — এই বারবার লেখাই write amplification.

পড়ার পথ বনাম লেখার পথ

লেখার পথ ছোট: WAL-এ যোগ আর memtable-এ বসানো; কোনো পুরোনো page পড়তে হয় না। পড়ার পথ লম্বা: memtable, immutable memtable, তারপর SSTable-গুলো নতুন থেকে পুরোনো — প্রথম মিলেই থামা। যে key কোথাও নেই, তার জন্য সব জায়গা খুঁজে তবে “নেই”।

এখানে কাজে আসে প্রতিটি SSTable-এর Bloom filter — একটি ছোট bit array, যা বলে “এই key এই ফাইলে অবশ্যই নেই” বা “হয়তো আছে”; “নেই” বললে তা নিশ্চিত। প্রতি key-এ ১০ bit দিলে ভুলের হার প্রায় ০.৮%: ১০টি SSTable-এ একটি অনুপস্থিত key-এর জন্য ১০টির বদলে গড়ে মাত্র প্রায় ০.০৮টি অকারণ ডিস্ক-পড়া। দাম RAM: ১০০ কোটি key-এ প্রায় ১.২৫ GB. সূত্র আর প্রমাণ আসবে পর্ব ৪৫-এ (Probabilistic data structure — Bloom filter, count-min sketch আর HyperLogLog). Range query-তে Bloom filter সাহায্য করে না।

এবার পুরো কৌশলটি একটি ছোট simulator-এ চালান — stepper-এর মতোই ১০টি লেখা (একটি delete-সহ), প্রতিটি অবস্থা, ৫টি খোঁজ আর amplification-এর হিসাব:

// Tiny LSM-tree: write -> WAL + memtable (RAM) -> sorted runs (SSTables) -> compaction.
const LIMIT = 4, TOMB = null;               // flush when the memtable holds 4 keys
let mem = new Map(), runs = [], io = { user: 0, wal: 0, flush: 0, compact: 0 };  // runs[0] = newest
const sorted = (m) => [...m].sort((a, b) => (a[0] < b[0] ? -1 : 1));
function put(k, v) {                        // v === TOMB means "delete k"
  io.user++; io.wal++; mem.set(k, v);       // 1) append to WAL  2) insert into memtable
  if (mem.size >= LIMIT) { runs.unshift(sorted(mem)); io.flush += mem.size; mem = new Map(); }
}
function get(k) {                           // memtable first, then runs newest -> oldest
  if (mem.has(k)) return [mem.get(k), 1];
  for (let i = 0; i < runs.length; i++) {   // (a real SSTable: index block + binary search)
    const hit = runs[i].find((e) => e[0] === k); if (hit) return [hit[1], i + 2]; }
  return [TOMB, runs.length + 1];
}
function merged() {                         // newest version of every key, tombstones dropped
  const m = new Map(); for (const run of [...runs].reverse()) for (const [k, v] of run) m.set(k, v);
  return sorted(m).filter((e) => e[1] !== TOMB);
}
const compact = () => { runs = [merged()]; io.compact += runs[0].length; };
const txt = (r) => r.map(([k, v]) => k + "=" + (v === TOMB ? "(tomb)" : v)).join(" ");
function show(title) {
  console.log("== " + title + "\n  memtable: " + txt(sorted(mem)));
  runs.forEach((r, i) => console.log("  run " + (i + 1) + ": " + txt(r)));
  const disk = runs.flat().length, live = merged().length, sst = io.flush + io.compact;
  console.log("  on disk " + disk + " entries for " + live + " live keys -> space amp " + (disk / live).toFixed(2));
  for (const k of ["u2", "u1", "u4", "u3", "u9"]) { const [v, n] = get(k);
    console.log("  get " + k + " -> " + (v === TOMB ? "not found" : v) + "  (looked in " + n + ")"); }
  console.log("  writes: user " + io.user + ", WAL " + io.wal + ", SSTable " + sst +
    " -> write amp " + (sst / io.user).toFixed(1) + " (with WAL " + ((sst + io.wal) / io.user).toFixed(1) + ")");
}
"u1=Rahim u2=Karim u3=Salma u4=Nila u2=Karim2 u3 u5=Rafi u1=Rahim2 u6=Tania u2=Karim3".split(" ")
  .forEach((w) => { const [k, v] = w.split("="); put(k, v === undefined ? TOMB : v); });  // "u3" = delete
show("after 10 writes (2 flushes)");
compact(); show("after compaction");

আউটপুট:

== after 10 writes (2 flushes)
  memtable: u2=Karim3 u6=Tania
  run 1: u1=Rahim2 u2=Karim2 u3=(tomb) u5=Rafi
  run 2: u1=Rahim u2=Karim u3=Salma u4=Nila
  on disk 8 entries for 4 live keys -> space amp 2.00
  get u2 -> Karim3  (looked in 1)
  get u1 -> Rahim2  (looked in 2)
  get u4 -> Nila  (looked in 3)
  get u3 -> not found  (looked in 2)
  get u9 -> not found  (looked in 3)
  writes: user 10, WAL 10, SSTable 8 -> write amp 0.8 (with WAL 1.8)
== after compaction
  memtable: u2=Karim3 u6=Tania
  run 1: u1=Rahim2 u2=Karim2 u4=Nila u5=Rafi
  on disk 4 entries for 4 live keys -> space amp 1.00
  get u2 -> Karim3  (looked in 1)
  get u1 -> Rahim2  (looked in 2)
  get u4 -> Nila  (looked in 2)
  get u3 -> not found  (looked in 2)
  get u9 -> not found  (looked in 2)
  writes: user 10, WAL 10, SSTable 12 -> write amp 1.2 (with WAL 2.2)

দুটি flush: প্রথম ৪টি লেখা run 2-এ (পুরোনো), পরের ৪টি run 1-এ (নতুন), যেখানে u3-এর delete বসে আছে (tomb) হয়ে। শেষ দুটি লেখা এখনো memtable-এ — তাই এই মুহূর্তের write amp ০.৮।

Read amplification: u2 মেলে memtable-এই (১ জায়গা), u4-এ লাগে ৩ জায়গা। u3-এর খোঁজ run 1-এর tombstone-এ থামে, run 2-এর পুরোনো Salma থাকা সত্ত্বেও। u9 কোথাও নেই, তাই ৩ জায়গাই খুঁজতে হলো — Bloom filter থাকলে দুটি run প্রায় নিশ্চিতভাবে বাদ যেত।

Compaction-এর পরে: দুটি run মিলে একটি; tombstone আর u3=Salma দুটোই গেল। Space amp ২.০০ থেকে ১.০০, সব খোঁজ বড়জোর ২ জায়গায়। দাম: আরও ৪টি entry নতুন করে লেখা — write amp ১.২, WAL ধরলে ২.২। পর্ব ৬১-এর ক্যাপস্টোনে (distributed key-value store) এই engine-এর ধারণাগুলো আবার কাজে লাগবে।

Compaction-এর দুই প্রধান নীতি

কোন ফাইল কখন কার সাথে মিলবে — এই নীতিই LSM-এর read, write আর space খরচ ঠিক করে।

🧺 Size-tiered

প্রায় সমান আকারের কয়েকটি SSTable (ধরুন ৪টি) মিলে এক ধাপ বড় একটি ফাইল। প্রতিটি byte কম বার নতুন করে লেখা হয় — write amplification কম। কিন্তু একই key অনেক ফাইলে থাকতে পারে (read খরচ বেশি), আর বড় compaction-এর সময় সাময়িকভাবে প্রায় দ্বিগুণ জায়গা লাগতে পারে।

size-tiered · Cassandra-র দীর্ঘদিনের ডিফল্ট

🪜 Leveled

ডিস্ক কয়েকটি স্তরে, প্রতিটি আগেরটির প্রায় ১০ গুণ বড়; L1 থেকে নিচে একই স্তরের ফাইলগুলোর key-পরিসর ওভারল্যাপ করে না। তাই খোঁজে প্রতি স্তরে বড়জোর ১টি ফাইল — read খরচ কম; পুরোনো কপি মোটামুটি ১১%-এর বেশি জমে না (১ + ০.১ + ০.০১ + … ≈ ১.১১) — space কম। দাম: প্রতিটি byte স্তরে স্তরে বারবার লেখা হয়, তাই write amplification প্রায় ১০–৩০ গুণ বলা হয় (কাজ আর সেটিংভেদে আলাদা)।

leveled · LevelDB আর RocksDB-র ডিফল্ট

এখানেই RUM সবচেয়ে স্পষ্ট: শুধু compaction-এর নীতি বদলে খরচ এক দিক থেকে আরেক দিকে সরে, তিনটি একসাথে কমে না। অনেক engine মাঝামাঝি নীতিও দেয় (ওপরের স্তরে size-tiered, নিচে leveled).

Compaction পিছিয়ে পড়লে লেখা থামে। লেখার হার compaction-এর চেয়ে বেশি হলে L0-তে ফাইল জমে, প্রতিটি খোঁজ আরও বেশি ফাইলে যায়, ডিস্ক ভরে আসে। তখন engine লেখা ধীর বা বন্ধ করে — write stall, যা আসলে পর্ব ১৮-এর backpressure; দেখা যায় হঠাৎ লাফানো p99 latency-তে। প্রতিকার: compaction-এর জন্য ডিস্কের ক্ষমতায় headroom, আর L0 ফাইলের সংখ্যা ও বাকি compaction-এর byte মেপে alert.

৬ · B-tree বনাম LSM-tree — কোন কাজে কোনটি

দুটি পরিবার পাশাপাশি দেখুন। বাঁয়ে একই key 42-এর খোঁজ আর বদল B+-tree-তে, ডানে একটি লেখা LSM-tree-তে কোথায় কোথায় যায়:

B+-tree বনাম LSM-tree — একই কাজ, দুই দর্শন

In-place pages (left) vs. RAM buffer + immutable sorted files + background merge (right)

B+-tree বনাম LSM-tree: জায়গায় বদল বনাম জমিয়ে পরে মেলানো B+-tree জায়গায় বদল (in-place) WAL · শুধু শেষে যোগ [30 │ 60] [10│20][40│50][70│80] 31 3540 4250 55 42-এর leaf page-টিই জায়গায় নতুন করে লেখা নতুন page ভরলে split সবুজ: 42 খোঁজার পথ পড়া: h-টি page, স্থির লেখা: WAL + পুরো page random I/O, split LSM-tree জমিয়ে রাখা, পরে মেলানো WAL · শুধু শেষে যোগ RAM memtable(সাজানো) immutablememtable flush compaction L0L1L2 SSTSST SSTSSTSST প্রতি স্তর ~১০ গুণ বড় প্রতি SST-এ Bloom filter লেখা: sequential, দ্রুত পড়া: কয়েক জায়গায় খোঁজ পেছনে compaction-এর খরচ
ছবি ১৯.২ — দুই engine-ই প্রতিটি লেখা আগে WAL-এ যোগ করে। পার্থক্য তার পরে: B+-tree একটি নির্দিষ্ট page খুঁজে সেখানেই বদলায়; LSM-tree কখনো পুরোনো ফাইল বদলায় না, নতুন ফাইল লেখে আর পরে মেলায়।
প্রশ্নB+-treeLSM-tree
একটি লেখার খরচWAL + একটি page-এর random বদলWAL + RAM-এ বসানো; পরে ক্রমিক flush
Write amplificationpage ÷ সারির আকার; buffer pool জমালে কমsize-tiered কম, leveled কয়েক দশ
একটি key পড়া৩–৪ page, প্রায় সবসময় একইmemtable + কয়েকটি স্তর; Bloom filter সাহায্য করে
Range scanleaf-এর শিকল ধরে, চমৎকারকয়েকটি স্তর মিলিয়ে পড়া
জায়গাpage প্রায় ৬৫–৭০% ভরাঘন সাজানো ফাইল, ভালো compression
Latency-র স্থিরতাসাধারণত অনুমানযোগ্যcompaction-এর চাপে p99 লাফাতে পারে
উদাহরণ (লেখার সময়ে)PostgreSQL, MySQL InnoDB, SQLite, SQL Server; MongoDB-র WiredTiger (ডিফল্টে B-tree)RocksDB, LevelDB, Cassandra, HBase, ScyllaDB; MyRocks
  • লেখা-প্রধান, প্রায় শুধু যোগ (location ping, log, বার্তা, metrics) — LSM.
  • পড়া-প্রধান, লেনদেন আর range query (হিসাব, অর্ডার, পণ্যের তালিকা) — B-tree-এর relational database.
  • মাঝামাঝি: আধুনিক SSD-তে দুটোই যথেষ্ট ভালো; সিদ্ধান্ত আসে data model, transaction আর ops-এর অভিজ্ঞতা থেকে (পর্ব ২০), আর শেষ কথা বলে নিজের ধাঁচের benchmark.

তৃতীয় পথ — log আর memory-তে hash index

সবচেয়ে সরল টেকসই key-value store: ডিস্কে একটি append-only log, আর RAM-এ একটি hash table, যা মনে রাখে প্রতিটি key-এর সর্বশেষ মান log-এর কোন offset-এ। লেখা = log-এর শেষে যোগ + hash table হালনাগাদ; পড়া = offset ধরে ঠিক ১টি ডিস্ক-পড়া; পুরোনো segment পেছনে মিলিয়ে ছোট করা হয়। এই নকশার পরিচিত উদাহরণ Bitcask, যা ২০১০ সালের দিকে Riak database-এর জন্য বর্ণিত হয় (DSA · hash table).

এই hash index-এর সীমা দুটি। সব key RAM-এ ধরতে হবে: key গড়ে ২০ byte আর entry-প্রতি ঠিকানা-তথ্য ২০ byte ধরলে ১০ কোটি key-এ প্রায় ৪ GB RAM. আর hash ক্রম রাখে না, তাই range query চলে না। কম key, অনেক লেখা, শুধু point lookup — সেখানে এটি দারুণ সরল আর দ্রুত।

৭ · Row বনাম column store — OLTP আর OLAP

এতক্ষণের সব engine row store: একটি সারির সব কলাম একই page-এ পাশাপাশি — একটি অর্ডার পুরোটা পড়া-লেখার, অর্থাৎ OLTP-এর জন্য আদর্শ। কিন্তু “দুই বছরে জেলাভিত্তিক মোট বিক্রি”-র মতো OLAP প্রশ্নে দরকার কোটি সারির দু-তিনটি কলাম, অথচ row store-কে সব কলাম পড়তে হয়।

ঈদের আগে একটি সমিতি ২০০ পরিবারকে উপহারের প্যাকেট দেয় — প্রতিটিতে চাল, ডাল, তেল, চিনি, সেমাই। একটি পরিবারকে দিতে প্যাকেট আদর্শ, এটি row store; কিন্তু “মোট কত কেজি চিনি গেল” জানতে ২০০টি প্যাকেটই খুলতে হয়। পাইকারি আড়তে মাল থাকে জাতভেদে — চালের স্তূপ এক জায়গায়, চিনির বস্তা আরেক জায়গায়, এটি column store; চিনির হিসাব এক জায়গায় দাঁড়িয়েই হয়, কিন্তু একটি প্যাকেট বানাতে পাঁচ জায়গায় ঘুরতে হয়।

Column store প্রতিটি কলাম আলাদা ধারায় রাখে। লাভ তিনটি: শুধু দরকারি কলাম পড়া; একই ধরনের মান পাশাপাশি থাকায় চমৎকার compression — dictionary encoding (লম্বা মানের বদলে ছোট সংখ্যা), run-length encoding (“মান × কতবার”), bit-packing (যত bit লাগে ঠিক ততটুকু); আর vectorised execution — হাজারখানেক মানের দল ধরে একটি ছোট loop, যা CPU-র cache আর SIMD-এর (একটি নির্দেশে কয়েকটি মান) পক্ষে আদর্শ। নিচে একই প্রশ্ন দুই সাজানোয়:

sales — ৪টি সারি, ৫টি কলামপ্রশ্নের দরকার ২টি কলাম
id │ date       │ district │ amount │ channel
 1 │ 2025-01-03 │ Dhaka    │    500 │ app
 2 │ 2025-01-03 │ Khulna   │   1200 │ web
 3 │ 2025-01-04 │ Dhaka    │    300 │ app
 4 │ 2025-01-04 │ Sylhet   │    900 │ app

SELECT district, SUM(amount)
FROM sales GROUP BY district;
Row store — page-এ পুরো সারিসব byte পড়তে হয়
page 1: [1, 2025-01-03, Dhaka, 500, app]
        [2, 2025-01-03, Khulna, 1200, web]
page 2: [3, 2025-01-04, Dhaka, 300, app]
        [4, 2025-01-04, Sylhet, 900, app]
↑ উজ্জ্বল অংশটুকুই দরকার, বাকি সব পড়তে হলো
Column store — প্রতিটি কলাম আলাদা ধারায়শুধু ২টি ধারা পড়া
district: [Dhaka │ Khulna │ Dhaka │ Sylhet]
amount:   [500 │ 1200 │ 300 │ 900]
id:       [1 │ 2 │ 3 │ 4]           (বাদ)
date:     [2025-01-03 │ … ]         (বাদ)
channel:  [app │ web │ app │ app]   (বাদ)
সংকুচিত columndictionary + bit-packing
অভিধান:   0 = Dhaka, 1 = Khulna, 2 = Sylhet
district: [0 │ 1 │ 0 │ 2]   প্রতিটি 2 bit
amount:   [500 │ 1200 │ 300 │ 900]
          প্রতিটি 11 bit (সর্বোচ্চ 2047)

loop: sum[code] += amount
(একসাথে হাজারখানেক মানের দলে)
ধরা যাক প্রতিটি কাঁচা মান ৮ byte
পড়া byte
কাজে লাগল
ফল
  1. প্রশ্ন আর টেবিল। sales-এ ৪টি সারি, ৫টি কলাম; প্রতিটি মান ৮ byte ধরলে মোট ১৬০ byte. প্রশ্নের দরকার শুধু district আর amount — ৫টির ২টি কলাম।
  2. Row store পড়ে। Page-এ পুরো সারি পাশাপাশি, তাই দুটি কলাম চাইলেও দুটি page-এর সব byte আসে — ১৬০ byte, কাজের মাত্র ৬৪.
  3. Row store-এর ফল। Dhaka ৫০০ + ৩০০ = ৮০০, Khulna ১২০০, Sylhet ৯০০. কাজে লাগল ৬৪ ÷ ১৬০ = ৪০ শতাংশ; ৫০ কলামের টেবিলে ২টি কলাম হলে মাত্র ৪ শতাংশ।
  4. Column store পড়ে। প্রতিটি কলাম আলাদা ধারায়; পড়া হলো শুধু district আর amount — ৬৪ byte. id, date আর channel ছোঁয়াই হয়নি।
  5. Compression. District-কে ছোট সংখ্যায় বদলানো (dictionary: Dhaka = 0, Khulna = 1, Sylhet = 2) — প্রতিটি ২ bit; amount-এর সর্বোচ্চ ১২০০ ধরতে ১১ bit (bit-packing). ৪টি সারিতে মোট ৫২ bit, প্রায় ৭ byte, সাথে ছোট্ট অভিধান।
  6. Vectorised scan. হাজারখানেক মানের দল নিয়ে একটি ছোট loop: sum[code] += amount — CPU-র cache-এর পক্ষে আদর্শ। ফল একই: Dhaka ৮০০, Khulna ১২০০, Sylhet ৯০০. দাম লেখায়: নতুন সারি মানে ৫টি আলাদা ধারায় লেখা।

এবার বড় পরিসরে হিসাব করুন: ১০ লাখ সারি, ৫০টি কলাম, প্রশ্নটি ছোঁয় মাত্র ২টি — district আর amount. প্রতিটি অনুমান কোডের মাথায় লেখা; বদলে চালিয়ে দেখুন:

// SELECT district, SUM(amount) FROM sales GROUP BY district  -- bytes read: row vs column store
// Assumptions: 1,000,000 rows, 50 columns, 8 bytes per raw value, 64 districts, amount 50..100,000
// taka; an RLE run = 1-byte code + 4-byte count. Real engines add headers and smarter codecs.
function rng(seed) {                                   // mulberry32: seeded, repeatable
  return () => { seed = (seed + 0x6D2B79F5) | 0; let t = Math.imul(seed ^ (seed >>> 15), 1 | seed);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t; return ((t ^ (t >>> 14)) >>> 0) / 4294967296; };
}
const N = 1e6, COLS = 50, W = 8, rand = rng(19);
const district = new Uint8Array(N), amount = new Uint32Array(N);
for (let i = 0; i < N; i++) { district[i] = Math.floor(rand() * 64); amount[i] = 50 + Math.floor(rand() * 99951); }
const bits = (max) => Math.ceil(Math.log2(max + 1));    // bits needed for values 0..max
const runs = (a) => { let r = 1; for (let i = 1; i < a.length; i++) r += a[i] !== a[i - 1]; return r; };
const dBits = bits(63), aBytes = (N * bits(100000)) / 8, sortedD = district.slice().sort();
const plans = [
  ["row store: all 50 columns", N * COLS * W],
  ["column store: 2 raw columns", 2 * N * W],
  ["+ dictionary district (" + dBits + " bits)", (N * dBits) / 8 + N * W],
  ["+ bit-packed amount (" + bits(100000) + " bits)", (N * dBits) / 8 + aBytes],
  ["  RLE district instead, unsorted", runs(district) * 5 + aBytes],
  ["  RLE district instead, sorted", runs(sortedD) * 5 + aBytes],
];
console.log("plan".padEnd(36) + "bytes read".padStart(12) + "share".padStart(8) + "smaller".padStart(9));
for (const [name, b] of plans)
  console.log(name.padEnd(36) + ((b / 1e6).toFixed(3) + " MB").padStart(12) +
    ((100 * b / plans[0][1]).toFixed(2) + "%").padStart(8) + ((plans[0][1] / b).toFixed(0) + "x").padStart(9));
console.log("RLE runs: unsorted " + runs(district) + ", sorted " + runs(sortedD));
const sum = new Float64Array(64);                       // the scan itself: one tight loop, 2 arrays
for (let i = 0; i < N; i++) sum[district[i]] += amount[i];
console.log("district 0 total " + sum[0].toLocaleString("en-US") + ", all " + sum.reduce((a, b) => a + b, 0).toLocaleString("en-US"));

আউটপুট:

plan                                  bytes read   share  smaller
row store: all 50 columns             400.000 MB 100.00%       1x
column store: 2 raw columns            16.000 MB   4.00%      25x
+ dictionary district (6 bits)          8.750 MB   2.19%      46x
+ bit-packed amount (17 bits)           2.875 MB   0.72%     139x
  RLE district instead, unsorted        7.047 MB   1.76%      57x
  RLE district instead, sorted          2.125 MB   0.53%     188x
RLE runs: unsorted 984457, sorted 64
district 0 total 778,971,071, all 49,985,788,669

শুধু কলাম বাছাইয়েই ২৫ গুণ: ৫০টির ২টি কলাম মানে ৪০০ MB-র বদলে ১৬ MB, অর্থাৎ ৪ শতাংশ। Dictionary-তে ৬৪টি জেলা ৬ bit-এ (২৬ = ৬৪), আর amount-এর সর্বোচ্চ ১,০০,০০০ ধরতে ১৭ bit — মোট ২.৮৭৫ MB, row store-এর চেয়ে ১৩৯ গুণ কম।

RLE-এর শর্ত: এলোমেলো ক্রমে ৯,৮৪,৪৫৭টি run (প্রত্যাশা ১০ লাখ × ৬৩/৬৪ ≈ ৯,৮৪,৩৭৫), তাই RLE dictionary-র চেয়েও খারাপ; জেলা ধরে সাজানো থাকলে মাত্র ৬৪টি run — মোট ২.১২৫ MB, ১৮৮ গুণ কম। তাই column store-এ sort order একটি বড় নকশা-সিদ্ধান্ত। শেষ লাইনের মোট (গড় প্রায় ৫০,০০০ টাকা) দেখায় scan-টি সত্যিই চলেছে।

বাস্তবের column store আরও কৌশল যোগ করে: প্রতিটি block-এর সর্বনিম্ন-সর্বোচ্চ মান রেখে অপ্রাসঙ্গিক block পুরো বাদ (zone map), আর সংকুচিত ডেটার ওপরেই হিসাব। দাম লেখায়: নতুন সারি মানে প্রতিটি কলামের ধারায় লেখা, আর সংকুচিত block বদলানো দামি — তাই লেখা জমিয়ে দলে দলে নেওয়া হয়, অনেকটা LSM-এর মতো। ClickHouse, DuckDB, Parquet ফাইল-ফরম্যাট আর বড় cloud warehouse-গুলো column-ভিত্তিক। Data warehouse-এর star schema আর ETL পাবেন DBMS · Data warehouse — OLAP, star schema আর ETL-এ; OLTP থেকে warehouse-এ ডেটার ধারা আসবে পর্ব ৩৩ আর ৩৬-এ।

৮ · In-memory database আর durability-র বিকল্প

সব ডেটা RAM-এ ধরে গেলে page, buffer pool আর ডিস্ক-পড়া অপ্রয়োজনীয় মনে হয়। In-memory database (যেমন Redis, বা গবেষণা থেকে জন্ম নেওয়া VoltDB) এভাবেই চলে: hash table, skip list-এর মতো RAM-বান্ধব কাঠামো, অনুরোধ মাইক্রোসেকেন্ডের ঘরে। কিন্তু RAM বিদ্যুৎ গেলে মুছে যায়, তাই প্রশ্ন থাকে: কতটুকু হারানো চলে, আর ফিরতে কত সময় লাগে?

বিকল্পকীভাবেCrash-এ কী হারায়দাম
কিছুই না (শুধু cache)ডেটা আসল উৎস থেকে আবার আনা যায়সব, তবে উৎসে আছেrestart-এর পরে ঠান্ডা cache, উৎসে চাপ (পর্ব ২৯)
Snapshotকিছুক্ষণ পরপর পুরো ডেটার ছবি ফাইলেশেষ snapshot-এর পরের সববড় ডেটায় ভারী; fork করলে copy-on-write-এ RAM-এর চাপ বাড়ে
Log (AOF-ধাঁচ)প্রতিটি লেখা append-only ফাইলে, fsync নীতি বেছেপ্রতি লেখায় fsync হলে প্রায় কিছুই না; প্রতি সেকেন্ডে হলে মোটামুটি শেষ এক-দুই সেকেন্ডলেখা ধীর; log বড় হয়, মাঝে মাঝে নতুন করে লিখে ছোট করা লাগে
Snapshot + logsnapshot থেকে শুরু, তারপর log-এর বাকিটাlog-এর fsync নীতির সমানসবচেয়ে প্রচলিত আপস — WAL + checkpoint-এরই রূপ
অন্য যন্ত্রে replicaলেখা আরেকটি যন্ত্রে পৌঁছালে তবে OKদুই যন্ত্র একসাথে না পড়লে কিছু নাnetwork latency; একই rack বা বিদ্যুৎ লাইনে একসাথে পড়ার ঝুঁকি (পর্ব ২২, ৫১)

Recovery-র সময়ও হিসাব করুন: ৫০ GB-র snapshot সেকেন্ডে ৫০০ MB হারে পড়তে লাগে ৫০ × ১০০০ ÷ ৫০০ = ১০০ সেকেন্ড, তারপর log-এর বাকিটা — এ সময় সার্ভিস হয়তো অনুরোধ নিতে পারে না। Redis-এর RDB snapshot আর AOF (appendfsync-এর always, everysec, no) এই আপসগুলোর বাস্তব রূপ; ডিফল্ট সংস্করণভেদে আলাদা, বিস্তারিত DBMS · Redis — in-memory key-value আর cache-এ।

আরও গভীরে: WiscKey (FAST ২০১৬) বড় value আলাদা log-এ রেখে LSM-এ শুধু key আর ঠিকানা রাখে, SSD-র সস্তা random পড়া কাজে লাগিয়ে write amplification কমায়। Monkey (SIGMOD ২০১৭) দেখায়, Bloom filter-এর RAM সব স্তরে সমান না দিয়ে স্তরভেদে ভাগ করলে একই RAM-এ পড়ার খরচ কমে। B-tree-এর দিকেও নতুন রূপ এসেছে — লেখা internal node-এ জমিয়ে নামানো Bε-tree, আর lock ছাড়া চলা Bw-tree (ICDE ২০১৩)।

পরীক্ষা ও ইন্টারভিউতে — বিশ্ববিদ্যালয়, চাকরি বা system design ইন্টারভিউর পরীক্ষায় এই ধরনের প্রশ্ন আসতে পারে:

  • B-tree আর LSM-tree-এর মূল পার্থক্য এক লাইনে? B-tree পুরোনো page জায়গায় বদলায় (পড়ায় ভালো, অনুমানযোগ্য); LSM নতুন সাজানো ফাইল লেখে আর পরে মেলায় (লেখায় ভালো, পেছনে compaction).
  • Fan-out ৫০০-র ৩ স্তরের B+-tree-তে সর্বোচ্চ কত key? ৫০০৩ = ১২,৫০,০০,০০০ (১২.৫ কোটি); ৪ স্তরে ৬,২৫০ কোটি।
  • WAL কেন দ্রুত, অথচ নিরাপদ? শুধু ফাইলের শেষে ক্রমিক লেখা; commit-এর আগে log fsync, তাই crash-এর পরে redo-undo করা যায়।
  • Analytics-এ column store কেন দ্রুত? শুধু দরকারি কলাম (৫০টির ২টি = ৪%), ভালো compression, আর vectorised loop.

৯ · AI-এর যুগে storage engine বাছাই যাচাই

“লেখা অনেক, কোন database নেব?” — AI প্রায়ই এক নিঃশ্বাসে বলে “Cassandra নিন, পাশে ClickHouse, সামনে Redis, মাঝে Kafka.” নামগুলো ভুল নয়, কিন্তু মূল প্রশ্ন বাদ পড়ে: পড়ার ধাঁচ, delete, compaction-এর জায়গা, crash-এ কী হারায়। সাধারণ ভুলগুলো:

AI-এর ভুলকেন ভুলকীভাবে ধরবেন
“লেখা বেশি, তাই LSM” — আর কিছু না দেখেRange query, অনুপস্থিত key, tombstone আর compaction-এর ডিস্ক-খরচ বাদRead:write ratio, p99 পড়া, delete-এর ধাঁচ আর ডিস্কের headroom জিজ্ঞেস করুন
ভুল অঙ্ক — log₂ ধরে B-tree-এর উচ্চতা, MB আর MiB গুলিয়ে ফেলাFan-out কয়েকশো, ২ নয়; ১,০২৪ আর ১,০০০-এর ফারাক বড় সংখ্যায় জমেfh ≥ N নিজে কষুন; অংশ ৪-এর হিসাবযন্ত্রে মেলান
পণ্যের ডিফল্ট আর সীমা বানিয়ে বলা — memtable-এর আকার, page size, fsync নীতিসংস্করণ আর setup-ভেদে আলাদাঅফিসিয়াল documentation, সংস্করণ লিখে প্রশ্ন
“fsync বন্ধ করুন, replication তো আছেই”একসাথে পড়ে যাওয়া (একই বিদ্যুৎ, একই bug) ভাবা হয়নি; ACID-এর durability আর replica-র কপি আলাদা স্তরFailure-mode পর্যালোচনা: কোন ঘটনায় কোন commit হারায়?
সবকিছুর জন্য column store, বা সবকিছুর জন্য একটি OLTP databaseএক-এক সারির লেখায় column store দামি; কোটি সারির scan-এ row store ধীরপ্রতিটি query-র ধাঁচ আলাদা করে লিখুন
অতিরিক্ত নকশা — ২০ লাখ সারির জন্য তিনটি databaseএকটি relational database আর ঠিক index-এই কাজ হয়পর্ব ৩-এর হিসাব: আকার, QPS আর বৃদ্ধি

AI-কে সংখ্যা, ধাঁচ আর ব্যর্থতার প্রশ্ন দিয়ে বাঁধুন, যাতে উত্তর যাচাই করা যায়:

তুমি একজন অভিজ্ঞ database engineer. একটি কাল্পনিক ride-sharing অ্যাপে চালকদের location ping আসে সেকেন্ডে প্রায় ৫০,০০০টি (প্রতিটি ~১০০ byte), ৭ দিন রাখতে হবে; পড়া মূলত "এই চালকের শেষ ১০ মিনিট"। পাশে একটি পেমেন্ট ledger: লেখা কম, প্রতিটি commit টেকসই হতে হবে, statement-এ তারিখের range query লাগে। ১. দুই কাজে B-tree না LSM-ভিত্তিক storage — কারণ read, write আর space amplification-এর ভাষায় বলো। ২. দিনে কত GB লেখা আসবে, আর write amplification ১০ ধরলে ডিস্কে কত — প্রতিটি ধাপ দেখাও। ৩. ৭ দিনের পুরোনো ডেটা কীভাবে মুছবে (tombstone, TTL, না পুরো partition বাদ), আর compaction-এ তার প্রভাব কী? ৪. Crash, ডিস্ক ভরা আর compaction পিছিয়ে পড়ায় কী ঘটবে, কোন metric দেখে আগে টের পাব? ৫. পণ্যের ডিফল্ট বা সীমা বললে সংস্করণ আর সূত্র দাও; না জানলে বলো "জানি না"। নতুন tool-এর আগে সবচেয়ে সরল সমাধানটি বলো।

প্রশ্ন ২-এর হিসাব নিজে করে রাখুন: ৫০,০০০ × ১০০ byte = সেকেন্ডে ৫ MB; দিনে ৫ × ৮৬,৪০০ = ৪,৩২,০০০ MB, অর্থাৎ ৪৩২ GB. Write amplification ১০ হলে ডিস্কে দিনে প্রায় ৪.৩২ TB — SSD-র আয়ুর হিসাবেও লাগবে। AI-এর সংখ্যা আলাদা হলে ভুলটি খুঁজুন।

AI-এর storage-পরিকল্পনা হাতে পেলে — যাচাইয়ের চেকলিস্ট

AI দেয় দ্রুত খসড়া, যাচাইয়ের দায় আপনার।
অঙ্ক নিজে মেলান, documentation দেখুন, নিজের ডেটায় চালিয়ে দেখুন।

ভাবনার প্রশ্ন

আগে নিজে ভাবুন, তারপর “→ উত্তর” চাপুন। পণ্যের নাম নয়, খরচের হিসাব আর ইতিহাসই আসল শিক্ষা।

প্র ০১B-tree পঞ্চাশ বছরের বেশি পুরোনো, তবু প্রায় সব relational database-এর ভিত্তি। তাহলে ১৯৯৬ সালে LSM-tree-এর দরকার পড়ল কেন, আর পরে এটি এত ছড়াল কেন?

Rudolf Bayer আর Edward McCreight Boeing-এর গবেষণাগারে B-tree উদ্ভাবন করেন; প্রবন্ধটি ১৯৭২ সালে প্রকাশিত হয়। ১৯৭৯ সালে Douglas Comer-এর সমীক্ষার নামই ছিল “The Ubiquitous B-Tree” — ততদিনে এটি সর্বত্র। টিকে থাকার কারণ গভীর: একটি node একটি ডিস্ক-block, তাই প্রতিটি স্তর একটি I/O; balance-এর নিয়মে worst case-ও log; একই কাঠামো সমতা, range, ORDER BY আর prefix খোঁজ সব সামলায়। পরের দশকগুলোতে concurrency (১৯৮১ সালের Lehman–Yao-র B-link tree) আর recovery-র (ARIES) পরিণত কৌশল এর চারপাশে গড়ে ওঠে।

কিন্তু O'Neil ও সহকর্মীরা একটি নির্দিষ্ট কাজ দেখছিলেন: ব্যাংকের লেনদেনের ইতিহাসের মতো টেবিল, যেখানে প্রতি সেকেন্ডে বিপুল insert, আর প্রতিটি insert-এ index-এর একটি এলোমেলো leaf বদলাতে হয় — মানে প্রতিটি লেখায় ডিস্কের বাহুর একটি দামি নড়াচড়া। তাঁদের হিসাবের কেন্দ্রে ছিল টাকা: ডিস্কের বাহুর সময় দামি, RAM তুলনায় সস্তা হচ্ছে। সমাধান — index-এর বদলগুলো RAM-এ জমিয়ে, পরে বড় ক্রমিক merge-এ ডিস্কে নামানো, যাতে একটি বাহুর নড়াচড়ায় হাজারো লেখা যায়।

ধারণাটি জনপ্রিয় হয় এক দশক পরে, যখন কাজের ধাঁচ বদলাল। Web-এর log, বার্তা, click, sensor — সবই লেখা-প্রধান। Google-এর Bigtable (২০০৬) memtable আর SSTable নিয়ে এলো; Facebook-এর তৈরি Cassandra ২০০৮ সালে open source হলো; Google ২০১১ সালে LevelDB প্রকাশ করল, আর Facebook তার ভিত্তিতে RocksDB বানাল, যা আজ বহু database-এর ভেতরের engine.

তবু B-tree হারিয়ে যায়নি — পড়া-প্রধান লেনদেনে তার অনুমানযোগ্যতা এখনো মূল্যবান। আর মাঝামাঝি নকশাও এসেছে, যেমন লেখা internal node-এ জমিয়ে নামানো Bε-tree.

মূল উপলব্ধি: data structure-এর “সেরা” হওয়া নির্ভর করে যন্ত্রের দাম আর কাজের ধাঁচের ওপর। দুটো বদলালে হিসাবও নতুন করে কষতে হয় — বয়স দিয়ে নয়, RUM-এর খরচ দিয়ে বিচার করুন।

প্র ০২২০১৬ সালের একটি বহুল আলোচিত engineering blog PostgreSQL ছেড়ে MySQL-এ যাওয়ার কারণ হিসেবে write amplification দেখিয়েছিল। এই বিতর্ক index-নকশা সম্পর্কে কী শেখায়?

Uber-এর প্রকাশিত সেই লেখার (“Why Uber Engineering Switched from Postgres to MySQL”) একটি মূল যুক্তি ছিল এরকম। PostgreSQL-এ একটি সারি update করলে MVCC-র কারণে সারির একটি নতুন সংস্করণ লেখা হয়, প্রায়ই heap-এর নতুন জায়গায়। আর PostgreSQL-এর প্রতিটি index সারির ভৌত ঠিকানা রাখে। তাই নতুন জায়গা মানে টেবিলের প্রতিটি index-এ নতুন entry — বদলানো কলামে index না থাকলেও। ১০টি index-ওয়ালা টেবিলে একটি ছোট update হয়ে যেতে পারে ১১টি লেখা, প্রতিটির WAL, আর সেই WAL replica-তে পাঠানোর bandwidth.

InnoDB-র নকশা ভিন্ন: সারি থাকে primary key-এর clustered B+-tree-এর leaf-এ, আর secondary index রাখে primary key, ভৌত ঠিকানা নয়। তাই index-হীন কলাম বদলালে secondary index ছুঁতেই হয় না — write amplification কম। দাম অন্য দিকে: secondary index দিয়ে খুঁজলে পাওয়া যায় primary key, তারপর primary key-এর গাছে আবার নামতে হয় — প্রতিটি খোঁজে বাড়তি read amplification.

PostgreSQL সম্প্রদায় পাল্টা কয়েকটি কথা তুলে ধরে। ৮.৩ সংস্করণ (২০০৮) থেকে HOT (heap-only tuple) update আছে: কোনো indexed কলাম না বদলালে আর নতুন সংস্করণ একই page-এ ধরলে index-গুলো ছুঁতে হয় না। Page-এ ফাঁকা জায়গা রাখার fillfactor আর অপ্রয়োজনীয় index বাদ দেওয়া এই সুযোগ বাড়ায়। অর্থাৎ সমস্যার একটি বড় অংশ schema আর index-নকশা দিয়ে কমানো যেত।

কোন পক্ষ ঠিক, তা নির্ভর করে নির্দিষ্ট workload-এর ওপর। শিক্ষাটি সর্বজনীন: প্রতিটি secondary index প্রতিটি লেখায় একটি কর। আর “index কী রাখবে — ভৌত ঠিকানা না primary key” প্রশ্নটি নিজেই RUM-এর আপস: একটিতে লেখা সস্তা, অন্যটিতে পড়া।

মূল উপলব্ধি: engine বদলানোর আগে মাপুন কোন খরচটি সত্যিই ব্যথা দিচ্ছে, আর সেটি engine-এর মৌলিক নকশা থেকে আসছে, না আপনার schema থেকে। অনেক সময় একটি অপ্রয়োজনীয় index মুছে দেওয়াই সবচেয়ে সস্তা migration.

প্র ০৩SSD-তে random পড়া প্রায় sequential-এর মতোই সস্তা। তাহলে LSM-tree-এর মূল যুক্তি — ডিস্কের বাহুর নড়াচড়া কমানো — কি এখন অচল?

যুক্তিটি বদলেছে, কিন্তু মরেনি। HDD-র যুগে LSM-এর জয় ছিল seek এড়ানো। SSD-তে seek নেই, random পড়া কয়েক দশ µs, আর অনেক অনুরোধ একসাথে চলে — তাই B-tree-এর পড়া SSD-তে দারুণ চলে। কিন্তু SSD-র লেখার দিকে নতুন সমস্যা। Flash-এ পুরোনো জায়গায় সরাসরি নতুন করে লেখা যায় না; লেখা হয় ছোট page-এ, কিন্তু মোছা যায় শুধু অনেক page মিলে বড় erase block. ছোট ছোট random বদলে firmware-কে বারবার বৈধ ডেটা সরিয়ে block খালি করতে হয় — যন্ত্রের ভেতরের write amplification. আর flash-এর লেখার আয়ু সীমিত।

B-tree-এর জায়গায় বদল ঠিক এই ছোট random লেখা তৈরি করে। LSM লেখে বড়, অপরিবর্তনীয় ফাইল, আর মোছে পুরো ফাইল — firmware-এর জন্য সহজ। তার ওপর LSM-এর ফাইল ঘন আর সাজানো, তাই compression ভালো কাজ করে; অর্ধেক-ফাঁকা page-এর অপচয় নেই। Facebook-এর প্রকাশিত MyRocks গবেষণা (VLDB ২০২০) InnoDB-র তুলনায় উল্লেখযোগ্য কম জায়গা আর কম লেখার কথা জানায় — যেখানে খরচের বড় অংশ SSD-র জায়গা আর আয়ু, সেখানে এটাই নির্ণায়ক।

কিন্তু LSM-এর নিজের write amplification (leveled-এ কয়েক দশ গুণ) flash-এর আয়ুও খায়। ২০১৬ সালের WiscKey গবেষণা তাই একটি মাঝামাঝি পথ দেখায়: বড় value আলাদা একটি log-এ, LSM-এ শুধু key আর ঠিকানা। Compaction-এ তখন শুধু ছোট key ঘোরে, আর value পড়তে SSD-র সস্তা random পড়া কাজে লাগে — HDD-তে যা অসম্ভব ছিল।

অর্থাৎ SSD যুগের প্রশ্ন “seek কত” নয়; প্রশ্ন হলো কত byte লেখা হচ্ছে, কত জায়গা লাগছে, আর লেখা কতটা বড় আর ক্রমিক।

মূল উপলব্ধি: যন্ত্র বদলালে কোন amplification দামি, তাও বদলায়। পুরোনো cost model মুখস্থ না রেখে প্রতিটি যুগে নতুন করে কষুন — কখনো read, কখনো write, কখনো space সবচেয়ে দামি।

প্র ০৪Analytics-এ column store কয়েকশো গুণ পর্যন্ত দ্রুত হতে পারে। এটি কি শুধু কম byte পড়ার ফল, নাকি আরও কিছু আছে? আর তাহলে সব database column store নয় কেন?

কম byte পড়া প্রথম কারণ, একমাত্র নয়। অংশ ৭-এর হিসাবে কলাম বাছাইয়েই ২৫ গুণ, compression মিলিয়ে ১৩৯ থেকে ১৮৮ গুণ। কিন্তু আরও তিনটি স্তর আছে।

  • CPU-র দক্ষতা: ২০০৫ সালে Peter Boncz ও সহকর্মীরা (MonetDB/X100, CIDR) দেখান, প্রচলিত engine একটি একটি সারি ধরে কাজ করতে গিয়ে প্রতিটি মানে ফাংশন-ডাকার খরচ দেয়। হাজারখানেক মানের দলে (vector) কাজ করলে সেই খরচ ভাগ হয়ে যায়, আর CPU-র cache ও SIMD পুরো কাজে লাগে।
  • না পড়ার কৌশল: প্রতিটি block-এর সর্বনিম্ন-সর্বোচ্চ মান (zone map) দেখে অপ্রাসঙ্গিক block পুরো বাদ, বিশেষত টেবিল তারিখের ক্রমে সাজানো থাকলে।
  • সংকুচিত ডেটায় সরাসরি কাজ: dictionary-র সংখ্যায় তুলনা আর group করা, আর শুধু দরকারি সারির জন্য শেষে আসল মান আনা।

ইতিহাসও শিক্ষণীয়। ১৯৯০-এর দশকে Sybase IQ ছিল; ২০০৫ সালে Michael Stonebraker ও সহকর্মীদের C-Store গবেষণা (পরে Vertica) ধারণাটিকে নতুন করে জনপ্রিয় করে। একই বছর Stonebraker আর Uğur Çetintemel-এর প্রবন্ধের নামই ছিল “One Size Fits All: An Idea Whose Time Has Come and Gone” — এক engine দিয়ে সব কাজের দিন শেষ।

সব database column store নয়, কারণ OLTP-র কাজ উল্টো: একটি অর্ডার ঢোকানো মানে ৫০টি কলামের ৫০টি ধারায় লেখা, আর সংকুচিত block বদলানো দামি। তাই বাস্তবে দুই জগৎ আলাদা কপিতে চলে — লেনদেন row store-এ, আর তার বদলের ধারা (পর্ব ৩৩) গিয়ে পৌঁছায় column-ভিত্তিক warehouse-এ। দুটি একসাথে দেওয়ার চেষ্টাকে বলে HTAP; সেখানেও ভেতরে সাধারণত দুই রকম সাজানো থাকে।

মূল উপলব্ধি: ডেটার সাজানো হওয়া উচিত প্রশ্নের ধাঁচ মেনে। একই ডেটার দুটি সাজানো কপি রাখা অপচয় নয়, যদি দুই ধরনের প্রশ্ন সত্যিই আসে — শুধু কপি মিলিয়ে রাখার দায়টি নকশায় লিখে নিন।

প্র ০৫সব ডেটা যদি RAM-এ ধরে যায়, তাহলে storage engine-এর আর কী কাজ বাকি থাকে? In-memory database কি durability-র চিন্তা থেকে মুক্ত?

২০০৮ সালের একটি পরিচিত গবেষণা — Harizopoulos, Abadi, Madden আর Stonebraker-এর “OLTP Through the Looking Glass, and What We Found There” (SIGMOD) — প্রশ্নটি মেপে দেখে। তাঁরা একটি প্রচলিত engine-কে ধাপে ধাপে ছেঁটে দেখান: পুরো ডেটা RAM-এ থাকলেও সময়ের বেশিরভাগ যায় buffer pool, locking, latching আর logging-এ, আর এগুলো সরালে মোটামুটি ২০ গুণ পর্যন্ত গতির ফারাক ব্যাখ্যা করা যায়। কোনো একটি অংশ একা দায়ী নয় — পুরো কাঠামোটিই ডিস্কের যুগের।

এই চিন্তা থেকেই H-Store (২০০৭) আর পরে VoltDB: ডেটা partition-এ ভাগ, প্রতিটি partition একটি thread-এ, তাই lock প্রায় লাগে না; page নেই, buffer pool নেই। কিন্তু durability? তাঁদের উত্তর ছিল তিন স্তরের: অন্য যন্ত্রে replica, মাঝে মাঝে snapshot, আর প্রতিটি লেনদেনের ডেটা নয়, শুধু “কোন command চলল” তার ছোট log — command logging.

Redis-এও একই আপস ভিন্ন রূপে। RDB snapshot-এর জন্য Redis একটি child process fork করে; OS-এর copy-on-write-এ শুরুতে memory ভাগ থাকে, কিন্তু snapshot চলাকালীন যত page বদলায়, তত নতুন কপি তৈরি হয় — লেখা-ভারী সময়ে RAM-এর ব্যবহার অনেক বেড়ে যেতে পারে। AOF প্রতিটি লেখা log-এ যোগ করে, আর fsync কতবার হবে তা বেছে নিতে দেয়। Log বড় হলে Redis সেটি নতুন করে লিখে ছোট করে — LSM-এর compaction আর database-এর checkpoint-এর আত্মীয়।

আর recovery-র সময় একটি বড় প্রশ্ন: অংশ ৮-এর হিসাবে ৫০ GB snapshot পড়তেই প্রায় ১০০ সেকেন্ড, তারপর log চালানো। তাই in-memory সেবায় restart-এর বদলে replica-তে failover (পর্ব ৫১) প্রায়ই আসল পরিকল্পনা।

মূল উপলব্ধি: RAM পড়ার পথ থেকে ডিস্ক সরিয়ে দেয়, কিন্তু durability-র প্রশ্ন সরায় না। Log থেকেই যায় — কোথায় লেখা হবে (নিজের ডিস্ক, না অন্য যন্ত্র) আর কতক্ষণ পরপর fsync, সেটাই নকশার আসল সিদ্ধান্ত।

অনুশীলন

কাগজ-কলমে হিসাব করুন, তারপর উত্তর মেলান। শেষ প্রশ্নে আগে অনুমান, তারপর simulator চালিয়ে যাচাই।

  1. B+-tree-এর উচ্চতা হিসাব করুন: একটি কাল্পনিক মেসেজিং সেবার টেবিলে ২০০ কোটি (২ × ১০৯) বার্তা, primary key একটি ১৬ byte-এর UUID. Page ১৬ KB, প্রতিটি child pointer ৮ byte, page গড়ে ৭৫% ভরা। ধরা যাক leaf-এও প্রতি page-এ একই সংখ্যক entry. (ক) Fan-out কত? (খ) গাছের উচ্চতা কত, আর ৩ স্তরে সর্বোচ্চ কত key ধরত? (গ) Leaf স্তর আর তার ওপরের সব স্তর মিলিয়ে কত জায়গা? (ঘ) একটি খোঁজে cache একেবারে ফাঁকা থাকলে কতটি ডিস্ক-পড়া, আর ওপরের স্তরগুলো RAM-এ থাকলে কতটি? (ঙ) Key ৮ byte-এর BIGINT হলে উচ্চতা কি বদলায়?
    • (ক) f = ⌊১৬,৩৮৪ × ০.৭৫ ÷ (১৬ + ৮)⌋ = ⌊১২,২৮৮ ÷ ২৪⌋ = ৫১২।
    • (খ) ৫১২৩ = ১৩,৪২,১৭,৭২৮ (প্রায় ১৩.৪ কোটি) < ২০০ কোটি, কিন্তু ৫১২৪ ≈ ৬,৮৭২ কোটি ≥ ২০০ কোটি। তাই উচ্চতা ৪।
    • (গ) Leaf page = ⌈২ × ১০৯ ÷ ৫১২⌉ = ৩৯,০৬,২৫০টি × ১৬ KB = ৬৪,০০,০০,০০,০০০ byte, অর্থাৎ ৬৪ GB (প্রায় ৫৯.৬ GiB). ওপরের স্তর: ⌈৩৯,০৬,২৫০ ÷ ৫১২⌉ = ৭,৬৩০, তারপর ১৫, তারপর ১ — মোট ৭,৬৪৬টি page, প্রায় ১২৫ MB (১১৯.৫ MiB).
    • (ঘ) ফাঁকা cache-এ ৪টি ডিস্ক-পড়া (SSD-তে প্রায় ০.৪ ms); ১২৫ MB-র ওপরের স্তর RAM-এ থাকলে মাত্র ১টি — ৬৪ GB-র টেবিলের জন্য সস্তা বিনিময়।
    • (ঙ) f = ⌊১২,২৮৮ ÷ ১৬⌋ = ৭৬৮; ৭৬৮৩ = ৪৫,২৯,৮৪,৮৩২ (প্রায় ৪৫ কোটি), তখনো ২০০ কোটির কম — উচ্চতা এখনো ৪, তবে leaf স্তর ছোট (প্রায় ৪২.৭ GB). এলোমেলো UUID-এর আসল দাম উচ্চতায় নয়, সব leaf-এ ছড়ানো insert-এ (অংশ ৪)।
  2. B-tree না LSM — তিনটি কাজ বিচার করুন: প্রতিটির জন্য একটি engine-পরিবার বাছুন, কারণ RUM-এর ভাষায় বলুন, আর একটি ঝুঁকি লিখুন। (ক) একটি কাল্পনিক mobile banking সেবা: লেখা মাঝারি, প্রতিটি transfer-এ দুটি হিসাব একসাথে বদলায়, statement-এ তারিখের range, p99 পড়া স্থির থাকা জরুরি। (খ) ride-sharing অ্যাপের চালকের location: সেকেন্ডে লাখের ঘরে লেখা, পড়া মূলত “শেষ কয়েক মিনিট”, ৭ দিন পরে মোছা। (গ) অনলাইন দোকানের পণ্যের তালিকা: read:write প্রায় ১০০:১, ক্যাটাগরি আর দামের range-এ খোঁজ।

    (ক) B-tree-ভিত্তিক relational database. একাধিক সারির atomic বদল এখানে সবচেয়ে জরুরি (পর্ব ২৪); statement মানে (account, date)-এর composite index আর leaf-এর শিকল ধরে range scan; পড়া ৩–৪ page-এ অনুমানযোগ্য। লেখা মাঝারি, তাই write amplification বড় সমস্যা নয়। ঝুঁকি: অতিরিক্ত index প্রতিটি transfer ধীর করে; durability-র সেটিং ঢিলে করা চলবে না।

    (খ) LSM-ভিত্তিক store (wide-column বা time-series). লেখা-প্রধান, প্রায় শুধু যোগ — memtable আর ক্রমিক flush এর জন্যই তৈরি; key-এ (চালক, সময়) রাখলে “শেষ কয়েক মিনিট” একটি ছোট range. ঝুঁকি: প্রতিটি পুরোনো সারিতে tombstone লিখলে compaction-এ বিশাল চাপ — ভালো পথ দিন বা ঘণ্টাভিত্তিক ফাইল বা partition পুরো বাদ দেওয়া; সাথে ডিস্ক headroom আর write stall-এর alert.

    (গ) B-tree-ভিত্তিক relational database, সামনে cache (পর্ব ২৮)। পড়া-প্রধান কাজে কম read amplification আর range-এর জন্য secondary index লাভজনক; লেখা কম, তাই index-এর দাম মানা যায়। ঝুঁকি: search box-এর পূর্ণ-লেখা খোঁজে B-tree যথেষ্ট নয় — সেখানে inverted index (পর্ব ২৭)।

    কোনো উত্তরেই অন্যটি অচল নয় — LSM দিয়েও (ক) চলে, B-tree দিয়েও (খ); শুধু কোন খরচ বাড়ছে, মেপে নিতে হয়।

  3. Column store-এ কত বাঁচে হিসাব করুন: একটি কাল্পনিক e-commerce warehouse-এ ২০ কোটি অর্ডারের সারি, ৪০টি কলাম, প্রতিটি কাঁচা মান ৮ byte. প্রশ্ন “শেষ মাসে বিভাগভিত্তিক মোট বিক্রি” ছোঁয় ৩টি কলাম: division (৮টি বিভাগ), order_date (দুই বছরে ৭৩০টি দিন) আর amount (সর্বোচ্চ ১,০০,০০০)। (ক) Row store আর কাঁচা column store-এ কত byte? (খ) Dictionary আর bit-packing দিলে? (গ) ডিস্ক সেকেন্ডে ২ GB পড়লে তিন ক্ষেত্রে scan-এর সময়? (ঘ) টেবিল তারিখের ক্রমে সাজানো আর zone map থাকলে, ২৪ মাসের ১ মাসে কত byte?
    • (ক) Row store: ২ × ১০৮ × ৪০ × ৮ = ৬৪ GB. কাঁচা column: ২ × ১০৮ × ৩ × ৮ = ৪.৮ GB — ৩ ÷ ৪০ = ৭.৫%.
    • (খ) division: ৮টি মান → ৩ bit; order_date: ৭৩০টি → ১০ bit (২১০ = ১,০২৪); amount: ১,০০,০০০ পর্যন্ত → ১৭ bit. সারিপ্রতি ৩০ bit = ৩.৭৫ byte; মোট ২ × ১০৮ × ৩.৭৫ = ৭৫০ MB — row store-এর ১.১৭%, প্রায় ৮৫ গুণ কম।
    • (গ) ৬৪ GB ÷ ২ GB/s = ৩২ সেকেন্ড; ৪.৮ GB → ২.৪ সেকেন্ড; ৭৫০ MB → ০.৩৭৫ সেকেন্ড।
    • (ঘ) তারিখে সাজানো থাকলে zone map দেখে বাকি ২৩ মাসের block ছোঁয়াই লাগে না: ৭৫০ ÷ ২৪ ≈ ৩১.২৫ MB — row store-এর পুরো scan-এর প্রায় দুই হাজার ভাগের এক ভাগ। শিক্ষা: কলাম বাছাই, compression আর sort order — তিনটি মিলেই বড় লাভ।
  4. আউটপুট অনুমান করুন: অংশ ৫-এর tiny_lsm.js-এর শেষ লাইনের পরে (ক) compact(); show("one more compaction"); যোগ করলে নতুন অংশে memtable, run, get-এর “looked in” আর writes-এর লাইন কী দেখাবে? (খ) তার বদলে put("u7", "Mitu"); put("u8", "Dola"); compact(); show("2 more writes + compaction"); যোগ করলে? আগে লিখে রাখুন, তারপর চালিয়ে মেলান।

    (ক) ডেটা কিছুই বদলায় না — একটিমাত্র run আবার নিজের সাথেই মিলে একই run হয়। কিন্তু compaction ৪টি entry আবার লেখে:

    == one more compaction
      memtable: u2=Karim3 u6=Tania
      run 1: u1=Rahim2 u2=Karim2 u4=Nila u5=Rafi
      on disk 4 entries for 4 live keys -> space amp 1.00
      get u2 -> Karim3  (looked in 1)
      get u1 -> Rahim2  (looked in 2)
      get u4 -> Nila  (looked in 2)
      get u3 -> not found  (looked in 2)
      get u9 -> not found  (looked in 2)
      writes: user 10, WAL 10, SSTable 16 -> write amp 1.6 (with WAL 2.6)

    SSTable-এ লেখা ১২ থেকে ১৬, write amp ১.২ থেকে ১.৬ (WAL-সহ ২.৬) — কোনো লাভ ছাড়াই। তাই engine compaction চালায় কোনো শর্ত (ফাইলের সংখ্যা, স্তরের আকার) পূর্ণ হলে তবেই।

    (খ) দুটি নতুন লেখায় memtable-এ ৪টি key হয় (u2, u6, u7, u8), তাই flush হয়ে একটি নতুন run; তারপর compaction দুটি run মিলিয়ে ৭টি key-এর একটি run বানায়:

    == 2 more writes + compaction
      memtable: 
      run 1: u1=Rahim2 u2=Karim3 u4=Nila u5=Rafi u6=Tania u7=Mitu u8=Dola
      on disk 7 entries for 7 live keys -> space amp 1.00
      get u2 -> Karim3  (looked in 2)
      get u1 -> Rahim2  (looked in 2)
      get u4 -> Nila  (looked in 2)
      get u3 -> not found  (looked in 2)
      get u9 -> not found  (looked in 2)
      writes: user 12, WAL 12, SSTable 23 -> write amp 1.9 (with WAL 2.9)

    u2-এর মান এখন Karim3 — memtable-এর নতুনতম মান flush হয়ে compaction-এ জিতেছে; memtable খালি, তাই u2 খুঁজতেও ২ জায়গা। লেখা: flush ৮ + ৪ = ১২, compaction ৪ + ৭ = ১১, মোট ২৩; ব্যবহারকারীর ১২টি লেখায় write amp ২৩ ÷ ১২ ≈ ১.৯ (WAL-সহ ৩৫ ÷ ১২ ≈ ২.৯)। প্রতিবার পুরো ডেটা মেলালে ডেটা যত বাড়ে, compaction তত দামি — তাই আসল engine স্তরে স্তরে অল্প অল্প মেলায়।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমLSM-tree-ভিত্তিক একটি engine-এ একটি লেখার ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. ✍️ ক্লায়েন্টের put অনুরোধ আসে
  2. 📜 WAL-এর শেষে record যোগ আর fsync
  3. 🧠 RAM-এর সাজানো memtable-এ key বসানো — তারপর ক্লায়েন্ট OK পায়
  4. 🔒 Memtable ভরে immutable হয়, নতুন memtable লেখা নেয়
  5. 💾 Immutable memtable ডিস্কে নতুন SSTable হিসেবে flush
  6. 🔀 Compaction পুরোনো SSTable-এর সাথে মিলিয়ে পুরোনো কপি আর tombstone বাদ দেয়

একটি B+-tree-এর প্রতিটি node-এর fan-out ৫০০, আর leaf-সহ গাছটি ৩ স্তরের। এতে সর্বোচ্চ মোটামুটি কতগুলো key ধরে?

  • ১,৫০০
  • ২,৫০,০০০
  • ১২,৫০,০০,০০০
  • ৬২,৫০,০০,০০,০০০

একটি database-এর log ডিস্কে প্রতিটি fsync-এ ২ ms লাগে। Group commit চালু, আর ব্যস্ত সময়ে প্রতি fsync-এ গড়ে ২০টি commit একসাথে যায়। এই একটি log সেকেন্ডে সর্বোচ্চ মোটামুটি কতগুলো commit টেকসই করতে পারে?

  • ৫০০
  • ১০,০০০
  • ১,০০,০০০
  • ২৫

একটি LSM-tree-এ key u3 মুছে ফেলা হলো, অথচ পুরোনো একটি SSTable-এ u3-এর মান এখনো আছে। Engine কীভাবে নিশ্চিত করে যে পরের খোঁজে u3 পাওয়া যাবে না?

  • একটি tombstone লেখা হয়, যা পুরোনো মানকে ঢেকে দেয়; compaction পরে দুটোই সরায়
  • পুরোনো SSTable খুলে u3-এর সারিটি সঙ্গে সঙ্গে মুছে ফেলা হয়
  • Bloom filter থেকে u3 সরিয়ে দেওয়া হয়
  • কিছুই করতে হয় না, memtable থেকে u3 মুছলেই যথেষ্ট

LSM-tree-এর প্রতিটি SSTable-এর সাথে একটি Bloom filter রাখার মূল উদ্দেশ্য কী?

  • SSTable-এর ডেটা সংকুচিত করা
  • Range query দ্রুত করা
  • কোনো key আছে কি না, নিশ্চিতভাবে বলা
  • যে SSTable-এ key নিশ্চিতভাবে নেই, তা না পড়েই বাদ দেওয়া

LSM-tree-এর size-tiered আর leveled compaction-এর তুলনায় কোন কথাটি সাধারণভাবে সত্য?

  • Leveled-এর write amplification কম, size-tiered-এর বেশি
  • Leveled-এ read আর space খরচ কম কিন্তু write বেশি; size-tiered-এ write কম কিন্তু read আর space বেশি
  • Leveled তিন দিকেই ভালো, size-tiered শুধু পুরোনো সিস্টেমে থাকে
  • দুটির খরচ একই, শুধু নাম আলাদা

একটি টেবিলে ৫০টি কলাম, সব কলামের আকার সমান, compression নেই। একটি প্রশ্ন মাত্র ২টি কলাম ছোঁয়। পুরো টেবিল scan করলে column store, row store-এর তুলনায় মোটামুটি কত ভাগ byte পড়বে?

  • ১০০%
  • ৫০%
  • ৪%
  • ০.৪%

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. Storage engine ডেটা রাখে, খোঁজে আর টেকসই রাখে; ডিস্কে random I/O sequential-এর চেয়ে বহু গুণ দামি (HDD-তে কয়েকশো গুণ), আর এই ফারাকই সব নকশার উৎস।
  2. যেকোনো engine-কে মাপুন read, write আর space amplification দিয়ে; RUM: তিনটি একসাথে কমানো যায় না।
  3. WAL: আগে log-এর শেষে যোগ আর fsync, পরে আসল কাঠামো; crash-এ checkpoint থেকে redo-undo; group commit একটি fsync-এ অনেক commit.
  4. B+-tree: h = ⌈logf N⌉, fan-out ৫০০-র ৩ স্তরে ১২.৫ কোটি key; জায়গায় বদল, page split, অনুমানযোগ্য পড়া। LSM-tree: memtable → SSTable → compaction, tombstone, Bloom filter; ক্রমিক লেখা, পেছনে compaction আর write stall-এর ঝুঁকি।
  5. Row store OLTP-তে, column store OLAP-এ; hash index point lookup-এ সরল আর দ্রুত; in-memory database-এও snapshot, log আর replica লাগে।

পরের পর্বে: engine-এর ভেতর দেখা হলো; এবার ওপর থেকে বাছাই। পর্ব ২০-এ SQL বনাম NoSQL — key-value, document, wide-column আর graph database কোন কাজে কোনটি, আর কোথায় কোন engine লুকিয়ে আছে।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান