Storage Engine — file, page, slotted page, tuple
Storage Engine: Files, Pages, Slotted Pages and Tuples
Database table আসলে fixed-size page-এর file — PostgreSQL-এর ৮ KB page খুলে দেখি: header, line pointer array, উল্টো দিক থেকে বাড়া tuple, slotted-page design-এর যুক্তি, আর প্রতিটা row-র লুকানো overhead।
আগে এটা বুঝি
এই module-এর driving question-টা মনে আছে? — বিদ্যুৎ চলে গেলেও আমার committed
transaction টিকে থাকে কীভাবে? উত্তর magic-এ না; লুকিয়ে আছে disk-এ byte কোন
আকৃতিতে শোয়া তাতে — সেই আকৃতিই আজকের বিষয়, সবচেয়ে নিচের তলা থেকে। প্রশ্ন করুন: INSERT INTO users VALUES (...) চালালে আপনার row disk-এ কোথায়
থাকে? Filesystem-এ কি হাজার হাজার ছোট file — প্রতি row-এ একটা? না। বাস্তবতা
গোছানো: database = file-দের collection, প্রতিটা file = fixed-size page-
দের সারি (PostgreSQL-এ ৮ KB), page-এর ভেতরে জন্মায় tuple — আপনার row-র
disk-রূপ। আজকের journey:
- কেন row-per-file নয় — filesystem-এর লুকানো ভাড়া
- Heap file-এর layout পরিবার আর PostgreSQL-এর directory কাঠামো
- Page anatomy: header, ItemId array, দুই প্রান্ত থেকে বাড়া tuple — slotted page; tuple header-র ২৩ byte-এ MVCC-র প্রথম ইঙ্গিত; TOAST-ও দেখা হবে
- ১০ লাখ ছোট row ঢুকিয়ে overhead নিজের চোখে মাপা
মূল ধারণা
এক row = এক file কেন নয়
ধরুন আমরা সরল পথ নিলাম: প্রতিটা row একটা আলাদা OS file — user_1042.dat। ছোট
dataset-এ চলে। কিন্তু:
| খরচ | কারণ |
|---|---|
| inode ফুরানো | প্রতিটা file inode খায় — ১০ কোটি row = ১০ কোটি inode |
| Directory explosion | এক directory-তে লাখ file — lookup O(n), backup অচল |
| open()/close() প্রতি row | প্রতি read-এ syscall + path resolution — µs × কোটি query |
| Fragmentation | ৪ KB block-এ ৪০-byte row — disk space-এর ৯৯% অপচয় |
Filesystem metadata-র ভাড়াই এত বড় যে design-টা জন্মাতেই মরে। তাই সব serious engine-এর সমাধান এক: অল্প কয়েকটা বড় file, ভেতরে নিজের মতো ভাগ করা। Filesystem শুধু বড় chunk ধরে রাখে; row-mapping-এর বুদ্ধিটা engine নিজে করে।
Heap file — table-এর ঘরবাড়ি
PostgreSQL-এ একটা table disk-এ এমনভাবে থাকে:
$PGDATA/base/16384/16385 ← database OID / table-এর relfilenode
16385 ← মূল file (segment)
16385.1 ← দ্বিতীয় segment (১ GB ছাড়লে)
16385_fsm ← free-space map — কোন page-এ কত ফাঁকা
16385_vm ← visibility map — visibility সংকেত (পরের লেসনে দরকার!)- Segment প্রতিটা সর্বোচ্চ ১ GB — 32-bit offset-এর ঐতিহাসিক উত্তরাধিকার
- File ভেতরে ভাগে ৮ KB page-এ — একমাত্র I/O unit; ৫০ byte চাইলেও ৮ KB পড়ে
- Layout-টার নাম heap file: tuple-রা order-এ নেই, যেখানে জায়গা সেখানে ঢোকে। Engine-দের layout পরিবার:
| Layout | ভাবনা | দোষ |
|---|---|---|
| Append-only | শুধু শেষে লেখো, কখনো update নয় | পুরনো version জমে — vacuum/compaction লাগে |
| Linked pages | প্রতি page-এ next/prev pointer | traversal ধীর, free-space খোঁজা ব্যথা |
| Tablespace dir + FSM | ফাঁকা-জায়গা map আলাদা structure-এ | metadata বাড়তি — তবে insert-path দ্রুত |
PostgreSQL তিনটার সংকর: append-style heap (update-এ নতুন tuple), FSM দিয়ে জায়গা-খোঁজা, directory-তে segment-ভাগ।
Page anatomy — ৮ KB-র নকশা
offset 0 offset 8191
┌───────────────────────────────────────────────┐
│ PageHeader (২৪ B) │ pd_lsn, pd_lower, pd_upper…
│ ItemId[0] → (off,len) │
│ ItemId[1] → (off,len) ↓ নিচে বাড়ে │ line pointer array
│ … │
├───────────────────────────────────────────────┤ ← pd_lower
│ ফাঁকা জায়গা (free space) │
├───────────────────────────────────────────────┤ ← pd_upper
│ tuple C (সবচেয়ে নতুন) │
│ tuple B │ ↑ উপরে বাড়ে
│ tuple A (সবচেয়ে পুরনো) │
└───────────────────────────────────────────────┘চারটা নাম: PageHeader — ২৪ byte (pd_lsn — WAL teaser!, checksum, flags,
cursor-দ্বয় pd_lower/pd_upper); ItemId / line pointer — ৪ byte: lp_off +
lp_len + flags (normal/dead/unused); tuple region — উল্টোদিকে বাড়ে; free
space = pd_upper − pd_lower。
Slotted page কেন — indirection-এর যুক্তি
সরল alternative: tuple-রা পাশাপাশি, index সরাসরি byte-offset রাখে। এবার
UPDATE name = name || '!' — tuple বড়, পুরনো জায়গায় ধরে না → relocate। সঙ্গে
সঙ্গে index-এর সব pointer ভাঙে, বাকি tuple-রাও shift-বাধ্য (বিশাল memcpy),
concurrent reader-রা ভাঙা pointer পড়ছে।
Slotted page-এর সমাধান: pointer কেউ tuple address রাখে না — সবাই slot-number
রাখে। Tuple সরলে শুধু ItemId-র lp_off বদলায়, ৪ byte; ctid/index entry স্থির।
এই indirection-ই update হওয়া page-কে বাঁচায় — memory allocator লেসনের fragmentation
পাঠের ডান হাত।
Tuple header — ২৩ byte-এ গল্প
আপনার int id, varchar name row disk-এ শুধু data না — সামনে tuple header, ~২৩ byte:
| Field | Size | কাজ |
|---|---|---|
t_xmin | 4 | যে transaction এই tuple বানাল — MVCC-র জন্মসনদ |
t_xmax | 4 | যে transaction delete/update করল (0 = জীবিত) |
t_cid/t_combocid | 4 | command id — এক transaction-এর ভেতরের ক্রম |
t_ctid | 6 | নিজের ঠিকানা (page, offset); update-এ নতুন tuple-এর দিকে মোড় নেয় |
t_infomask2 | 2 | attribute count + flags |
t_infomask | 2 | null bitmap আছে? has-varwidth? frozen? |
t_bits[] | 0–n | null bitmap — column-per-bit, দরকার হলেই |
দুটো বিষয় এখনই খেয়াল করুন। এক: xmin/xmax — MVCC-র seed; row “deleted”
মানে আসলে xmax ভরা — মৃতদেহ পরে vacuum সরায়। দুই: null bitmap — NULL-রা
জায়গা নেয় bit-এ, bitmap আসে table-এ প্রথম NULL ঢোকার পরেই, তার পরেই padding।
TOAST — যখন row page-এ ধরে না
Page ৮ KB, তাহলে ৫ MB-র PDF blob কোথায়? উত্তর TOAST — The Oversized-Attribute
Storage Technique। Value ~২ KB crossing-এ: (১) আগে compress, (২) তবু বড় হলে
টুকরো করে আলাদা toast table-এ — সেখানেও slotted-page জগৎই! (৩) main tuple-এ
থাকে মাত্র ১৮ byte-র toast pointer। ফলে main page ছোট — SELECT count(*) চলতে
গিয়ে বিশাল text column-এর byte-ও engine ছোঁয় না।
ভেতরে কী ঘটছে
Alignment padding — নীরব জায়গা-খোর
Memory allocation লেসনের পাঠ এখানেও: CPU aligned access চায়। PostgreSQL page-ও
তাই MAXALIGN = 8 byte-এ চলে — int ৪-এ, bigint ৮-এ বসে। ফলাফল: column order
পর্যন্ত size বদলে দেয়:
CREATE TABLE bad (a smallint, b bigint, c smallint); -- 2+pad6+8+2+pad ≈ 24 B
CREATE TABLE good (b bigint, a smallint, c smallint); -- 8+2+2+pad4 ≈ 16 B
-- ১ কোটি row-তে ফারাক ~৮০ MB — শুধু column সাজিয়ে!Naive math (৫–১০ byte data) আর বাস্তব (২৩-byte header + padding + varlena ≈ ৪০+ byte) — ফারাকটাই experiment-এ মাপবেন। Rule of thumb: aligned fixed-width বড়গুলো
আগে, ছোটগুলো পরে, string-রা শেষে。
Sequential scan পুরো page কেন পড়ে
SELECT * FROM orders WHERE amount < 50 — index না থাকলে engine প্রতিটা page
পড়ে, প্রতিটা tuple বিচারে তোলে। অদ্ভুত অপচয় নয়: page-রা contiguous — OS
readahead/prefetch চালু করে, kernel একাই পরের page আনতে থাকে; sequential
per-byte দাম random-এর ভগ্নাংশ — “wasted” বাকি অংশও প্রায় বিনামূল্যে।
আরেক teaser: tuple-র physical ঠিকানা ctid — (block, offset); index-রা এটাই
ধরে রাখে, আর এই ঠিকানাই পরের দুই লেসনে বারবার ফিরবে।
উদাহরণ
একটা ৮ KB page-এর হাতে-বসানো hexdump
নতুন table t(id int, tag char(4))-এ ৩টা row ঢোকানোর পরে page 0-এর প্রথম bytes
(xxd আউটপুট; LSN-জাতীয় মান run-ভেদে বদলাবে):
00000000: a8d4 1500 0000 0000 0000 0000 2400 881f pd_lsn … lower=36 upper=8072
00000010: 0020 0420 0000 0000 d89f 4200 b09f 4200 special=8192 ver=0x2004 ItemId[0..1]
00000020: 889f 4200 ItemId[2]Byte-by-byte পাঠ:
offset 0–7 : pd_lsn = A8 D4 15 00 00 00 00 00 → WAL position 0/15D4A88
offset 8–9 : pd_checksum = 0x0000 (checksum off হলে 0)
offset 10–11 : pd_flags = 0x0000
offset 12–13 : pd_lower = 0x0024 = 36 → 24(header) + 3×4(ItemId) — ঠিক তিন slot
offset 14–15 : pd_upper = 0x1F88 = 8072 → tuple region এখান থেকে শুরু
offset 16–17 : pd_special = 0x2000 = 8192 (heap page-এ সবসময় pagesize-ই)
offset 18–19 : pd_pagesize_version = 04 20 → little-endian 0x2004 = 8192|layout-4 ✓
offset 20–23 : pd_prune_xid = 0x00000000
ItemId[0] @24 (0x18) = D8 9F 42 00 → word 0x429FD8 (LE):
lp_off = word & 0x7FFF = 8152 · flags = (word>>15)&3 = 1 (normal) · lp_len = word>>17 = 33
ItemId[1] @28 = B0 9F 42 00 → off=8112, len=33 · ItemId[2] @32 = 88 9F 42 00 → off=8072lp_len ৩৩ byte = t_hoff ২৪ (২৩ header + pad) + int ৪ + char(4) varlena ৫;
placement MAXALIGN-এ spacing ঠিক ৪০ (= MAXALIGN(33))। তিনটা tuple শেষপ্রান্ত
থেকে উল্টোদিকে বেড়েছে, pd_upper ঠিক শেষ tuple-এর মাথায় — নকশা হুবহু মিলল。
pageinspect — SQL দিয়ে একই খোঁচা
হাতে hexdump না করে extension দিয়েও page খোলা যায়:
CREATE EXTENSION pageinspect;
SELECT * FROM page_header(get_raw_page('t', 0));
SELECT lp, lp_off, lp_len, t_xmin, t_ctid,
tuple_data_split('t', t_data, t_infomask, t_infomask2, t_bits)
FROM heap_page_items(get_raw_page('t', 0));Output (সংক্ষেপে):
lp | lp_off | lp_len | t_xmin | t_ctid | tuple_data_split
----+--------+--------+--------+--------+------------------------------
1 | 8152 | 33 | 751 | (0,1) | {"\x00000001","abcd"}
2 | 8112 | 33 | 751 | (0,2) | {"\x00000002","efgh"}
3 | 8072 | 33 | 751 | (0,3) | {"\x00000003","ijkl"}t_data-র প্রথম ৪ byte-ই id, পরেরটা tag — tuple_data_split header কেটে data
আলাদা করে দেখাচ্ছে।
ছবিতে
insert A,B,C,D,E,F: delete B,E: compact/vacuum:
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Id[0]→A │ │ Id[0]→A │ │ Id[0]→A │
│ Id[1]→B │ │ Id[1]=DEAD │ │ Id[1]→C │
│ Id[2]→C │ │ Id[2]→C │ │ Id[2]→D │
│ Id[3]→D │ │ Id[3]→D │ │ Id[3]→F │
│ Id[4]→E │ │ Id[4]=DEAD │ │ Id[4]=unused │
│ Id[5]→F │ │ Id[5]→F │ │ Id[5]=unused │
│·····free·····│ │·····free·····│ │·····free·····│
│F E D C B A←──│ tuple end │F · D · C · A │ │F D C A←──────│ reclaimed
└──────────────┘ └──────────────┘ └──────────────┘
lower→, ←upper গর্ত থাকে, free-space byte-move হয় এখানে,
টুকরো টুকরো — FSM জানে কিন্তু Id নম্বর stable!row: id int4 + name varchar(20) = 'Rahim' → বাস্তবে ~৪৪ byte
┌──────────────────────────────────────────────┐
│ t_xmin(4) t_xmax(4) t_cid(4) t_ctid(6) │ ← MVCC হিসাবখাতা: ১৮ B
│ t_infomask2(2) t_infomask(2) [t_bits(0)] │ ← মেটাডেটা: ৪ B
│ ── pad to 8-alignment: ১ B ── │
│ varlena header(1) + 'Rahim'(5); id int4(4) │ ← data: ১০ B
│ ── trailing MAXALIGN pad ── │
└──────────────────────────────────────────────┘
data ১০ B, overhead ~৩৪ B — ছোট row-র দুনিয়ায় overhead-ই রাজানিজে চালিয়ে দেখুন
৮ KB page খোলা আর overhead মাপা — pg_relation_size বনাম naive math
ধাপ ০ — PostgreSQL না থাকলে: docker run --rm -d --name pg-lab -e POSTGRES_PASSWORD=lab -p 5432:5432 postgres:16,
তারপর docker exec -it pg-lab psql -U postgres; নিজের machine-এ থাকলে সরাসরি psql।
ধাপ ১ — tiny-row table, ১০ লাখ row:
CREATE TABLE tiny (id integer, flag boolean);
INSERT INTO tiny SELECT g, g % 2 = 0 FROM generate_series(1, 1000000) g;
ANALYZE tiny;ধাপ ২ — naive math বনাম বাস্তব (naive: id ৪ B + flag ১ B = ৫ B/row → ৫ MB):
SELECT pg_size_pretty(pg_relation_size('tiny')); -- data pages
SELECT pg_size_pretty(pg_total_relation_size('tiny')); -- + FSM/VM/toast
SELECT relpages FROM pg_class WHERE relname = 'tiny'; -- কয়টা ৮ KB page?প্রত্যাশিত: ~35–38 MB, relpages ≈ 4500। Per-row ≈ ৩৬–৩৭ byte — ৫ byte
data-র জন্য! ভাঙুন: ২৪-byte header block (২৩+pad) + ItemId ৪ B + page header
amortized (২৪ B ÷ ~২২৫ tuple/page) + half-full tail pages।
ধাপ ৩ — page-এর ভেতরে (pageinspect):
CREATE EXTENSION IF NOT EXISTS pageinspect;
SELECT count(*) AS live_items, min(lp_len) AS tuple_bytes,
round(avg(lp_len)) AS avg_tuple_bytes
FROM heap_page_items(get_raw_page('tiny', 100));
SELECT * FROM page_header(get_raw_page('tiny', 100));avg_tuple_bytes ≈ ২৯–৩২; free space = pd_upper − pd_lower — ধাপ ২-এর
বাজেট-ভাঙার সাথে ItemId + tuple + overhead ≈ ৩৬–৩৭ byte/row-এ মিলবে।
ধাপ ৪ — (optional) raw hexdump: relfilenode বের করুন
SELECT pg_relation_filepath('tiny'); — pg_filedump -i -f <path> | head -40
install থাকলে annotate করে দেখায়; না থাকলে example-এর hand-annotated dump-ই পাঠ।
দেখা গেল কী: naive math প্রতি ধাপে হারে — header, ItemId, padding, half-empty tail। Storage engine-এর প্রথম পাঠ: disk-এ ‘শুধু data’ নেই।
১০ লাখ tiny row-এর table naive math-এর চেয়ে ~৩× জায়গা নেয় — overhead-এর হিসাব মিলে যায় pageinspect-এর byte-level report-এর সাথে; page আসলে ঠিক নকশার মতোই ভরে।
নিজে বানান
SlottedPage — ৪ KB buffer-এ pack/unpack, fragmentation report সহ
- fixed bytearray(PAGE_SIZE) — কোনো Python list নয়, আসল byte-buffer
- slot array নিচ থেকে, tuple data উপর থেকে — pd_lower/pd_upper cursor
- insert(data: bytes) → slot number; জায়গা না থাকলে compact করে আবার চেষ্টা
- delete(slot) → tombstone bit ItemId-তেই; get(slot) dead হলে KeyError
- compact(): live tuple-গুলো শেষ প্রান্তে টেনে slot rewrite — Id stable
- report(): live/dead bytes, slack, fragmentation % — delete+reinsert cycle-এ ছাপুন
লক্ষ্য: PostgreSQL-এর page-machinery-র হাড়কাঁটা — indirection, দুই-cursor free space, tombstone, compaction — সব milestone এক struct-দুটোতে:
import struct
PAGE_SIZE = 4096
HEADER = 8 # mini-header: lower@0, upper@2
SLOT = 4 # '>HH' → offset, len(+0x8000 tombstone bit)
DEAD = 0x8000
class PageFull(Exception):
pass
class SlottedPage:
def __init__(self):
self.buf = bytearray(PAGE_SIZE)
self.lower = HEADER # slot array-র শেষ প্রান্ত
self.upper = PAGE_SIZE # tuple region-র শুরু
def _slot(self, i):
off, ln = struct.unpack_from('>HH', self.buf, HEADER + i * SLOT)
return off, ln
def _set_slot(self, i, off, ln):
struct.pack_into('>HH', self.buf, HEADER + i * SLOT, off, ln)
def insert(self, data: bytes) -> int:
if self.upper - self.lower < SLOT + len(data):
self.compact() # গর্ত সরিয়ে আবার চেষ্টা
if self.upper - self.lower < SLOT + len(data):
raise PageFull(f'{len(data)}B needs {SLOT + len(data)}B')
off = self.upper - len(data)
self.buf[off:self.upper] = data
self.upper = off
sid = (self.lower - HEADER) // SLOT
self._set_slot(sid, off, len(data))
self.lower += SLOT
return sid
def get(self, sid: int) -> bytes:
off, ln = self._slot(sid)
if ln & DEAD:
raise KeyError(f'slot {sid} deleted')
return bytes(self.buf[off:off + ln])
def delete(self, sid: int) -> None:
off, ln = self._slot(sid)
if not ln & DEAD:
self._set_slot(sid, off, ln | DEAD) # data থাকে, flag-ই মৃত্যুসনদ
def compact(self):
"""live tuple-গুলো page-এর শেষে টেনে slot-offset rewrite"""
moved = 0
top = PAGE_SIZE
nslots = (self.lower - HEADER) // SLOT
for i in range(nslots): # পুরনো ক্রমেই
off, ln = self._slot(i)
if ln & DEAD:
self._set_slot(i, 0, 0) # dead slot সম্পূর্ণ মুছি
continue
if ln == 0:
continue
new_off = top - ln
if new_off != off:
self.buf[new_off:top] = self.buf[off:off + ln]
moved += ln
self._set_slot(i, new_off, ln & 0x7FFF)
top = new_off
self.upper = top
return moved
def report(self):
nslots = (self.lower - HEADER) // SLOT
live = dead = 0
for i in range(nslots):
_, ln = self._slot(i)
if ln == 0:
continue
dead += ln & 0x7FFF if ln & DEAD else 0
live += (ln & 0x7FFF) if not ln & DEAD else 0
used = PAGE_SIZE - self.upper
frag = dead / used * 100 if used else 0.0
print(f'slots={nslots} live={live}B dead={dead}B '
f'slack={self.upper - self.lower}B '
f'fragmentation={frag:.1f}%')
if __name__ == '__main__':
pg = SlottedPage()
payload = lambda k: f'record-{k:04d}'.encode() * 3 # 33 B each
slots = [pg.insert(payload(k)) for k in range(60)]
print('after 60 inserts:')
pg.report()
for s in slots[::3]: # প্রতি তৃতীয়টা মারো
pg.delete(s)
print('after deleting every 3rd:')
pg.report()
extra = [pg.insert(payload(1000 + k)) for k in range(10)]
print('after re-inserting 10 new (holes still there):')
pg.report()
moved = pg.compact()
print(f'compact() moved {moved}B:')
pg.report()
assert all(pg.get(slots[k]) == payload(k)
for k in range(60) if k % 3 != 0) # Id-রা stable!
assert pg.get(extra[0]) == payload(1000)
print('survivors readable via same slot numbers ✓')প্রত্যাশিত output:
after 60 inserts:
slots=60 live=1980B dead=0B slack=1868B fragmentation=0.0%
after deleting every 3rd:
slots=60 live=1320B dead=660B slack=1868B fragmentation=33.3%
after re-inserting 10 new (holes still there):
slots=70 live=1650B dead=660B slack=1498B fragmentation=28.6%
compact() moved 1650B:
slots=70 live=1650B dead=0B slack=2158B fragmentation=0.0%
survivors readable via same slot numbers ✓লক্ষ্য করুন দুইটা জিনিস: (১) reinsert করলেও dead bytes জমেই থাকে — allocator শুধু উপরে append করে; PostgreSQL-ও এই কারণেই vacuum-এর ভরসা। (২) compact-এর পরেও বেঁচে থাকা record-রা একই slot number-এ পড়া যায় — indirection-ই promise।
নিজে বাড়ান: (১) update(sid, data) — delete+insert + forwarding slot hint;
(২) max-tuple cap (PG-র ~২ KB rule-এর মতো) — বড় payload-এ TOAST-style exception;
(৩) header-এ checksum যোগ করে torn-page detect।
বাস্তব সিস্টেমে
বাস্তব জগতে page আর tuple
- PostgreSQL: আজ যা দেখলাম — ৮ KB page, ১ GB segment, FSM/VM sidecar, TOAST;
pg_relation_filepath()-এ disk-ঠিকানা। - SQLite: পুরো database এক file, fixed-size ৪ KB page; সবকিছু B-tree page-এ — page-ভিত্তিক চিন্তার সবচেয়ে পরিষ্কার নমুনা।
- InnoDB: ১৬ KB page; COMPACT row format-এ tuple header + var-length array — slotted ভাবনা একই, রূপ ভিন্ন।
- Kafka/Parquet: segment file / row-group — ‘ছোট record নয়, বড় contiguous chunk’; batch-read locality-ই king।
Production tip: column order সাজালে (bigint আগে, string/bool শেষে) ১০–১৫% storage বাঁচে — backup window আর cache hit-rate দুটোতেই সুদে ফেরে।
যে ভুলগুলো সবাই করে
“Database-এ প্রতিটা row disk-এ আলাদা file হিসেবে থাকে।”
Filesystem-এর inode/directory ভাড়া scale-এ অসম্ভব — ১০⁸ row = ১০⁸ file। বাস্তবে table = কয়েকটা বড় file-এর ভেতরে page-সারি; row-mapping engine নিজের ItemId-তেই করে; filesystem শুধু ‘বড় byte-array’ service দেয়।
“UPDATE disk-এ শুধু বদলানো field-টাই overwrite করে।”
PostgreSQL না-ই (নতুন tuple লেখে, পুরনোটা xmax-এ মৃত); general সত্যও তা-ই: page-ই I/O unit, এক field-এর জন্যও পুরো page read-modify-write। In-place engine-রাও field-granularity patch করে না — ‘row update = row rewrite’-ই সঠিক model।
“NULL column কিছুই জায়গা নেয় না — একদম ফ্রি।”
Data-area-তে নেয় না ঠিক, কিন্তু null bitmap-এ ১ bit, আর bitmap চালু হয় প্রথম NULL- এই — সব row-এ header-বাজেট বাড়ে; padding geometry-ও বদলাতে পারে। ‘প্রায় ফ্রি’, ‘ফ্রি’ নয়।
“Page size যত বড়, performance তত ভালো — ৩২ KB কেন নয়?”
বড় page = point-query-তে wasted bandwidth, pool-এ কম frame, write amplification; ছোট = বেশি header overhead, কম sequential efficiency। ৮/১৬ KB decades-এর balance point — WAL/recovery geometry-ও জড়ানো।
বুঝেছেন কি না দেখুন
1Slotted page-এর চারটা region আর দুই cursor-এর নাম বলুন। Free space হিসাব?
স্মরণ
Region: PageHeader (২৪ B), ItemId array, free space, tuple region। Cursor:
pd_lower/pd_upper; free space = pd_upper − pd_lower — insert-এ lower
৪ বাড়ে, upper tuple-size কমে; ছুঁয়ে গেলে page full।
2Index entry সরাসরি tuple-র byte-offset রাখলে UPDATE-এ ঠিক কী ভাঙত? Slotted
page কোন ৪-byte খরচে সেটা রোধ করে?
যুক্তি
Tuple relocate হলে সব direct offset ভুল — ঠিক রাখতে পরবর্তী সব tuple-ও shift
করতে হতো: বিশাল memcpy + race। Slotted page-এ সবাই slot-number ধরে; relocate-এ
শুধু ওই slot-এর lp_off (৪ B) বদলায় — external pointer stable。
3Table (id int4, active bool) — এক row-র disk-বাজেট header/data/padding/ItemId
সহ। ১০ লাখ row মোট কত MB?
প্রয়োগ
(id int4, active bool) — এক row-র disk-বাজেট header/data/padding/ItemId
সহ। ১০ লাখ row মোট কত MB?Header ২৩ + int4 ৪ + bool ১ = ২৮ → maxalign-৮-এ ৩২ B + ItemId ৪ = ৩৬ B/row; page overhead মিলিয়ে ~৩৭ B × ১০⁶ ≈ ৩৫–৪০ MB — experiment-এর ~৩৬–৩৭ B/row-এর সাথে মেশে, naive ৫ MB নয়।
4(a smallint, b bigint, c smallint) বনাম (b bigint, a smallint, c smallint)
— data area-তে পার্থক্য কত, কোন alignment নিয়মে?
যুক্তি
(a smallint, b bigint, c smallint) বনাম (b bigint, a smallint, c smallint)
— data area-তে পার্থক্য কত, কোন alignment নিয়মে?প্রথম: a@0(2)+pad৬+b@8(8)+c@16(2)+tail-pad৬ = ২৪ B; দ্বিতীয়: b@0(8)+ a@8(2)+c@10(2)+tail-pad৪ = ১৬ B। নিয়ম: natural alignment + tuple MAXALIGN- 8-এ শেষ। ৮ B/row × কোটি row = শত শত MB।
5TOAST কেন compress-আগে-out-of-line-পরে নয় — উল্টো ক্রম? Main page ছোট রাখা কার
জন্য জরুরি?
যুক্তি
Compress সস্তা CPU, out-of-line write দামি I/O — compress-ই যথেষ্ট হলে toast-table
I/O এড়ানো যায়। Main page ছোট রাখা জরুরি কারণ scan/index heap-fetch প্রতিটা tuple
ছোঁয় — inline blob থাকলে count(*)-ও শত শত MB touch করত।
6Workload: ৯৫% query WHERE device_id = ? AND ts BETWEEN ? AND ? (few rows),
৫% nightly full-scan analytics। আজকের পাঠ থেকে তিনটা concrete design সিদ্ধান্ত।
ডিজাইন
WHERE device_id = ? AND ts BETWEEN ? AND ? (few rows),
৫% nightly full-scan analytics। আজকের পাঠ থেকে তিনটা concrete design সিদ্ধান্ত।(১) Column order: aligned fixed-width আগে, বড় text/json শেষে — hot pages ঘন। (২) বড় payload TOAST-able — full-scan-এ main pages ছোট। (৩) Hot point-path-এর index ভাবুন (পরের লেসনগুলো), কিন্তু index-ও page খায় — ingest-এ count সামলে।
এরপর কী
এরপর কী
Disk-এ সব গোছানো: page-ভরা file, slotted page, tuple ctid-ঠিকানায় শোয়া। কিন্তু প্রতি query-তে disk ছোঁয়া অসহ্য — HDD-তে ১০ ms, SSD-তেও ১০০ µs, RAM ১০০ ns। তাই engine disk-কে RAM-এ ভাড়া দেয় — buffer pool। পরের লেসনে:
- Double buffering — database OS pagecache-কে কেন বিশ্বাস করে না
- pin/unpin + dirty flag — buffer manager-এর contract
- LRU-র বিপদ, CLOCK-এর second chance — PostgreSQL ring buffer
- Hit-rate math: cold-vs-warm query-র হাজারগুণ ফারাকের ব্যাখ্যা
আজকের ৮ KB page-টাই পরের লেসনের currency — frame-এ ভাড়া বসবে।
আরও পড়ুন
- PostgreSQL Documentation — Database Physical Storage (Chapter 65) — The PostgreSQL Global Development Group · Page layout, heap file structure, TOAST — এই লেসনের প্রামাণ্য উৎস
- Database Internals — Part 1: Storage Engines — Alex Petrov · Page-oriented storage, slotted page আর cell/page anatomy-র গোছানো আলোচনা
- pg_filedump — PostgreSQL page inspection utility — PostgreSQL community · Raw relation file-এর page hexdump করার tool — experiment-এ ব্যবহৃত