Foundationপ্রথম নীতি থেকে
LEVEL 6লেসন ৯/২৪অ্যাডভান্সড৫৫ মিনিট

B-Tree ও B+ Tree — disk-এর জন্য জন্ম নেওয়া tree

B-Trees and B+ Trees

Disk byte না, পুরো page পড়ে — তাই B-tree node page-সমান চওড়া করে fanout হাজারে তোলে আর height ৩-৪-তে নামায়; B+ variant leaf-link দিয়ে range scan sequential read বানায়।

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

  • block storage-এর economics ব্যাখ্যা করে বলতে পারবেন কেন disk-এ comparison নয়, depth কমানোই আসল খেলা
  • branching factor = page size ÷ entry size হিসাব করে দেখাতে পারবেন কীভাবে node-এর প্রস্থ fanout ঠিক করে
  • B-tree-র structural property (t ≥ 2, keys-bound, uniform leaf depth) লিখে split propagation trace করতে পারবেন
  • B বনাম B+ tree-র পার্থক্য থেকে range scan-এর খরচ-পার্থক্য যুক্তি দিয়ে বলতে পারবেন
  • fanout 2/64/1024-এর height-হিসাব দিয়ে প্রমাণ করতে পারবেন কেন AVL disk-এ টিকে না, B-tree টিকে

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

আগে এটা বুঝি

আজ পর্যন্ত আমাদের সব tree RAM-এ। RAM-এ এক address পড়া ~১০০ ns — AVL-এ ২০-৩০ ধাপ নামা নির্ভয়ে করা যায়।

কিন্তু database-index, filesystem-metadata-দের ঘর disk। সেখানে অর্থনীতি উল্টো:

Mediumএক random access
L1 cache~১ ns
RAM~১০০ ns
SSD~১০০ µs
HDD (seek+rotate)~১০ ms

SSD random-read = RAM-এর হাজার গুণ; HDD-তে শত-সহস্র গুণ। আর mechanical সত্যটা: storage কখনো byte এনে দেয় না — পুরো block/page (৪–১৬ KB), ৮ byte চাইলেও।

এই দুটো সত্য গত-লেসনের সূত্র উল্টে দেয়। RAM-এ ছুটেছিলাম comparison কমাতে; disk-এ pointer-hop-ই দামি — প্রতি hop = এক page read, অথচ পড়া page-এ হাজার byte এসেই ঝুলছে, scan করা প্রায় ফ্রি!

মূল সূত্র: disk-এ tree-র খরচ ≈ depth × page-read-দাম। Comparison হাজার করার চিন্তা নয় — depth ন্যূনতম, আর প্রতি depth-এ আসা page-এর ভেতরটা পুরো ব্যবহার। এই এক বাক্যের জন্ম B-tree, ১৯৭০- এ — আজও প্রতিটা database index-এর ভিত্তি।

মূল ধারণা

Node = Page — ডিজাইনের প্রথম সিদ্ধান্ত

B-tree-র প্রথম চাল: node-কে ঠিক এক page-এর সমান চওড়া। ভেতরে শত শত key + child-pointer — সব contiguous array-তে। ফলে node-পড়া = এক page-read; node-ভেতরের খোঁজা = CPU-র কাছে ছোট array-scan, প্রায় ফ্রি। Branching factor (fanout) সরাসরি geometry থেকে:

fanout≈pagekey+child pointer\text{fanout} \approx \frac{\text{page}}{\text{key} + \text{child pointer}}

16 KB page, 8-byte key + 8-byte pointer → fanout ≈ 1024 — প্রতি ধাপে জগৎ হাজার ভাগ! (Experiment-এ সংখ্যাটা নিজে বের করবেন।)

Height math — তিন level-এ দশ লাখ

Fanout f হলে depth d-তে (edge-গুনে) ~f^d key:

Fanout১০⁶ key১০⁹ key
2 (binary!)2030
6445
51234
10242–33

