MVCC — PostgreSQL-এর বাস্তব implementation
MVCC in PostgreSQL
Readers-never-block-writers-এর ইঞ্জিন: xmin/xmax versioning, snapshot horizon, clog, dead tuples, HOT update, VACUUM-অপরিহার্যতা আর long-transaction-এর বিষ — PostgreSQL concurrency-র হৃৎপিণ্ড খোলা।
আগে এটা বুঝি
Locking লেসনে lock-শৃঙ্খলা দেখলাম। কিন্তু একটা অদ্ভুত ঘটনা খেয়াল
করেছেন? বাস্তব PostgreSQL-এ plain SELECT — কোটি row-র টেবিলেও —
কোনো lock চায় না, কাউকে block করে না, পেছনে দাঁড়ায় না। Readers
never block writers, writers never block readers — এই promise সম্ভব
যখন দুইজন একই row-তে হাজারবার হাত দিচ্ছে?
উত্তর: MVCC — Multi-Version Concurrency Control। Idea সরল-বিস্ময়কর: conflict ঘটে কারণ সবাই একটাই copy নিয়ে লড়ছে; তাহলে প্রতিটা change- এ নতুন version, পুরনোটা ফেলো না — reader-রা নিজেদের মুহূর্তের ছবি থেকে পড়ে, writer-রা version যোগ করে, কারো পথ ছেদে না। Stonebraker-র ১৯৮৭ Postgres paper ডাকত “no-overwrite storage”।
কিন্তু উপহারের বিল আসে: নতুন version বানালে পুরনোগুলো জমে — আর ময়লা সরাতে স্থায়ী কর্মচারী লাগে: VACUUM। কেউ tx খুলে ঘুমালে পুরো database-এর ময়লা-সংগ্রহ আটকে যায়। আজ promise, mechanism, আর দাম — পুরোটা।
মূল ধারণা
Tuple header — জন্ম-মৃত্যুর দুই stamp
Storage লেসনের <Term id="tuple">tuple</Term> = header + data; header-
এর নায়ক দুই field:
| Field | মানে |
|---|---|
| xmin | যে transaction-id version-টা জন্ম দিল (INSERT / UPDATE-নতুন-পাশ) |
| xmax | যে tx মৃত্যু ঘোষণা করল (DELETE / UPDATE-পুরনো-পাশ); 0 = জীবিত |
মূল নিয়ম:
UPDATE = in-place বদল নয়।
= পুরনো version-এ xmax=আমার-xid + নতুন version (xmin=আমার-xid)
DELETE = শুধু xmax ছাপা — কিছুই মোছা হয় না!
INSERT = নতুন version, xmin=আমার-xid, xmax=0Data overwrite হয় না — history জমা হয়। তাহলে SELECT ঠিক কোন version দেখবে?
Visibility function — চার-শর্তের রায়
Reader আসে xmin/xmax-জগতে তার snapshot নিয়ে
— isolation লেসনের packet: {horizon, running}। Version দৃশ্যমান যদি —
- জন্ম-শর্ত: xmin committed এবং horizon-নিচে (বা আমারই কাজ);
- মৃত্যু-শর্ত: xmax=0, বা xmax-ধারী abort, বা xmax committed-but- snapshot-পরে (আমার ছবিতে মৃত্যুটা এখনো ঘটেনি)।
দুইটা মিললেই দেখা যাবে; না মিললে version disk-এ থাকলেও আপনার query- জগতে অস্তিত্বহীন।
clog — “ও কি commit করেছিল?”
Visibility-তে বারবার লাগে প্রশ্ন: “xid 887 committed?” উত্তর থাকে clog (pg_xact)-এ — per-xid দুই bit (in-progress/committed/aborted)। Hot অংশ shared memory-cache-এ; আর hint bits প্রথম check-এর উত্তর tuple-header-তেই ছাপে — পরের reader clog ছোঁয়ই না।
Version-জন্মের তিন দৃশ্য
INSERT t=1: [v1: xmin=100, xmax=0] ← একটা tuple
DELETE t=2: [v1: xmin=100, xmax=101] ← stamp-ই মৃত্যু
UPDATE t=3: [v1: xmin=100, xmax=102] [v2: xmin=102, xmax=0]
(DELETE+INSERT এক atom-এ)ফল: প্রতিটা UPDATE বাড়তি version রেখে যায়। যে version কোনো সম্ভাব্য snapshot-এ আর দেখা যাবে না — dead tuple, ময়লা; স্তূপই bloat।
heap page(s):
┌───────────────────────────┐ পুরনো → নতুন
│ v4: xmin=105 xmax=0 ◄───┼─ live head (fresh reader এটা দেখে)
│ v3: xmin=104 xmax=105 │
│ v2: xmin=103 xmax=104 │ ← dead tuples (VACUUM-খাবার)
│ v1: xmin=100 xmax=103 │
└───────────────────────────┘HOT update — index রক্ষা পাওয়া
Version-chain-এর ব্যয়বহুল দিক index: নতুন version নতুন ctid-তে, তাই
সব index-এ নতুন entry — ৫ index মানে ৫ index-write। ঔষধ HOT
(Heap-Only Tuple): দুই শর্তে engine নতুন version same page-এ রেখে
index-write skip করে — (১) updated column indexed না, (২) page-এ free
space আছে। Index তখন পুরনো ctid-তেই আঙুল রাখে; chain page-ভেতরে ঘোরে।
Fillfactor আর n_hot_upd metric production-রোজকার।
Snapshot timing — mechanism-সহ আরেকবার
RC: statement-শুরুতে নতুন snapshot — মাঝপথে commit হওয়া change পরের
statement-এ হাজির। RR/SER: প্রথম statement-এ একবার — দুনিয়া জুড়ে
জমে পুরনো ছবি। আর সূক্ষ্মতা: RR-এ already-updated-by-other row লিখতে
গেলে optimistic rejection — could not serialize access due to concurrent update; snapshot পড়তে দেয়, দ্বিতীয়বার লিখতে দেয় না।
Long-running transaction — একজনের অলসতা, সবার অনুশোচনা
Snapshot-horizon এগোয় না যদি কেউ পুরনো ছবি ধরে থাকে:
- Bloat: VACUUM ময়লা সরাতে পারে না (“idle tx-এর ছবিতে হয়তো এই dead tuple এখনো দেখা যায়”) — টেবিল/index স্ফীত, slow queries।
- Vacuum age: xid মাত্র ৩২ bit — ~৪২৯ কোটির পরে গুণতা ঘুরে
যায় (wraparound); তার আগে VACUUM-কে পুরনো row-তে frozen-stamp
বসাতে হয়। Idle tx থাকলে age বাড়তেই থাকে — শেষ সীমায় PG নিজেই
সব লেখা বন্ধ করে:
database is not accepting commands to avoid wraparound data loss— production-সবচেয়ে-ভয়ংকর অলার্ট।
InnoDB কেন ভিন্ন পথে
InnoDB-ও MVCC করে, উল্টো পথে: পুরনো version heap-এ নয় — undo log- এ; reader undo replay করে ছবি reconstruct করে।
| PostgreSQL (versions in heap) | InnoDB (versions in undo log) | |
|---|---|---|
| Read-path | tuple + header check | tuple + দরকারে undo-replay |
| পুরনো version-দের জায়গা | টেবিল/index-ভেতরেই (bloat) | আলাদা undo tablespace |
| Rollback | প্রায় বিনামূল্যে (status flip) | undo apply — লম্বা tx-rollback ধীর |
| ময়লা-দর্শন | VACUUM ঝাড়বেই | purge thread truncate করে |
PG-র rollback-free আর InnoDB-র rollback-cost — এই এক ঘরেই দুই design- দর্শন: কে কার খরচ আগে দেবে।
ভেতরে কী ঘটছে
একটা tuple-এর বিচার — SELECT-এর ভেতরে
- index/scan → candidatectid দিয়ে heap-chain-এর কোনো নোড
- hint bit? না হলে clog lookupxmin/xmax status — first-check-এ hint ছাপা
- xmin বিচারcommitted AND visible-to-my-snapshot?
- xmax বিচারমৃত্যু-stamp আমার ছবিতে এখনো কার্যকর?
- WHERE filter → clientপাশে কয়টা version বাদ গেল — জানবেনই না
দুই পরিণাম: read-path-এ কোনো lock নেই — readers/writers একে অপরকে ছোঁয় না; আর scan যত version পার করে তত কাজ — dead-tuple-স্তূপ মানেই slower seq-scan, এজন্যই bloat performance-বিষ।
Wraparound-এর গণিত
xid ৩২-bit unsigned — ঘুরে গেলে পুরনো xid “ভবিষ্যৎ” হয়, visibility logic
উল্টে প্রাচীন row হঠাৎ অদৃশ্য। প্রতিরক্ষা: frozen rows (ageless) +
autovacuum aggressive freezing (~২০০ কোটি age-এ); শেষ প্রতিরক্ষা hard-stop
— লেখা বন্ধ, data-loss নয়। এজন্যই age(datfrozenxid) monitoring DBA-
সাপ্তাহিক অভ্যাস।
উদাহরণ
পূর্ণ trace — এক row-এর জীবন, দুই reader-এর চোখে
সময় Session A Session B storage
──── ───────────────────────────────── ────────────── ──────────────────
t0 [v1: xmin=50, xmax=0]
t1 BEGIN REPEATABLE READ;
snap S_A {horizon: 60}
t2 BEGIN;
UPDATE ... val=200;
[v1:xmax=80][v2:xmin=80]
t3 SELECT val; -- 100 ! v1-বিচার(A):
(uncommitted) xmin=50 ✓;
xmax=80 pending
→ মৃত্যু আমার
ছবিতে নেই
t4 COMMIT;
t5 SELECT val; -- 100 !! v1: xmax=80
committed, কিন্তু
> horizon(60)
t6 COMMIT;
t7 (নতুন reader) v1 dead, v2 দেখেt5-এর “100”-ই MVCC-হৃদয়স্পর্শী মুহূর্ত: commit হয়ে গেছে, তবু A দেখে না — A-র snapshot-ছবিতে v1 জীবিত, v2 অস্তিত্বহীন। একই mechanism দুই দান: A-কে consistency (RR), B-কে non-blocking write (A lock-ই ধরে না)। আর t6-র পরে দামটা: v1 এখন dead — disk-এ বসে আছে, VACUUM- অপেক্ষায়। প্রতিটা গল্প ময়লা জমায়; অর্থনীতির ব্যয় এখানেই।
ছবিতে
Version-chain আর দুই snapshot-এর দুই ছবি
chain: v1(xmin=10,xmax=30) → v2(xmin=30,xmax=55) → v3(xmin=55,xmax=0)
RC reader: stmt@t40 horizon=40 → v2 দেখে
stmt@t60 horizon=60 → v3 দেখে ← মাঝপথে বদলাল!
RR reader: horizon=40 fixed → সব stmt-এ v2-ই (v3 তার ছবির ভবিষ্যৎ)
DEAD ZONE: v1 (সবার horizon পেরিয়েছে) → VACUUM-eligibleনিজে চালিয়ে দেখুন
xmin/xmax নিজের চোখে, dead-tuple growth meter, idle-tx-এর বিষ
Setup: docker run --rm -d --name pg-lab -e POSTGRES_PASSWORD=lab -p 5432:5432 postgres:16;
দুই terminal-এ docker exec -it pg-lab psql -U postgres; Terminal A:
CREATE TABLE items (id int PRIMARY KEY, val int);
INSERT INTO items VALUES (1, 100);অংশ ১ — xmin/xmax দেখা:
Terminal A Terminal B
───────────────────────────────────── ──────────────────────────
SELECT xmin, xmax, val FROM items;
-- xmin | xmax | val
-- ------+------+-----
-- 752 | 0 | 100 ← জন্ম-id, জীবিত
BEGIN; UPDATE items SET val=200 WHERE id=1;
SELECT xmin, xmax, val FROM items;
-- পুরনো v: xmax=753 (pending!)
-- নতুন v: xmin=753 (invisible)
COMMIT;
SELECT xmin, xmax FROM items;
-- v1 হারিয়েছে, v2: xmin=753
SELECT xmin, xmax FROM items; -- A-ও এখন v2 দেখেSELECT xmin,* — hidden header আপনার SQL-এর ভেতরে।
অংশ ২ — dead-tuple জন্ম আর VACUUM-ভোজ:
CREATE TABLE churn (id serial PRIMARY KEY, val int);
INSERT INTO churn SELECT g, g FROM generate_series(1,10000) g;
UPDATE churn SET val = val + 1; -- ১০,০০০ dead জন্মাল
SELECT n_live_tup, n_dead_tup FROM pg_stat_user_tables
WHERE relname='churn';
-- n_dead_tup ≈ 10000 — টেবিল দৈর্ঘ্যে দ্বিগুণ!
VACUUM churn;
SELECT n_dead_tup FROM pg_stat_user_tables WHERE relname='churn';
-- ≈ 0 — জায়গা reuse-যোগ্য (OS-কে ফেরত নয় — সেটা VACUUM FULL-গল্প)আরও নাটকীয়: UPDATE ×৫ চালিয়ে dead-count ~৫০,০০০ আর
pg_total_relation_size('churn') স্ফীতি দেখুন।
অংশ ৩ — idle transaction বিষ দেখানো:
Terminal A Terminal B
───────────────────────────────────── ──────────────────────────
BEGIN;
SELECT txid_current(); -- ধরুন 800
UPDATE churn SET val=val+1;
UPDATE churn SET val=val+1;
VACUUM churn;
SELECT age(relfrozenxid)
FROM pg_class
WHERE relname='churn';
-- age কমছে না!
ROLLBACK; -- বিষ সরল — এবার vacuum freeদুই terminal-এ পাশাপাশি SELECT age(relfrozenxid) FROM pg_class WHERE relname='churn'; চালিয়ে দেখুন: A-র tx খোলা অবস্থায় UPDATE+VACUUM-এও
age আটকে, ROLLBACK-এর পরে কমছে। এই এক সংখ্যাই wraparound-alarm-এর
মূল signal।
Tuple-জন্ম-মৃত্যু stamp সরাসরি দেখা যায়, UPDATE-loop n_dead_tup স্ফীত করে আর VACUUM গিলে ফেলে, আর একটা idle tx vacuum-age আটকে রাখে — promise আর দাম দুই-ই live।
নিজে বানান
MVCC engine — snapshots, visibility function, versioned UPDATE, GC
- Row model: প্রতি key list-of-versions, প্রতিটা (xmin, xmax, value)
- begin() নতুন xid + snapshot {horizon, running}; commit/abort status আপডেট
- visible(v, snap): জন্ম-শর্ত AND মৃত্যু-শর্ত — লেসনের নিয়ম হুবহু
- update(key, value): old-এ xmax stamp + new version append
- gc(): যে version কোনো live/future snapshot দেখবে না — মুছে দিন
লক্ষ্য: PG-visibility-র core — snapshot + version-list + GC rule — scripted scenario-তে verify।
from collections import defaultdict
class Engine:
def __init__(self):
self.rows = defaultdict(list) # key -> [[xmin, xmax, value], ...]
self.status = {} # xid -> 'c'(ommitted)/'a'(borted)
self.next_xid = 1
self.snapshots = {}
def begin(self):
xid = self.next_xid; self.next_xid += 1
self.snapshots[xid] = {
'me': xid, 'horizon': xid,
'running': {s for s, _ in self.snapshots.items()
if s not in self.status},
}
return xid
def _st(self, xid): return self.status.get(xid, 'p') # p=pending
def commit(self, x): self.status[x]='c'; self._forget(x)
def abort(self, x): self.status[x]='a'; self._forget(x)
def _forget(self, x): self.snapshots.pop(x, None)
def insert(self, tx, key, value):
self.rows[key].append([tx, 0, value])
def read(self, tx, key):
for xmin, xmax, value in reversed(self.rows[key]):
if self.visible(xmin, xmax, self.snapshots[tx]):
return value
return None
def update(self, tx, key, value):
assert self.read(tx, key) is not None, "key invisible!"
for v in self.rows[key]:
if self.visible(v[0], v[1], self.snapshots[tx]):
v[1] = tx # old: মৃত্যু-stamp
self.rows[key].append([tx, 0, value]) # new version
# ---------- the heart ----------
def visible(self, xmin, xmax, snap):
born_ok = (xmin == snap['me']) or (
self._st(xmin)=='c' and xmin <= snap['horizon']
and xmin not in snap['running'])
if not born_ok: return False
if xmax == 0: return True
gone_ok = (self._st(xmax)!='c') or (xmax==snap['me']) or \
(xmax>snap['horizon']) or (xmax in snap['running'])
return gone_ok # মৃত্যু আমার ছবিতে এখনো নেই
# ---------- garbage collection ----------
def gc(self):
removed = 0
for key in list(self.rows):
keep = []
for v in self.rows[key]:
needed = any(self.visible(*v[:2], s)
for s in self._live_snaps()) or \
self.visible(v[0], v[1],
{'me': -1, 'horizon': self.next_xid,
'running': set()})
if needed: keep.append(v)
else: removed += 1
self.rows[key] = keep
return removed
def _live_snaps(self):
return self.snapshots.values()
# ---------- scripted scenario ----------
e = Engine()
t1 = e.begin(); e.insert(t1, 'acct', 100) # T1: acct=100
e.commit(t1)
tR = e.begin() # T_R: লম্বা reader (RR)
t2 = e.begin(); e.update(t2, 'acct', 200) # T2: 100→200
e.commit(t2)
print("T_R sees:", e.read(tR, 'acct')) # 100 — snapshot-এ আটকে
print("new reader:", e.read(e.begin(), 'acct')) # 200 — fresh
print("GC removed:", e.gc()) # 0! T_R-এর জন্য old দরকার
e.commit(tR) # reader চলে গেল
print("after release:", e.gc()) # 1 — old version মৃত
t3 = e.begin()
print("post-GC read:", e.read(t3, 'acct')) # 200 — কিছুই হারায়নি
e.commit(t3)প্রত্যাশিত output:
T_R sees: 100
new reader: 200
GC removed: 0
after release: 1
post-GC read: 200তিন যাচাই একসাথে: (১) long-reader পুরনো ছবি — isolation; (২) GC তার
version রাখে — correctness; (৩) release-পরে ময়লা যায়, নতুন reader-
উত্তর অক্ষত — economy। এখন abort-path: e.abort(t2) দিলে পুরনো version-ই
জীবিত থাকত — atomicity-by-invisibility, transactions-লেসন-প্রতিশ্রুতির
engine-রূপ।
নিজে বাড়ান: (১) abort-scenario print করে প্রমাণ করুন aborted write সবার কাছে invisible; (২) future-reader-check ছাড়া gc() কী ভুল করবে — edge-case দেখান; (৩) RC-mode: read()-এ প্রতিবার fresh snapshot — ফল কী বদলায়; (৪) xid mod-256 circular-comparison — কোথায় ভাঙে।
বাস্তব সিস্টেমে
Autovacuum tuning — নীরব hero-র দুর্বলতা। Busy write-table-এ autovacuum
পিছিয়ে যায়, dead-tuple জমে, seq-scan ধীর। ঔষধ: per-table
autovacuum_vacuum_scale_factor কমানো (busy table-এ 0.01), naptime ছোট,
dead-ratio monitor — mechanism জানলে tuning জাদু-মন্ত্র নয়, হিসাব।
pg_stat_activity-র সন্দেহভাজন সারি। state='idle in transaction' +
পুরনো xact_start — production-প্রায়-সব bloat/wraparound incident-এর
অপরাধী-জাল; alert: “idle > ৫ min” — এই লেসন বলে alert-টা কেন জীবন-মরণ।
তিন engine, এক চুক্তি। Oracle rollback-segments, InnoDB undo logs, PG in-place versions — version-রাখার ভাণ্ডার তিনটা, চুক্তি একটাই: readers non-blocking। Design-interview-তে এই তুলনা implementation-সচেতনতা-প্রমাণ।
Git-মিল কাকতালীয় নয়। Commit = snapshot, history অক্ষত, GC deferred
(git gc ↔ VACUUM); Datomic/FoundationDB সরাসরি MVCC-lineage, Linux RCU
একই দর্শনের extreme — readers বিনামূল্যে, ময়লা-সরানো deferred।
যে ভুলগুলো সবাই করে
“MVCC-তে lock আর নেই — সব সমস্যার অবসান।”
Read-path lock নেই — সত্য; কিন্তু same-row write-vs-write conflict এখনো আছে (RC-অপেক্ষা, RR-error), আর table/advisory/SIREAD locks সব জায়গায়। MVCC read-contention সরায় — write-contention-এ locking-জ্ঞান তবু লাগবে।
“VACUUM টেবিল ছোট করে — disk ফেরত দেয়।”
Plain VACUUM dead-tuple-জায়গা reuse-যোগ্য করে — file-size কমে না, OS-কে কিছু ফেরত নয়। Shrink = VACUUM FULL/pg_repack — full rewrite + exclusive lock। তাই লক্ষ্য: bloat জমতে না দেওয়া, শেষে shrink নয়।
“UPDATE in-place হয় যদি value একই বা ছোট হয়।”
PG-তে কখনোই in-place update নেই — প্রতিটা UPDATE নতুন version, পুরনোটায় xmax (value-এক হলে planner কাজ skip করতে পারে, ঘটা update সবসময় version- ব্যবসা)। HOT শুধু index-write skip — heap-version তবু নতুন। “ছোট update = সস্তা” churn-table-bloat-incident-এর জনক।
“Long report-query ক্ষতিকর না — পড়ছে তো, লিখছে না।”
পড়া-ও বিষ যদি tx খোলা থাকে: snapshot-horizon পেছনে, ওই সীমার-নিচের dead-tuple VACUUM সরাতে পারে না — পুরো cluster-bloat। ঔষধ: chunked autocommit, সংক্ষিপ্ত RR-window, idle-in-trans-timeout। “Read-only is harmless” — MVCC-জগতের বিভ্রান্তিকতম half-truth।
বুঝেছেন কি না দেখুন
1xmin আর xmax কী record করে? DELETE-এ কোনটা বদলায়?
স্মরণ
xmin = creator-xid (জন্ম); xmax = deleter-xid (মৃত্যু-ঘোষণা), 0 = জীবিত। DELETE-এ শুধু xmax বদলায় — data যায় না; version থাকে, visibility হারায়।
2Visibility function-এর জন্ম-শর্ত আর মৃত্যু-শর্ত — এক বাক্য করে।
যুক্তি
জন্ম: xmin committed ও আমার snapshot-জগতে অতীত (horizon-নিচে/আমার)। মৃত্যু: xmax নেই, বা deleter abort/আমার ছবির ভবিষ্যৎ — মৃত্যুটা আমার ছবিতে এখনো ঘটেনি। দুই-ই true হলে দেখা যায়।
3Example-এ t4-এ B commit-এর পরেও t5-এ A (RR) 100 দেখল — কোন শর্তটা
সম্ভব করল?
প্রয়োগ
মৃত্যু-শর্ত: v1-এর xmax=80 committed, কিন্তু A-র horizon (60)-এর উপরে — A-র ছবিতে মৃত্যু “ভবিষ্যৎ”, v1 জীবিত। Snapshot-fixed property-ই RR- জাদু।
4HOT update-এর দুই শর্ত, আর indexed-column update কেন নিষিদ্ধ?
যুক্তি
শর্ত: indexed column untouched + নতুন version same-page-এ। Indexed column বদলালে নতুন key-value index-entry বাধ্যতামূলক — skip করলে index পুরনো মান দেখিয়ে ভুল উত্তর দেবে; তাই HOT অসম্ভব, সব-index-write।
5n_dead_tup=৪ লাখ, autovacuum চলেও কমছে না। প্রথম suspect, কোন query?
প্রয়োগ
Long/idle-in-transaction session যার horizon আটকে। Query:
SELECT pid,state,xact_start,query FROM pg_stat_activity WHERE xact_start < now()-interval '5 min' ORDER BY xact_start;
— পুরনোতম
xact-ই vacuum-প্রতিবন্ধক; finish/kill-এ vacuum ছুটবে।
6PG rollback-free বনাম InnoDB rollback-via-undo — heavy-long-transaction
workload-এ কোন trade-off-এ বিচার করবেন?
ডিজাইন
Abort-ফ্রিকোয়েন্সি দেখুন: বারবার-abort বড় batch (ETL retry) InnoDB- undo-replay-ব্যয়, PG-প্রায়-বিনামূল্যে। উল্টে update-churn + দীর্ঘ readers- এ PG-র in-heap versions bloat/VACUUM-pressure, InnoDB-আলাদা undo-space pressure ভালো ছড়ায়। সার: abort-cost বনাম vacuum-cost — কোন খরচ আপনার workload-ঘন।
এরপর কী
এরপর কী
MVCC-র promise আর দাম — দুই-ই এখন হাতে। কিন্তু গভীরতর প্রশ্ন ঝুলে: UPDATE-এর নতুন version disk-এ গেল কখন? Commit-মুহূর্তে বিদ্যুৎ গেলে half-written pages থেকে database কীভাবে ফিরবে? Transactions-লেসনে fsync- ইঙ্গিত পেয়েছিলেন — এবার পূর্ণ প্রকাশ।
পরের লেসনে — WAL ও ARIES recovery:
- Write-Ahead Logging-এর golden rule: log আগে, data-page পরে — crash- safety-একমাত্র-পথ কেন
- redo/undo, LSN, dirty-page queue — recovery-র তিন যন্ত্র
- Checkpoint: কখন থামবেন, কত পিছনে যেতে হবে
- ARIES-র repeat-history দর্শন আর PG-র বাস্তব রূপ
আজ দেখলাম version কীভাবে জমে; পরের লেসনে driving question-এর (“বিদ্যুৎ চলে গেলেও committed transaction টিকে থাকে কীভাবে?”) চূড়ান্ত উত্তর — প্রতিটা change-এর প্রমাণ আগে durable হওয়ার গল্প।
আরও পড়ুন
- PostgreSQL Documentation — Chapter 13: Concurrency Control / Chapter 73 Internals (Row Versioning Concepts) — The PostgreSQL Global Development Group · Visibility rules, clog, HOT, vacuum-age — এই লেসনের প্রতিটা দাবির প্রামাণ্য উৎস
- Database System Concepts — Chapter 15 (snapshot isolation section) — Abraham Silberschatz, Henry F. Korth, S. Sudarshan · Snapshot isolation-এর theory আর multi-version scheduling-এর পাঠ্যপুস্তক ভিত্তি
- The Design of the Postgres Storage System (VLDB 1987) — Michael Stonebraker, Lawrence A. Rowe, Michael Hirohama · Time-travel storage থেকে MVCC — no-overwrite philosophy-র জন্মস্থান