Foundationপ্রথম নীতি থেকে
LEVEL 8লেসন ১৩/১৯রিসার্চ-লেভেল১ ঘণ্টা ৫ মিনিট

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-র হৃৎপিণ্ড খোলা।

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

  • Tuple header-এর xmin/xmax আর snapshot-horizon মিলিয়ে visibility function-টা চার-শর্তে trace করে বলতে পারবেন — ঠিক কোন reader কোন version দেখবে, হুবহু তা-ই
  • INSERT, DELETE, UPDATE storage-এ ঠিক কী রেখে যায় — new-version-plus-old-xmax প্যাটার্ন থেকে dead tuple জন্মানোর মেকানিজম আঁকতে পারবেন
  • HOT update-এর index-bypass শর্ত (same page + indexed column untouched) মিলিয়ে বলতে পারবেন UPDATE কখন index-ও ভারী হবে, কখন না — শর্তগুলো মুখস্ত-নয়, যুক্তিতে পারবেন
  • Long-running transaction-এর দুই বিষ — bloat আর vacuum-age freeze — production symptom থেকে root cause ধরতে পারবেন, আর xid wraparound guard কেন সবচেয়ে ভয়ংকর অলার্ট তা বলতে পারবেন
  • PG-র new-version-in-place আর InnoDB-র undo-log পথের trade-off তুলনা করতে পারবেন, আর MVCC engine program-এ লিখে visibility rules যাচাই করতে পারবেন

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

আগে এটা বুঝি

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=0

Data overwrite হয় না — history জমা হয়। তাহলে SELECT ঠিক কোন version দেখবে?

Visibility function — চার-শর্তের রায়

Reader আসে xmin/xmax-জগতে তার snapshot নিয়ে — isolation লেসনের packet: {horizon, running}। Version দৃশ্যমান যদি —

  1. জন্ম-শর্ত: xmin committed এবং horizon-নিচে (বা আমারই কাজ);
  2. মৃত্যু-শর্ত: 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     │
 └───────────────────────────┘
'price' row-এর version-chain — চার UPDATE-র পরে heap। জীবিত head + জমা ইতিহাস; ctid chain পেছনের version দেখায়, index প্রতিটা version-এ entry (HOT না হলে)।

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 এগোয় না যদি কেউ পুরনো ছবি ধরে থাকে:

  1. Bloat: VACUUM ময়লা সরাতে পারে না (“idle tx-এর ছবিতে হয়তো এই dead tuple এখনো দেখা যায়”) — টেবিল/index স্ফীত, slow queries।
  2. 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-pathtuple + header checktuple + দরকারে 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-এর ভেতরে

candidate tuple থেকে return — visibility pipeline
  1. index/scan → candidatectid দিয়ে heap-chain-এর কোনো নোড
  2. hint bit? না হলে clog lookupxmin/xmax status — first-check-এ hint ছাপা
  3. xmin বিচারcommitted AND visible-to-my-snapshot?
  4. xmax বিচারমৃত্যু-stamp আমার ছবিতে এখনো কার্যকর?
  5. 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
একই heap, দুই reader, দুই বাস্তবতা — RC-reader প্রতি statement-এ নতুন horizon; RR-reader জন্ম-ছবিতে আটকে। নিচে dead-zone — শুধু VACUUM-ই মোছে।

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

EXPERIMENT

xmin/xmax নিজের চোখে, dead-tuple growth meter, idle-tx-এর বিষ

PostgreSQL 14+ (psql)· ২৫ মিনিট

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।

নিজে বানান

BUILD IT

MVCC engine — snapshots, visibility function, versioned UPDATE, GC

Python · ●●●●○
  1. Row model: প্রতি key list-of-versions, প্রতিটা (xmin, xmax, value)
  2. begin() নতুন xid + snapshot {horizon, running}; commit/abort status আপডেট
  3. visible(v, snap): জন্ম-শর্ত AND মৃত্যু-শর্ত — লেসনের নিয়ম হুবহু
  4. update(key, value): old-এ xmax stamp + new version append
  5. 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।

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

1

xmin আর xmax কী record করে? DELETE-এ কোনটা বদলায়?

স্মরণ

xmin = creator-xid (জন্ম); xmax = deleter-xid (মৃত্যু-ঘোষণা), 0 = জীবিত। DELETE-এ শুধু xmax বদলায় — data যায় না; version থাকে, visibility হারায়।

2

Visibility function-এর জন্ম-শর্ত আর মৃত্যু-শর্ত — এক বাক্য করে।

যুক্তি

জন্ম: xmin committed ও আমার snapshot-জগতে অতীত (horizon-নিচে/আমার)। মৃত্যু: xmax নেই, বা deleter abort/আমার ছবির ভবিষ্যৎ — মৃত্যুটা আমার ছবিতে এখনো ঘটেনি। দুই-ই true হলে দেখা যায়।

3

Example-এ t4-এ B commit-এর পরেও t5-এ A (RR) 100 দেখল — কোন শর্তটা সম্ভব করল?

প্রয়োগ

মৃত্যু-শর্ত: v1-এর xmax=80 committed, কিন্তু A-র horizon (60)-এর উপরে — A-র ছবিতে মৃত্যু “ভবিষ্যৎ”, v1 জীবিত। Snapshot-fixed property-ই RR- জাদু।

4

HOT update-এর দুই শর্ত, আর indexed-column update কেন নিষিদ্ধ?

যুক্তি

শর্ত: indexed column untouched + নতুন version same-page-এ। Indexed column বদলালে নতুন key-value index-entry বাধ্যতামূলক — skip করলে index পুরনো মান দেখিয়ে ভুল উত্তর দেবে; তাই HOT অসম্ভব, সব-index-write।

5

n_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 ছুটবে।

6

PG 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-র জন্মস্থান