Binary tree-র ৩০ disk-hop বনাম B-tree-র ৩ — প্রতি hop ~১০০ µs-এ milliseconds-vs-microseconds; এই ফারাকই database-কে database বানায়।

B-tree-র নিয়মকানুন

Minimum degree t ≥ 2 (CLRS convention):

  1. প্রতিটা node-এ ≤ 2t − 1 key (full)
  2. Root বাদে প্রতিটা node-এ ≥ t − 1 key
  3. Internal node-এ child = key + 1; key-গুলো child-দের মাঝে separator — child_i-র সব key প্রতিবেশী separator-দের মাঝে
  4. সব leaf একই depth — tree নিজেই perfect-কাছাকাছি!
  5. নতুন key সবসময় leaf-এ ঢোকে

Property 4-ই আত্মা: BST নিচে-উপরে হতো; B-tree শুধু প্রস্থে বাড়ে। আর সমান-depth টেকানোর একটাই উপায় — split।

Insert — উপর থেকে ভাঙা (preemptive split)

Leaf ভরলে দুই-ভাগ, মাঝের key উপরে; parent-ও full হলে সে-ও… bottom-up split-chain-এ ফেরা লাগে। CLRS-এর চাল: নামার পথেই প্রতিটা full child আগে ভাঙো —

Insert-এর এক-পথ journey
  1. Root full?ভাঙো — নতুন root; height বাড়ে শুধু এখানে
  2. Path ধরে নামোযে child-এ যাবেন সে full? নামার আগেই split
  3. Splitmedian parent-এ ওঠে; দুই ভাই অর্ধেক-অর্ধেক key পায়
  4. Leaf-এsorted অবস্থানে key বসাও — ফেরার পথ নেই

একবার নামা, একবারই — উপরে কেউ full থাকার সম্ভাবনাই রইল না। খরচ O(log_t n) page-access + O(t) ভেতরে-কাজ; height বাড়ে শুধু root-split- এ — কোনো rotation-ই লাগে না, শুধু ভাঙা!

ভেতরে কী ঘটছে

AVL disk-এ কেন মরে — সংখ্যায়

১০⁹ record, HDD (seek ~১০ ms):

  • BST/AVL: height ~৩০ hop → query ~৩০০ ms — প্রতি lookup-এ তৃতীয়াংশ second!
  • B-tree (fanout 512): height ৪ → ~৪০ ms; SSD-তে ৩ ms vs 0.4 ms।

সূক্ষ্মতা: B-tree-র node-scan আসলে RAM/cache-এ (page একবার পড়ার পরে) — binary search সেখানে বিনামূল্যে। যেখানে খরচ নেই সেখানে comparison ঠেলে দেওয়াই B-tree-র দর্শন।

B+ Tree — data leaf-এ, leaf-রা সুতোয়

Classic B-tree-তে প্রতি node-এ key + data। B+ tree দুটো বদল:

  1. Data শুধু leaf-এ — internal key-রা নিঃস্ব signpost (“ছোটগুলো বাঁয়ে”)। একই জায়গায় আরো signpost → fanout আরো বড় → height আরো কম।
  2. Leaf-linked list — প্রতিটা leaf ডান-প্রতিবেশীর address জানে।

দ্বিতীয়টার ফল range scan: WHERE id BETWEEN 500 AND 900 = ৫০০-তে এক descend + leaf-link ধরে হাঁটা — sequential access! Disk-এর সবচেয়ে সস্তা কাজ (HDD-তে seek নেই, SSD/OS prefetch-friendly)। Classic B-tree- তে বারবার root-descent = বারবার random hop। তাই InnoDB, PostgreSQL, SQLite সবাই B+-tree; “B-tree index” বললে মানুষ B+-ই বোঝে।

এরা কোথায় বাস করে

  • Databases: PostgreSQL default index = B+-tree (৮ KB page); InnoDB ১৬ KB; SQLite পুরো database-ই B+-tree-র সমারোহ। databases module-এ page-এর ভেতরে ঢুকব।
  • Filesystems: ext4 extent tree, XFS metadata B+-tree, NTFS MFT, APFS — আপনার laptop এই মুহূর্তে হাজারখানে B-tree চালাচ্ছে।
  • Key-value: LMDB = এক copy-on-write B+-tree।

উদাহরণ

t = 3-এ insert — হাতে split trace

t=3: প্রতি node max ৫ key, min ২ (root বাদে)। Sorted input 1..12 — গত-লেসনের ভিলেন, আজ harmless:

insert 6 → প্রথম split:
        [3]
       /   \
   [1,2]   [4,5,6]

insert 9 → ডান leaf ভারী, split:
      [3, 6]
     /   |   \
 [1,2] [4,5] [7,8,9]

insert 12 → শেষ leaf ভারী, split:
       [3, 6, 9]
     /    |     |    \
 [1,2] [4,5] [7,8] [10,11,12]

মোট ৩ split; height বাড়ল একবার (root-split);
কোনো rotation, কোনো rebalance — শুধু ভাঙা।

Sorted input-ও এখন সমস্যা না — key leaf-এ জমে, leaf ভরলে ভাঙে, median separator হয়ে উঠে যায়। BST-র degenerate-chain-এর প্রশ্নই জন্মায় না।

Range scan: 5..11। Descend ৫-এ (root [3,6,9]: 5 > 3, 5 < 6 → দ্বিতীয় child [4,5])। তারপর ৫, ৬(signpost পার করে), ৭, ৮, ৯, ১০, ১১ — leaf-link ধরে এক-দিকা হাঁটা, root-এ ফেরা শূন্যবার। Sequential।

ছবিতে

                 ┌─────────────┐
                 │  17 │  35   │        ← internal: শুধু signpost
                 └──┬───────┬──┘
              ┌─────┘       └─────┐
      ┌─────────────┐      ┌─────────────────┐
      │  5 │ 10  17*│      │  25 │  35*      │   ← 17*, 35*: routing copy
      └──┬────────┬─┘      └──┬───────────┬──┘   (data নয়!)
   ┌─────┘         └──┐    ┌────┘           └───┐
┌──────────┐   ┌──────────┐   ┌──────────┐   ┌──────────┐
│data: 2,3,5│→ │data: 9,10,13│→ │data: 17,21,25│→ │data: 28,35,40│
└──────────┘   └──────────┘   └──────────┘   └──────────┘

point '21': ৩ hop descent; range '10..28': ১ descent → link-walk
B+ anatomy — internal মাত্র signpost; data leaf-এ, leaf-link ডানে।
16 KB page = এক internal node:
┌───────────────────────────────────────────────────────┐
│ header (type, key count, sibling ptr...)   ~১০০ bytes │
├───────────────────────────────────────────────────────┤
│ k₁ │ k₂ │ k₃ │ ...                    8 bytes × k     │
├───────────────────────────────────────────────────────┤
│ p₀ │ p₁ │ p₂ │ p₃ │ ...              8 bytes × (k+1) │
└───────────────────────────────────────────────────────┘

8k + 8(k+1) ≤ 16384  →  k ≤ 1023  →  fanout ≤ 1024
(leaf-এ p = record-pointer/data — আলাদা, সাধারণত আরো বড় হিসাব)
Page-এর ভেতরটা — 'entry size' ফর্মুলার উৎস।

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

EXPERIMENT

Page → fanout → height — সংখ্যাটা নিজে বের করুন

Python 3.8+· ১০ মিনিট
import math

# ---------- ১. Optimal node size ----------
PAGE = 16 * 1024          # InnoDB page
KEY  = 8                  # bigint key
PTR  = 8                  # child pointer
entry = KEY + PTR
k_max = (PAGE - entry) // entry
print(f'entries/node ≈ {k_max}, fanout ≈ {k_max + 1}')

# ---------- ২. Fanout vs height ----------
def levels_needed(fanout, n):
    return math.ceil(math.log(n, fanout))

for f in (2, 64, 512, 1024):
    print(f'fanout {f:>5}: 10^6 keys -> {levels_needed(f, 10**6)} levels,'
          f'  10^9 keys -> {levels_needed(f, 10**9)} levels')

# ---------- ৩. Query-time estimate ----------
for medium, ns in (('HDD (10 ms/read)', 10**7), ('SSD (100 us/read)', 10**5),
                   ('RAM (100 ns)', 100)):
    bst_ms = 30 * ns / 1e6                    # binary tree, 10^9 keys
    bt_ms  = levels_needed(1024, 10**9) * ns / 1e6
    print(f'{medium:20s} BST-index ~{bst_ms:>7,.1f} ms/query,'
          f'  B+-tree ~{bt_ms:>6,.2f} ms/query')

প্রত্যাশিত output:

entries/node ≈ 1023, fanout ≈ 1024
fanout     2: 10^6 keys -> 20 levels,  10^9 keys -> 30 levels
fanout    64: 10^6 keys -> 4 levels,  10^9 keys -> 5 levels
fanout   512: 10^6 keys -> 3 levels,  10^9 keys -> 4 levels
fanout  1024: 10^6 keys -> 2 levels,  10^9 keys -> 3 levels
HDD (10 ms/read)     BST-index ~  300.0 ms/query,  B+-tree ~ 40.00 ms/query
SSD (100 us/read)    BST-index ~    3.0 ms/query,  B+-tree ~  0.40 ms/query
RAM (100 ns)         BST-index ~    0.3 ms/query,  B+-tree ~  0.00 ms/query

তিন reading: (১) int-key-তে fanout হাজার ছোঁয়; (২) মাঝারি fanout-৬৪-এও ১০⁶ key ৪ level — তাই ৮ KB engine-রা আরামে; (৩) HDD-তে BST-vs-B-tree হাজারগুণ — এই সারিটাই ১৯৭২ paper-এর justification।

নিজে বাড়ান: KEY = 128-byte string করে চালান — fanout কমে height কয়েক level বাড়ে, তবু ৩-ঘেঁষা (B-tree কত অনায়াসী); PAGE = 4 KB করে দেখুন — engine-রা ৮/১৬ KB কেন, intuition তৈরি হবে।

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

বাস্তব page/key/pointer মাপ থেকে fanout ~১০২৪ দাঁড়ায়, আর সেই fanout-এ ১০⁹ key-ও ৩ level-এ; binary tree-র দরকার ৩০ — disk-access গণিতের এই ফারাকই B-tree-র অস্তিত্বের কারণ।

নিজে বানান

BUILD IT

B-tree insert-only — parameterized t, split propagation, validator

Python · ●●●○○
  1. BTNode: keys + children + leaf flag
  2. _split_child(x, i): i-তম full child দুই-ভাগ, median parent-এ
  3. insert: root full → নতুন root + split; নামার পথে preemptive split
  4. in_order(): recursive dump — sorted-output প্রমাণ
  5. validate(): bounds + ordering + child-count + uniform leaf depth
  6. suite: t ∈ `{2,3,4}`, random + sorted input, প্রতি insert-এ validate
import math
import random


class BTNode:
    __slots__ = ('keys', 'children', 'leaf')

    def __init__(self, leaf=True):
        self.keys = []          # sorted
        self.children = []      # leaf হলে খালি
        self.leaf = leaf


class BTree:
    def __init__(self, t):
        assert t >= 2, 'minimum degree অন্তত ২'
        self.t = t
        self.root = BTNode(True)
        self.splits = 0

    def _split_child(self, x, i):
        """x-এর i-তম (full) child দুই-ভাগ; median x-এ ওঠে"""
        self.splits += 1
        t = self.t
        y = x.children[i]
        z = BTNode(y.leaf)
        mid = y.keys[t - 1]
        z.keys = y.keys[t:]             # উপরের t−1 key z-এ
        y.keys = y.keys[:t - 1]         # নিচের t−1 y-তে
        if not y.leaf:
            z.children = y.children[t:]
            y.children = y.children[:t]
        x.children.insert(i + 1, z)
        x.keys.insert(i, mid)

    def _insert_nonfull(self, x, key):
        while not x.leaf:
            i = len(x.keys) - 1
            while i >= 0 and key \< x.keys[i]:
                i -= 1
            i += 1
            if len(x.children[i].keys) == 2 * self.t - 1:
                self._split_child(x, i)      # preemptive!
                if key > x.keys[i]:
                    i += 1
            x = x.children[i]
        pos = len(x.keys)
        while pos > 0 and x.keys[pos - 1] > key:
            pos -= 1
        x.keys.insert(pos, key)

    def insert(self, key):
        r = self.root
        if len(r.keys) == 2 * self.t - 1:
            s = BTNode(False)                # নতুন root —
            s.children = [r]                 # height বাড়ে শুধু এখানে
            self.root = s
            self._split_child(s, 0)
            self._insert_nonfull(s, key)
        else:
            self._insert_nonfull(r, key)

    def in_order(self, x=None):
        x = x if x is not None else self.root
        if x.leaf:
            yield from x.keys
        else:
            for i, k in enumerate(x.keys):
                yield from self.in_order(x.children[i])
                yield k
            yield from self.in_order(x.children[-1])

    def height(self):
        n, h = self.root, 1
        while not n.leaf:
            n = n.children[0]; h += 1
        return h

    def validate(self, x=None, is_root=True):
        """সব property assert; returns leaf-depth"""
        x = x if x is not None else self.root
        assert len(x.keys) \<= 2 * self.t - 1, 'over-full node!'
        if not is_root:
            assert len(x.keys) >= self.t - 1, 'under-full node!'
        assert all(x.keys[i] \< x.keys[i + 1]
                   for i in range(len(x.keys) - 1)), 'unsorted node!'
        if x.leaf:
            return 0
        assert len(x.children) == len(x.keys) + 1, 'child count!'
        depths = {self.validate(c, False) for c in x.children}
        assert len(depths) == 1, 'leaf-depth সমান নয়!'
        return 1 + depths.pop()


if __name__ == '__main__':
    bt = BTree(3)
    before = marks = 0
    events = []
    for k in range(1, 13):
        pre = bt.splits
        bt.insert(k)
        bt.validate()
        if bt.splits > pre:
            events.append((k, bt.splits))
    print('splits:', events)
    print('height:', bt.height())
    assert list(bt.in_order()) == list(range(1, 13))
    print('in-order OK:', list(bt.in_order()))

    for trial in range(20):                  # assertion suite
        tr = BTree(random.choice([2, 3, 4]))
        ks = random.sample(range(100_000), 500)
        for k in ks:
            tr.insert(k); tr.validate()
        assert list(tr.in_order()) == sorted(ks)
        seq = BTree(tr.t)                     # sorted attack — harmless
        for k in range(1, 301):
            seq.insert(k); seq.validate()
        assert list(seq.in_order()) == list(range(1, 301))
    print('assertion suite OK — ২০ trial × (random + sorted)')

প্রত্যাশিত output:

splits: [(6, 1), (9, 2), (12, 3)]
height: 2
in-order OK: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]
assertion suite OK — ২০ trial × (random + sorted)

(example-এর hand-trace-এর সাথে splits মিলল? মিললে split-mechanics আয়ত্তে।)

