وقتی در PostgreSQL یک جدول میسازید و داخلش داده میریزید، شاید فکر کنید دادهها «در جدول» ذخیره میشوند.
اما واقعیت این است که پشت هر جدول، یک فایل واقعی روی دیسک وجود دارد — و PostgreSQL دادهها را در قالب صفحات (Pages) و تاپلها (Tuples) داخل این فایلها نگهداری میکند.
هر جدول در PostgreSQL در فایل مخصوص خودش ذخیره میشود.
اگر به مسیر دادههای PostgreSQL (مثل /var/lib/postgresql/18/main/base/) سر بزنید، میتوانید فایلهای جدولها را ببینید.
نام فایل برابر با شناسه داخلی جدول است که با دستور زیر میتوانید آن را ببینید:
SELECT relfilenode FROM pg_class WHERE relname = 'demo_users';
PostgreSQL دادهها را در واحدهایی به نام Page ذخیره میکند.
هر Page معمولاً ۸ کیلوبایت (۸KB) حجم دارد.
میتوانید تصور کنید هر جدول از تعداد زیادی بلوک ۸KB تشکیل شده که هر کدام چندین ردیف (tuple) را در خود جای میدهند:
┌──────── page 0 ────────┐
│ row1 | row2 | row3 ... │
└────────────────────────┘
┌──────── page 1 ────────┐
│ row4 | row5 | row6 ... │
└────────────────────────┘
وقتی دادههای جدید اضافه میکنید، PostgreSQL در صورت نیاز صفحههای جدیدی به فایل اضافه میکند.
CREATE TABLE demo_users (
id SERIAL PRIMARY KEY,
username TEXT,
email TEXT,
bio TEXT
);
الان جدول خالی است و اندازهاش تقریباً صفر است:
SELECT pg_size_pretty(pg_relation_size('demo_users')) AS heap_size,
pg_size_pretty(pg_total_relation_size('demo_users')) AS total_size;
📦 خروجی:
heap_size | total_size
------------+------------
۰ bytes | 24 kB
این ۲۴KB مربوط به فایلهای کنترلی مثل Free Space Map (FSM) است، نه داده واقعی.
INSERT INTO demo_users (username, email, bio)
SELECT 'user_' || g,
'user' || g || '@example.com',
'This is bio for user ' || g
FROM generate_series(1, 50) g;
حالا جدول واقعاً داده دارد. دوباره اندازه را بررسی کنیم:
SELECT pg_size_pretty(pg_relation_size('demo_users')) AS heap_only,
pg_size_pretty(pg_total_relation_size('demo_users')) AS total;
خروجی ممکن است چیزی شبیه این باشد:
heap_only | total
------------+---------
۸ kB | 32 kB
🎯 یعنی جدول حالا حداقل یک Page (۸KB) برای دادهها دارد.
بیایید نگاهی به ساختار درونی هر ردیف بیندازیم:
SELECT ctid, xmin, xmax, username
FROM demo_users
LIMIT 5;
نتیجه نمونه:
| ctid | xmin | xmax | username |
|---|---|---|---|
| (۰,۱) | ۱۲۳۴۵ | ۰ | user_1 |
| (۰,۲) | ۱۲۳۴۵ | ۰ | user_2 |
| ستون | توضیح |
|---|---|
| ctid | موقعیت فیزیکی رکورد روی دیسک (Page و Slot). مثلاً (۰,۱) یعنی در صفحه ۰، جایگاه ۱. |
| xmin | شناسه تراکنشی که رکورد را ایجاد کرده. |
| xmax | شناسه تراکنشی که رکورد را حذف یا بهروزرسانی کرده (اگر صفر باشد یعنی هنوز زنده است). |
بیایید یک ردیف را تغییر دهیم:
UPDATE demo_users
SET bio = bio || ' - updated version'
WHERE username = 'user_1';
سپس دوباره بررسی کنیم:
SELECT ctid, xmin, xmax, username
FROM demo_users
WHERE username = 'user_1';
خروجی جدید:
| ctid | xmin | xmax | username |
|---|---|---|---|
| (۲,۴) | ۴۵۶۷۸ | ۰ | user_1 |
🔁 میبینید که CTID عوض شد!
چون PostgreSQL هنگام UPDATE در واقع رکورد جدیدی مینویسد و رکورد قبلی را منقضی میکند — این همان رفتار MVCC است.
INSERT INTO demo_users (username, email, bio)
SELECT 'user_' || g,
'user' || g || '@example.com',
'Another bio ' || g
FROM generate_series(51, 1000) g;
سپس:
SELECT pg_size_pretty(pg_relation_size('demo_users')) AS heap_only;
خروجی مثلاً:
heap_only
------------
۳۲۰ kB
📈 هر بار که داده اضافه میشود، فایل جدول با گامهای ۸KB بزرگتر میشود.
| مفهوم | توضیح |
|---|---|
| Page | بلوک ۸KB که دادهها در آن ذخیره میشوند. |
| CTID | موقعیت فیزیکی رکورد در قالب (page, slot) |
| XMIN / XMAX | شناسه تراکنشهای ایجاد / حذف رکورد |
| FSM (Free Space Map) | نقشه فضای خالی برای درج داده جدید |
| Page Growth | هر بار ۸KB داده جدید → افزایش یک Page |
CTID ابزاری عالی برای فهمیدن نحوهی ذخیره فیزیکی دادهها در PostgreSQL است.
در واقع هر بار که دادهای را تغییر میدهید، یک نسخهی جدید از آن در جای دیگری از فایل نوشته میشود.
اگر بخواهید دقیقتر ببینید دادهها کجا روی دیسک ذخیره شدهاند، میتوانید مسیر فایل را از PostgreSQL بگیرید:
SELECT pg_relation_filepath('demo_users');
و سپس فایل را در سیستمعامل بررسی کنید.
نگاهی عمیقتر به درون فایلهای واقعی PostgreSQL
وقتی در پوشهی دادههای PostgreSQL (مثلاً /var/lib/postgresql/18/main/base/...) به دنبال جدولها میگردید، احتمالاً چیزی شبیه این میبینید 👇
۱۶۴۷۷
16477_fsm
16477_vm
16477_toast
16477_toast_fsm
شاید تعجب کنید این فایلهای اضافی چیستند.
در واقع PostgreSQL برای هر جدول اصلی (heap) چند فایل جانبی هم میسازد تا بتواند فضای دیسک را بهتر مدیریت کند، سرعت جستوجو را بالا ببرد و دادههای حجیم را جداگانه نگه دارد.
| فایل | وظیفه | توضیح کوتاه |
|---|---|---|
۱۶۴۷۷ | Heap file | دادههای واقعی جدول |
16477_fsm | Free Space Map | نگهداری فضای خالی صفحات |
16477_vm | Visibility Map | وضعیت قابلمشاهده بودن صفحات |
16477_toast | TOAST table | ذخیرهی دادههای بسیار بزرگ |
16477_toast_fsm | FSM برای TOAST | فضای خالی جدول TOAST |
فایل اصلی جدول بدون هیچ پسوندی است، مثلاً ۱۶۴۷۷.
در این فایل، دادههای واقعی (هر ردیف یا tuple) در قالب pageهای ۸KB ذخیره میشوند.
📦 ویژگیها:
ls -lh /var/lib/postgresql/18/main/base/16384/16477
# => 320K (بسته به تعداد رکوردها)
فایل 16477_fsm که پسوند _fsm دارد، برای مدیریت فضای آزاد داخل جدول استفاده میشود.
🔹 PostgreSQL باید بداند هنگام INSERT، کجا فضا دارد تا دادهی جدید بنویسد.
به جای اینکه کل جدول را بگردد، از FSM (Free Space Map) کمک میگیرد.
میتوان گفت FSM شبیه دفترچهای است که PostgreSQL در آن یادداشت میکند کدام صفحه هنوز جا دارد.
📘 هر صفحهی FSM اطلاعات مربوط به هزاران صفحهی heap را نگه میدارد.
به همین دلیل ممکن است جدول شما فقط ۸KB داده داشته باشد ولی FSM آن ۲۴KB باشد — طبیعی است.
📊 برای مشاهده اندازه:
SELECT
pg_size_pretty(pg_relation_size('demo_users')),
pg_size_pretty(pg_relation_size('demo_users_fsm'));
فایل 16477_vm که پسوند _vm دارد، نقشهی دیدپذیری دادههاست.
PostgreSQL در آن علامت میزند که هر صفحه:
💡 چرا مهم است؟
بنابراین _vm یک فایل کوچک ولی حیاتی برای بهبود کارایی است.
PostgreSQL محدودیت اندازهی هر page را ۸KB تعیین کرده.
اما اگر ستونی از نوع TEXT یا BYTEA یا JSONB داشته باشید که خیلی بزرگ باشد (مثلاً 1MB یا بیشتر)، دیگر در page جا نمیشود.
اینجاست که PostgreSQL از TOAST استفاده میکند.
TOAST = The Oversized-Attribute Storage Technique 🍞
📦 وقتی دادهای بزرگتر از حد مجاز باشد:
_toast) ذخیره میکند.میتوانید ببینید چه جدول TOASTی برای هر جدول ساخته شده:
SELECT reltoastrelid::regclass AS toast_table
FROM pg_class
WHERE relname = 'demo_users';
اگر جدول شما دادههای حجیم نداشته باشد، ممکن است هنوز TOAST ایجاد نشده باشد.
برای هر جدول TOAST، PostgreSQL خودش نیز فایلهای _fsm و _vm دارد.
چون آن هم یک جدول واقعی است.
بنابراین ممکن است در فایلسیستم ببینید:
16477_toast
16477_toast_fsm
16477_toast_vm
📘 این یعنی حتی دادههای فشرده و بزرگ هم مثل هر جدول دیگری مدیریت و بهینهسازی میشوند.
┌────────────────────────────┐
│ demo_users (16477) │ ← دادههای واقعی جدول
│ ├── 16477_fsm │ ← فضای خالی در صفحات
│ ├── 16477_vm │ ← وضعیت دیدپذیری صفحات
│ └── 16477_toast (درصورت نیاز) ← دادههای بزرگ
│ ├── 16477_toast_fsm │
│ └── 16477_toast_vm │
└────────────────────────────┘
SELECT
pg_size_pretty(pg_relation_size('demo_users')) AS heap_only,
pg_size_pretty(pg_total_relation_size('demo_users')) AS total_including_fsm_vm;
📊 تفاوت مهم:
pg_relation_size() فقط heap اصلی را محاسبه میکند.pg_total_relation_size() شامل heap + fsm + vm + toast است.| فایل | توضیح | کاربرد |
|---|---|---|
relfilenode | دادهی واقعی جدول | ذخیره ردیفها (heap) |
relfilenode_fsm | Free Space Map | ثبت فضای خالی برای درج بعدی |
relfilenode_vm | Visibility Map | بهینهسازی VACUUM و Index Scan |
relfilenode_toast | TOAST Table | نگهداری دادههای بزرگتر از ۸KB |
relfilenode_toast_fsm | FSM برای TOAST | فضای آزاد در دادههای TOAST |
🧠 خلاصه ذهنی:
هر جدول PostgreSQL در واقع یک اکوسیستم کوچک است — با فایلهایی که هرکدام نقش خود را دارند: