Buffer Pool — disk যখন memory-তে ভাড়া থাকে
Buffer Pool and Replacement Policies
Page disk-এ থাকে, কাজ হয় RAM-এ — buffer manager কীভাবে frame ভাড়া দেয়, LRU-র ভিলেন-দৃশ্য থেকে CLOCK আর PostgreSQL-র ring buffer পর্যন্ত replacement policy-র রাজনীতি, hit-rate math আর mmap debate।
আগে এটা বুঝি
গত লেসনে দেখলাম table = ৮ KB page-দের file। হিসাবটা একবার করুন: HDD random read ~১০ ms, SSD ~১০০ µs, RAM ~১০০ ns — হাজার–লাখ গুণ ফারাক। প্রতি query- তে disk ছুঁলে database মরে যায়। সমাধান cache — কিন্তু কে? OS-ই তো pagecache চালায়! এখানেই বড় সিদ্ধান্ত: প্রায় সব serious engine নিজের buffer pool চালায় — fixed frames-এ ভাড়া বসে ৮ KB page, নিজস্ব replacement policy; বিরূপণী O_DIRECT পর্যন্ত।
আজকের প্রশ্নগুলো:
- Double buffering — OS pagecache থাকতেও নিজের pool কেন
- Buffer manager API: pin/unpin + dirty flag, WAL-ordered flush-এর ইঙ্গিত
- Policy-রাজনীতি: LRU-র পতন, CLOCK-এর second chance, PG-র ring buffer
- Hit-rate math — cold vs warm ১০০× ফারাকের হিসাব; শেষে mmap war
মূল ধারণা
Double buffering — একই page দুইবার ভাড়া
OS pagecache খারাপ না — readahead দারুণ। সমস্যা: engine page পড়ল copy #১ kernel-cache-এ, আবার pool-এও রাখল copy #২ — একই ৮ KB দুই জায়গায়, দুই eviction-pressure, কেউ কারো খবর রাখে না।
RAM duplicate হয়, eviction decision দুই স্তরে সংঘর্ষে যায়, আর dirty page কবে
disk-এ যাবে — সেই control engine-এর হাতে থাকা চাই (WAL ordering!)। উত্তর দুই
দলে: PostgreSQL/InnoDB দ্বৈত খরচ মেনে চলে; Oracle/DB2-style O_DIRECT bypass;
SQLite-র মতো embedded-রা উল্টো OS-কেই বিশ্বাস।
Buffer manager-এর contract
API ছোট, কিন্তু শৃঙ্খলা কঠোর:
pin(page_id) → frame page-টা pool-এ আনো; কেউ ফেলে দিতে পারবে না
unpin(frame, dirty?) কাজ শেষ; dirty=true মানে বদলানো হয়েছে- Pin count — কয়টা active user; zero হলেই কেবল eviction-এর যোগ্য
- Dirty flag — frame ভাড়াটিয়া বদলেছে কি না; clean victim মানে eviction = শুধু ভুলে যাওয়া (disk write নেই!), dirty victim = আগে লিখতেই হবে
- Pool full + সব frame pinned → query অপেক্ষা করে — pin leak মানে production deadlock-এর এক classic কারণ
Replacement policy — কাকে বিদায়
Pool ভরা, নতুন page দরকার — কাকে বের করব? ভবিষ্যৎ অজানা, অতীত proxy:
| Policy | ভাবনা | বাস্তব সমস্যা |
|---|---|---|
| FIFO | সবচেয়ে পুরনো যাবে | ঘন-ঘন ব্যবহৃত পুরনো page-ও যায় |
| LRU | সবচেয়ে আগে-অব্যবহৃত | Full-scan pollution — নিচে |
| MRU | সবচেয়ে নতুনটা | Loop query-তে অদ্ভুত ভালো — কৌতূহলের খোরাক |
| CLOCK | LRU-র সস্তা approximation | Bare form-এ pollution ঠেকায় না |
LRU-র পতন — এক scan-এ পুরো pool ধ্বংস
SELECT sum(x) FROM events — ৫০ GB scan। LRU-র চোখে প্রতিটা scanned page
‘সদ্য-ব্যবহৃত’; pool ভরে যায় one-hit-wonder-দের দিয়ে, OLTP app-এর গোটা hot set
বেরিয়ে যায়। Scan শেষে app ঠান্ডা pool-এ — প্রতিটা touch miss, সকালের SLA শেষ।
নাম scan pollution; প্রতিষেধ scan resistance。
CLOCK — LRU-র গরিব চাচা
Exact LRU-তে প্রতি access-এ list move — lock contention। CLOCK: সব frame বৃত্তে, প্রতিটায় reference bit, এক hand। Access → bit=1 (list move নেই); victim খুঁজতে hand ঘোরে, bit 1 → 0 করে এগোয় (second chance), bit 0 পেলে victim। Sweep-এ সবার bit শেষ হয়ে যায় — CLOCK ≈ ‘আনুমানিক LRU’, ভাতা-মাসিতে। কিন্তু ৩× pool- এর scan-ও দুই lap-এ সব গিলে ফেলে — bare CLOCK pollution ঠেকায় না, দেরি করায়।
PostgreSQL-এর জবাব — ring buffer
PG-র strategy: scan-কে চিনে isolate করা। বড় sequential scan/vacuum ছোট বেসরকারি ring buffer-এ (~২৫৬ KB) ঘোরে, মূল pool ছোঁয় না; scan page-রা নিজেরাই evict হয়, hot set অক্ষত। Scan resistance = policy নয়, isolation। Build section-এ এই তিন জনের লড়াই নিজেই চালাবেন।
Hit rate — সবকিছুর হিসাব
Average read time: T = h × t_mem + (1−h) × t_disk (h = hit rate)। সংখ্যায়: t_mem = ১০০ ns, t_disk (SSD) = ১০০ µs:
| h | T | অনুভূতি |
|---|---|---|
| 99% | ~1.1 µs | স্বপ্ন |
| 90% | ~10 µs | ঠিক আছে |
| 50% | ~50 µs | SSD-র দাস |
| 0% | 100 µs | pure disk |
লক্ষ্য করুন: h=99% আর h=90%-এর ফারাক ১০× — hit rate linearly পড়ে, latency exponentially বাড়ে। আর read amplification = এক logical read-এর পেছনে physical read কয়টা — index+heap fetch, miss-chain, evict-then-re-read সব মিলিয়ে।
ভেতরে কী ঘটছে
CLOCK-এর এক lap, হাতে
Pool ৪ frame, সব ref=1, hand → A দিকে:
frames: [A=1] [B=1] [C=1] [D=1] hand→A
victim খোঁজা (page X load):
A: 1→0 advance B: 1→0 advance C: 1→0 advance D: 1→0 advance
A: already 0 → EVICT A, X বসে ref=1, hand→B
ফল: পুরনোদের সবাই এক chance পেল, X-ও বাঁচল — exact-LRU-র আনুমানিক নকলInnoDB-র ভিন্ন ঔষধ — midpoint insertion
InnoDB-র ওষুধ: নতুন page MRU-end-এ না ঢুকে pool-এর মাঝখানে (new/old subchain সীমানায়) ঢোকে; old-zone-এ টিকে থাকলে তবেই পদোন্নতি — one-shot scan old-এই মরে। PG-ring-এর cousin: scan-কে hot-এলাকা থেকে দূরে রাখা।
mmap debate — এক screen-এ পুরো ঝগড়া
ভাবনা: OS pagecache-ই তো cache — file mmap করলেই তো pool লেখা লাগবে না! পক্ষে: কোড প্রায় শূন্য, memory management OS-এর; LMDB বাঁচিয়ে-বুঝিয়ে চলে। বিপক্ষে: eviction control নেই (kernel pin জানে না), write-back ordering WAL- এর সাথে খাপায় না, disk-খারাপ হলে SIGBUS, TLB-shootdown cost — আর double buffering তবু যায় না। PostgreSQL mmap ছেড়ে dynahash pool-এ গেছে; SQLite-ও mmap mode-এ caveat লেখা রাখে। মোদ্দা: correctness জড়ালে control > convenience; embedded/readonly-তে mmap এখনো রাজা。
উদাহরণ
Cold vs warm — একই query, দুই জগৎ
ধরুন orders table ২ GB, index-সহ point lookup। প্রথমবার (cold):
Index Scan using orders_pkey ...
Buffers: shared hit=3 read=4 ← ৪ page disk থেকে (cold)
Execution Time: 12.4 ms ← SSD random reads dominateদ্বিতীয়বার (warm — pool-এ সব আছে):
Index Scan using orders_pkey ...
Buffers: shared hit=7 read=0 ← সব pool-এ!
Execution Time: 0.08 ms ← ১৫৫× দ্রুতBuffers line-টাই আজকের লেসনের scoreboard: hit = buffer pool-
এ পাওয়া, read = OS/disk থেকে আনা। Production tuning-এর প্রথম
প্রশ্নই — গরম query-র জন্য read কি ০-এর কাছে?
pg_buffercache — pool-এর ভেতরটা live
CREATE EXTENSION pg_buffercache;
SELECT c.relname, count(*) AS buffers,
round(100.0*count(*)/sum(count(*)) OVER (), 1) AS pct
FROM pg_buffercache b JOIN pg_class c ON b.relfilenode = pg_relation_filenode(c)
GROUP BY c.relname ORDER BY buffers DESC LIMIT 6; relname | buffers | pct
--------------+---------+------
events | 61140 | 46.7
orders | 24388 | 18.6
users_pkey | 921 | 0.7
idx_events_ts| 15302 | 11.7এক নজরে দেখা যায় কে pool দখল করে বসেছে — আর vacuum-এর পরে কোন table-এর page-রা ‘ভুলে যাওয়া’ উচিত ছিল।
ছবিতে
┌────────────────────────────────────────┐
SELECT ──► parse/plan ──► "page 42 দাও" ──► BUFFER MANAGER
│
hit ◄── hash lookup: 42 আছে? ──► miss
│ │
▼ ▼
return frame pin_count[42]==0 frames?
│ yes │ no
▼ ▼
pick victim (policy) wait/queue
│
clean? ──yes──► load page 42 into frame
│no (OS read → memcpy)
▼
flush dirty to disk first
└──────► load, returnLRU (list): newest ⇄ [E][D][C][B][A] ⇄ oldest ← hit = move to newest
CLOCK (circle): ┌─►[A r=1]─[B r=0]◄─ victim!
[F r=1] [C r=1] ← hit = set bit
└──[E r=0]──[D r=1]──┘ hand ঘোরেই খোঁজে
PG ring (scan): main pool: [hot hot hot hot ...]
ring(৩২): [s1 s2 ... s32] ↺ ← scan page রা নিজেদের মধ্যেই মরেনিজে চালিয়ে দেখুন
pg_buffercache দেখা, pg_prewarm করা, আর cold/warm timing-এর বিশাল ফারাক
ধাপ ০ — server লাগবে: docker run --rm -d --name pg-lab -e POSTGRES_PASSWORD=lab -p 5432:5432 postgres:16,
তারপর docker exec -it pg-lab psql -U postgres।
ধাপ ১ — মাংসের dataset:
CREATE TABLE events AS
SELECT g AS id, (g % 10000)::int AS uid, md5(g::text) AS payload,
g::timestamp + interval '1 hour' AS ts
FROM generate_series(1, 2000000) g;
CREATE INDEX ON events (uid);
ANALYZE events;ধাপ ২ — cold ছোঁয়া: pool থেকে table বের করতে restart সবচেয়ে পরিষ্কার:
docker restart pg-lab (অথবা অন্য বড় table scan করে pool ধুয়ে ফেলুন)। তারপর:
\timing on
SELECT count(*) FROM events WHERE uid = 42; -- প্রথমবার: cold
SELECT count(*) FROM events WHERE uid = 42; -- দ্বিতীয়বার: warmপ্রত্যাশিত: প্রথমবার ~৫–১৫ ms, দ্বিতীয়বার ~০.১–০.৫ ms — ৩০–১০০×।
ধাপ ৩ — counter চোখে:
EXPLAIN (ANALYZE, BUFFERS) SELECT count(*) FROM events WHERE uid = 42;Buffers: shared hit=N read=M — warm run-এ M≈0, cold run-এ M>0। এই
M-ই ধাপ ২-এর latency-র পুরো ব্যাখ্যা।
ধাপ ৪ — pool-এ কে বসে আছে + ইচ্ছে করে গরম করা:
CREATE EXTENSION IF NOT EXISTS pg_buffercache;
CREATE EXTENSION IF NOT EXISTS pg_prewarm;
SELECT c.relname, count(*) FROM pg_buffercache b
JOIN pg_class c ON b.relfilenode = pg_relation_filenode(c)
GROUP BY 1 ORDER BY 2 DESC LIMIT 5;
SELECT pg_prewarm('events'); -- পুরো table pool-এ ঢোকানোর নির্দেশRestart-এর পরে pg_prewarm('events') চালিয়ে আবার uid query — cold
penalty আর নেই। Startup-এ critical table prewarm-ই production-এর
classic trick।
দেখা গেল কী: performance-এর পার্থক্য CPU-র না — পুরোটাই ‘page RAM-এ ছিল কি না’-র গল্প। Hit-rate-এর এক-দশমিক পয়েন্টই p99 বদলে দেয়।
একই query warm pool-এ শতগুণ দ্রুত — Buffers hit/read counter আর \timing output-এ hit-rate math-এর ভবিষ্যদ্বাণী হুবহু মিলে যায়।
নিজে বানান
BufferPool simulator — LRU vs CLOCK vs PG-style ring, scan-pollution প্রমাণসহ
- fixed frames array + page→frame dict — আসল manager-এর কাঠামো
- LRU: OrderedDict; CLOCK: ref bits + ঘোরতে থাকা hand
- workload: hot phase → ৩× pool-এর isolated scan → resume — চক্রে ৫ বার
- resume-পরবর্তী প্রথম ৩০০০ hot read-এ miss গুনুন — pollution-এর জ্বালাময়ী প্রমাণ
- clock+ring variant: scan page-রা ছোট private ring-এ, মূল pool অস্পৃশ্য
লক্ষ্য: policy-দের মতভেদ মুখস্থ না করে দেখা — একই workload-এ তিন manager চালিয়ে hit-rate আর resume-storm পাশাপাশি:
import random
from collections import OrderedDict
HOT_KEYS = 1_500 # app-এর hot working set
def workload(pool_size, cycles=5, hot_ops=15_000, seed=7):
"""app চলছে (hot random) → nightly-batch style একান্ত বিশাল scan (৩× pool)
→ app আবার শুরু — বাস্তব production-এর ছন্দ"""
rng = random.Random(seed)
for _ in range(cycles):
for _ in range(hot_ops):
yield ('hot', rng.randrange(HOT_KEYS))
for j in range(pool_size * 3):
yield ('scan', j)
class BufferPool:
def __init__(self, nframes, policy, ring=0):
self.n, self.policy = nframes, policy # 'lru' | 'clock'
self.ring_cap = ring # >0 = scan-bypass ring (PG-style)
self.ring_buf, self.ring_i = [], 0
self.page_of = [None] * nframes # frame -> page
self.slot = {} # page -> frame
self.lru = OrderedDict()
self.ref = [0] * nframes # CLOCK reference bits
self.hand = 0
self.hits = self.misses = self.storm = 0
self.storming = False
def access(self, page, kind='hot'):
if self.ring_cap and kind == 'scan':
self._ring_access(page) # sweep মূল pool ছোঁয় না!
return
if page in self.slot:
self.hits += 1
f = self.slot[page]
if self.policy == 'lru':
self.lru.move_to_end(page)
else:
self.ref[f] = 1
else:
self.misses += 1
if self.storming:
self.storm += 1
self._load(page)
def _ring_access(self, page):
if page in self.slot or page in self.ring_buf:
self.hits += 1
return
self.misses += 1
if len(self.ring_buf) < self.ring_cap:
self.ring_buf.append(page)
else:
self.ring_buf[self.ring_i] = page # round-robin overwrite
self.ring_i = (self.ring_i + 1) % self.ring_cap
def _load(self, page):
f = next((i for i in range(self.n) if self.page_of[i] is None), None)
if f is None:
f = self._evict()
self.page_of[f] = page
self.slot[page] = f
self.ref[f] = 1 # নতুন page ref-bit-সহ জন্মায়
if self.policy == 'lru':
self.lru[page] = None
def _evict(self):
if self.policy == 'lru':
victim, _ = self.lru.popitem(last=False)
f = self.slot.pop(victim)
self.page_of[f] = None
return f
while True: # CLOCK sweep
p = self.page_of[self.hand]
if self.ref[self.hand]:
self.ref[self.hand] = 0 # second chance!
self.hand = (self.hand + 1) % self.n
else:
f = self.hand
self.slot.pop(p)
self.page_of[f] = None
self.hand = (self.hand + 1) % self.n
return f
def rate(self):
return self.hits / max(1, self.hits + self.misses) * 100
def run(policy, ring=0, pool_size=2_000):
pool = BufferPool(pool_size, policy, ring)
storm_left, just_scanned, warm = 0, False, False
for kind, key in workload(pool_size):
if kind == 'scan':
just_scanned = True
else:
if just_scanned: # scan সবে শেষ → app resume
just_scanned = False
if warm: # প্রথম cycle warm-up বাদ
storm_left = 3_000
if storm_left > 0:
pool.storming = True
storm_left -= 1
if storm_left == 0:
pool.storming = False
warm = True
pool.access(key, kind)
return pool
if __name__ == '__main__':
POOL = 2_000
print(f'{POOL} frames; 5 cycles: 15k hot reads -> scan {POOL*3} pages')
print('policy overall resume-storm')
for label, args in (('lru ', ('lru', 0)),
('clock ', ('clock', 0)),
('clock+ring', ('clock', 32))):
p = run(*args)
print(f'{label} {p.rate():5.1f}% {p.storm}')প্রত্যাশিত output:
2000 frames; 5 cycles: 15k hot reads -> scan 6000 pages
policy overall resume-storm
lru 71.4% 5185
clock 71.4% 5185
clock+ring 77.1% 0পড়ার নিয়ম তিনটা লাইনে: (১) LRU আর bare CLOCK হুবহু একই ভেঙে পড়ে — ৩× pool-এর scan second-chance-কেও গিলে ফেলে; (২) resume-storm ৫১৮৫ মানে প্রতিটা scan-এর পরে hot set (~১৫০০ page × ৪ cycle) পুরো আবার-পড়া; (৩) ring-এ storm শূন্য — scan-কে isolate করাই আসল scan resistance, policy নিজে নয়। PostgreSQL-এর সিদ্ধান্তের নকল।
নিজে বাড়ান: (১) pin/unpin + dirty flag যোগ করে victim-flush cost মডেল করুন (write-back হিসাব); (২) InnoDB midpoint insertion variant — নতুন page-কে half-way-তে ঢোকান; (৩) hot_ratio আর scan_len sweep করে কোথায় boundary তা heat-map করুন।
বাস্তব সিস্টেমে
Production-এ buffer pool
- PostgreSQL:
shared_buffers(default 128 MB; RAM ~২৫% common পরামর্শ); bulk ops নিজে থেকেই ring নেয় (vacuum_buffer_usage_limit); pg_buffercache/pg_prewarm built-in। - InnoDB:
innodb_buffer_pool_size= tuning-এর প্রথম dial; old-zone ratioinnodb_old_blocks_pct; restart-time pool dump/load = pg_prewarm-এর ভাই। - Oracle: KEEP/RECYCLE/DEFAULT pools — segment-per-pool = isolation-এর manual মোড।
- SQL Server: buffer pool + plan cache এক জায়গা ভাগ করে — pressure-রাজনীতি অন্য মাত্রায়।
Incident pattern: রাতের batch-র forgotten full scan সকালে app-কে ঠান্ডা pool রেখে যায় — ৯টার p99 spike-এর অনেক postmortem এক গল্প। সমাধান এক: batch isolation (ring/separate pool) বা off-peak।
যে ভুলগুলো সবাই করে
“OS pagecache আছে তো — database-এর নিজের cache টাকার অপচয়।”
খরচ সত্যি, তবু উত্তর ‘বাদ’ নয়: eviction timing, pinning, WAL-ordered write-back — এই control generic pagecache দেয় না; O_DIRECT-বাড়িরাও নিজের pool-ই চালায়। mmap-only পথ readonly/embedded-তেই সীমাবদ্ধ。
“LRU-ই সেরা policy — textbook-এ সবাই তাই পড়ায়।”
Textbook theory, production scan-flood: ৩× pool scan LRU-কে গা-ঢাকসা করে। বলেই কোনো বড় engine exact-LRU চালায় না — CLOCK, midpoint, multi-pool, ring — সবাই recency ভাঙে।
“Dirty page মানে এখনই disk-এ লিখতে হবে, নইলে data হারাবে।”
Crash-এ হারাবে ঠিক, কিন্তু রক্ষা করে WAL: log record durable হলেই committed; data-page flush দেরিতেও নিরাপদ। Background writer ইচ্ছেমতো দেরি করে — তাড়াহুড়ো flush উল্টো throughput খায়। (পূর্ণ গল্প WAL লেসনে।)
“Buffer pool যত বড় তত ভালো — সব RAM দিয়ে দাও।”
Working-set-চেয়ে বড় pool-এর লাভ flat; খরচ বাড়ে — checkpoint flush-storm, লম্বা warm-up, OS-cache-র জায়গা শেষ। Sizing মাপা হয় hit-rate curve-এ, আবেগে নয়।
বুঝেছেন কি না দেখুন
1Buffer manager-এর দুই-call contract আর দুইটা মূল metadata — নাম করুন।
স্মরণ
pin(page) → frame আর unpin(frame, dirty?); metadata: প্রতি frame-এ
pin count (eviction-যোগ্যতা) আর dirty flag (eviction-এ write লাগবে কি
না)। Pool full + সব pinned = waiter queue।
2Double buffering-এর ঠিক কী খরচ? O_DIRECT কোন সমস্যাটা সমাধান করে, কোনটা নয়?
যুক্তি
খরচ: একই page kernel-cache + pool-এ দুইবার — RAM দ্বিগুণ, দুই eviction- system একে অপরের বিরুদ্ধে। O_DIRECT kernel-cache skip করে duplication সরায়; কিন্তু prefetch/admission/eviction সব engine-এর দায় — ওই জটিলতা কোথাও যায় না, ঘরেই থাকে।
3t_mem = 100 ns, t_SSD = 100 µs। h=99% বনাম h=90% average read time?
কেন এই nonlinearity production-এ ভয়ংকর?
প্রয়োগ
h=99%: 0.99×100ns + 0.01×100µs ≈ 1.1 µs; h=90%: 0.9×100ns + 0.1×100µs ≈ 10 µs — ৯% hit-drop = ~৯× latency। Nonlinear কারণ miss-দাম hit-দামের হাজার গুণ; p99 tail-এ miss-cluster জমে আরও খারাপ।
4Bare CLOCK ৩× pool-এর scan ঠেকাতে পারে না কেন — ref-bit mechanics-এ ধরে
ব্যাখ্যা করুন। তাহলে CLOCK লেখাই/তো লাগে কেন?
যুক্তি
Scan page-রাও bit=1 নিয়ে ঢোকে; hand-এর প্রথম pass-এ সবার bit clear হয়, দ্বিতীয় pass-এ eviction — ৩× load মানে ≥২ lap সহজেই, hot page-ও শেষে victim। CLOCK-এর লাভ exact-order maintenance না করে LRU-approximation — কম lock/overhead; scan-resistance আলাদা mechanism (ring/isolation)-এ আসে।
5PostgreSQL ring buffer আর InnoDB midpoint insertion — দুটোই scan-resistant,
কিন্তু কৌশলে কোথায় ভিন্ন? কোনটা কবে বেশি সুবিধা দেয়?
যুক্তি
Ring: scan-কে pool থেকে বাইরে ছোট private space-এ বন্দি — মূল pool zero-touch। Midpoint: scan page pool-এ ঢোকে, কিন্তু old-zone-এ — promote হতে হলে দ্বিতীয়বার দর্শক লাগে। Ring বেশি রক্ষক যখন scan বিশাল ও one-shot; midpoint ভালো যখন scan-data-র পুনর্ব্যবহার সম্ভাবনা থাকে (তাকে new-zone-এ ওঠার সুযোগই দেয়)।
6আপনার service: OLTP API (working set ২ GB) + রাত ২টায় ৮০ GB reporting
scan, RAM ৩২ GB। shared_buffers sizing + scan handling — design করুন।
ডিজাইন
shared_buffers ~৮ GB (২ GB working set-এর ৪× headroom, বাকিটা OS/other-
এর জন্য)। Reporting-কে dedicated role/connection-এ চালান — PG নিজেই
bulk scan ring-এ পাঠায়; একদম নিশ্চিত হতে রাতের job-এর পরে critical table
pg_prewarm, বা আলাদা replica-তে reporting সরান। Monitor: pg_stat_io /
pg_buffercache-তে hot tables-এর steady residency।
এরপর কী
এরপর কী
Pool গরম, page ভাড়ায় বসা — কিন্তু lookup-এর উত্তর এখনো হয় পুরো table scan। এবার সেই পুরনো বন্ধু disk-জগতের পোশাকে: B+ tree index। পরের লেসনে:
- PostgreSQL nbtree-র পেজ-চিড়িয়া: metapage, root, internal, leaf chain
- High key, deduplication, ctid — index entry আসলে কী ধরে রাখে
- Index scan vs index-only scan — visibility map-এর গোপন ভূমিকা
- প্রতিটা INSERT-এর ওপর index-র খাজনা, delete-এ bloat — আর EXPLAIN (ANALYZE, BUFFERS)-এ সব চোখে দেখা
গত লেসনের hit=7 read=0 দেখেছেন? পরের লেসনে সেই ৭ page-এর ভেতরে ঠিক কী ঘটে, step by step।
আরও পড়ুন
- PostgreSQL Documentation — Buffer Manager / shared_buffers — The PostgreSQL Global Development Group · Ring buffer, bulk-read strategy, pg_buffercache — production-এর প্রামাণ্য বর্ণনা
- Database Internals — Chapter 8: Buffer Pool (Alex Petrov) — Alex Petrov · Buffer manager API, eviction policy-র taxonomy — এই লেসনের মেরুদণ্ড
- SQLite mmap debate — '30% faster than mmap' ও related mailing-list threads — SQLite developers · mmap-as-cache-এর বাস্তব পাল্টা-যুক্তি — realworld section-এর উৎস