নিজে বাড়ান: (১) search(key) + node-visit counter — height-এর সাথে মিলান; (২) delete() — underflow-এ borrow/merge; validator বলবে কোথায় ভুল; (৩) fill-factor মাপুন (bytes_used tracking) — random insert- এ বিখ্যাত ~৬৯% আসে, bulk-load-এ ১০০% কেন engines সেট করে তার উত্তর।

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

এই মুহূর্তে চলা B-tree-রা

  • PostgreSQL: CREATE INDEX ডিফল্ট B+-tree; EXPLAIN-এ “Index Scan” দেখলেই আপনি B-tree descent দেখছেন।
  • InnoDB: clustered index — table-ই বিশাল B+-tree, leaf-এ পুরো row; secondary index-এ primary-key-র মাধ্যমে দ্বিতীয় descend — “secondary expensive”-এর আসল কারণ।
  • SQLite: single-file database-এর রহস্য = সবকিছু page-based B+-tree (table + প্রতি index একটা করে)।
  • Filesystems: XFS free-space/metadata B+-tree, ext4 extent index, NTFS $MFT, APFS OMAP — ls-এর পেছনে একাধিক descent।

Tuning-ত্রয়ী মনে রাখুন: page-size × key-size × fill-factor — আজকের ফর্মুলাই production সিদ্ধান্তের ইনপুট।

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

“B-tree মানে Binary বা Balanced — B তাই।”

না — fanout শত-সহস্র, binary কিছুই না। “Balanced”-সংক্ষেপটাও অনুমান মাত্র: Bayer-McCreight কখনো B-র অর্থ ঘোষণা করেননি। প্রার্থী: Bayer, Boeing, balanced, broad, bushy। Interview-এ বলুন “লেখকরা বলেননি” — এটাই সঠিক।

“B+ সব দিকে B-র চেয়ে ভালো — classic B-tree অচল।”

Industry-default B+ ঠিক, দামও আছে: key internal+leaf দুইবার (space + delete-এ দুই জায়গায় হাত); point-query সবসময় leaf পর্যন্ত, যেখানে classic উপরেই match পেয়ে থামতে পারে। In-memory/ছোট dataset-এ ফারাক মাপা যায় — “default” ≠ “universally superior”।

“Node যত চওড়া, performance তত ভালো — depth তো কমবেই।”

তিন বিল আসবে: point-query-তে wasted bandwidth (৮-byte key-র জন্য ৬৪ KB!), split/write amplification, buffer-cache pressure। RAM-resident ছোট tree- তে চওড়া node-এর linear scan উল্টে খারাপও হতে পারে। Optimal size workload- dependent — engine-গুলো configurable রাখে এজন্যই।

“LSM B-tree-কে replace করেছে — B-tree legacy।”

OLTP database-র মেরুদণ্ড এখনো B+-tree; LSM জিতেছে write-heavy append-style niche-এ। Compaction CPU/disk খায়, read-amplification ও space-amplification আছে। Team-রা read/write ratio + latency tail দেখে বাছে — TiDB-র মতো system দুটোর combination-ই চালায়। Proven ≠ dead।

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

1

t = 3: (ক) internal node-এ key min/max? (খ) root-এ? (গ) child max?

স্মরণ

(ক) non-root internal: min t−1 = 2, max 2t−1 = 5। (খ) root min 1 (নইলে one-node tree illegal), max তবু ৫। (গ) child = key+1 → max 6। Root-ছাড়ের কারণ: delete-এ root খালি হলেও children merge হয়ে নতুন root — bound ভাঙা ছাড়া উপায় নেই।

2

Fanout 200-এ ১০⁹ key: root→leaf কত hop? Binary tree-তে? SSD (১০০ µs/hop)- এ দুই query-time?

প্রয়োগ

ceil(log₂₀₀(10⁹)) = ceil(9/2.301) = ceil(3.91) = 4 hop; binary: ceil(log₂(10⁹)) = 30। সময়: 4×100µs = 0.4 ms vs 30×100µs = 3 ms — ৭.৫×; HDD-তে 40 ms vs 300 ms — order-of-magnitude। বাস্তবে upper levels cached — B+-tree effective আরো দ্রুত, BST-র ৩০ random access cache ধরে না।

