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 বানায়।
আগে এটা বুঝি
আজ পর্যন্ত আমাদের সব 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 থেকে:
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!) | 20 | 30 |
| 64 | 4 | 5 |
| 512 | 3 | 4 |
| 1024 | 2–3 | 3 |
Binary tree-র ৩০ disk-hop বনাম B-tree-র ৩ — প্রতি hop ~১০০ µs-এ milliseconds-vs-microseconds; এই ফারাকই database-কে database বানায়।
B-tree-র নিয়মকানুন
Minimum degree t ≥ 2 (CLRS convention):
- প্রতিটা node-এ ≤
2t − 1key (full) - Root বাদে প্রতিটা node-এ ≥
t − 1key - Internal node-এ child = key + 1; key-গুলো child-দের মাঝে separator — child_i-র সব key প্রতিবেশী separator-দের মাঝে
- সব leaf একই depth — tree নিজেই perfect-কাছাকাছি!
- নতুন key সবসময় leaf-এ ঢোকে
Property 4-ই আত্মা: BST নিচে-উপরে হতো; B-tree শুধু প্রস্থে বাড়ে। আর সমান-depth টেকানোর একটাই উপায় — split।
Insert — উপর থেকে ভাঙা (preemptive split)
Leaf ভরলে দুই-ভাগ, মাঝের key উপরে; parent-ও full হলে সে-ও… bottom-up split-chain-এ ফেরা লাগে। CLRS-এর চাল: নামার পথেই প্রতিটা full child আগে ভাঙো —
- Root full?ভাঙো — নতুন root; height বাড়ে শুধু এখানে
- Path ধরে নামোযে child-এ যাবেন সে full? নামার আগেই split
- Splitmedian parent-এ ওঠে; দুই ভাই অর্ধেক-অর্ধেক key পায়
- 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 দুটো বদল:
- Data শুধু leaf-এ — internal key-রা নিঃস্ব signpost (“ছোটগুলো বাঁয়ে”)। একই জায়গায় আরো signpost → fanout আরো বড় → height আরো কম।
- 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-walk16 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 → fanout → height — সংখ্যাটা নিজে বের করুন
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-র অস্তিত্বের কারণ।
নিজে বানান
B-tree insert-only — parameterized t, split propagation, validator
- BTNode: keys + children + leaf flag
- _split_child(x, i): i-তম full child দুই-ভাগ, median parent-এ
- insert: root full → নতুন root + split; নামার পথে preemptive split
- in_order(): recursive dump — sorted-output প্রমাণ
- validate(): bounds + ordering + child-count + uniform leaf depth
- 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।
বুঝেছেন কি না দেখুন
1t = 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 ভাঙা ছাড়া উপায় নেই।
2Fanout 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 ধরে না।
3Preemptive 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 দিয়ে ঢাকতে হয়। কোনো পছন্দ বিনামূল্যে না।
5Leaf-link ছাড়া range query a..b-তে ঠিক কত বাড়ে? Big-O আর disk-hops
দুই দিকেই।
যুক্তি
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