3

Preemptive top-down split-এর বিপরীতে bottom-up-এ ঠিক কী extra খরচ, আর কোন সুবিধা হাতছাড়া?

যুক্তি

Extra খরচ: split-chain উপরে propagate করলে path ফেরত traverse (parent-tracking/stack) — worst-case পথের প্রতিটা node দুইবার touch + implementation জটিল। হাতছাড়া সুবিধা: leaf-এ exact-position জানা অবস্থায় smart split (monotonic insert-এ rightmost-append, split-point বাছাই)। Top-down সেটা ত্যাগ করে single pass নেয় — storage-এ half page-touch, প্রায় সবসময়ই জেতায়।

4

(A) IoT telemetry — সেকেন্ডে ৫ লাখ event write, batch analytics read; (B) user-profile API — read-heavy point lookup + occasional export। LSM নাকি B+-tree, কোনটা কোথায়?

ডিজাইন

(A) LSM: SLA = write throughput; memtable-absorb + sequential flush এই pattern-এর জন্যই। Read-amplification acceptable — read তো batch; compaction steady-ingest-এ budgetable।

(B) B+-tree: SLA = point-lookup latency/tail; predictable ৩-৪ page-read, warm-cache sub-ms; compaction-stall-এ p99 spike-এর ঝুঁকি নেই। Range export leaf-link walk-এ sequential।

Bonus: (B)-তে হঠাৎ write burst এলে random-write ব্যথা — queue buffering দিয়ে ঢাকতে হয়। কোনো পছন্দ বিনামূল্যে না।

5

Leaf-link ছাড়া range query a..b-তে ঠিক কত বাড়ে? Big-O আর disk-hops দুই দিকেই।

যুক্তি

Link-ছাড়া: প্রতি key আলাদা descent — O((b−a)·log_f n) visits; disk- hops ≈ (b−a) random read (upper nodes cached হলেও leaves ছড়ানো)।

Link থাকলে: এক descent + chain-walk — log_f n + (b−a)/capacity, আর walk-টা sequential (HDD seek নেই, SSD/OS prefetch)। পার্থক্য constant না: sequential-vs-random SSD-তে ~১০×, HDD-তে ~১০০×। Design-এর এক pointer- ই query-class-এর খরচ গুণিতক থেকে যোগফলে নামায়।

এরপর কী

এরপর কী

B-tree শিখলাম disk-জগতে tree চওড়া করে চ্যাপ্টা রাখা — কিন্তু nodes তবু heap-allocated object, pointer-জাল। পরের লেসনে pointer-থেকেও মুক্তি: heap — complete binary tree flat array-তে, parent(i)=i//2, children(i)=2i,2i+1 — একটাও pointer নেই, locality নিখুঁত। Priority queue-র ইঞ্জিন সেই heap: scheduler-runqueue থেকে Dijkstra, event-loop timer থেকে k-way merge। আর build-heap-এর counter-intuitive O(n)-এর প্রমাণ-খসড়াও মিলবে।

আরও পড়ুন

  • "Organization and Maintenance of Large Ordered Indices" (Acta Informatica, 1972) — Rudolf Bayer, Edward M. McCreight · B-tree-র মূল পেপার — 'B' আসলে কীসের সংক্ষেপ, লেখকরাও বলেননি!
  • "The Ubiquitous B-Tree" (ACM Computing Surveys, 1979) — Douglas Comer · Variant-গুলোর ক্লাসিক survey — B+/B-tree naming এই লেখা থেকেই প্রচলিত
  • PostgreSQL Documentation, Chapter 11 — Indexes (B-Tree) — The PostgreSQL Global Development Group · Production database-এ B-tree page বাস্তবে কেমন — ৮ KB page, high key, deduplication