در هستهٔ معماری ذخیرهسازی PostgreSQL، یک محدودیت بنیادی وجود دارد: صفحات داده (Data Pages) اندازهٔ ثابت ۸ کیلوبایت دارند و یک رکورد (tuple) هرگز نمیتواند از یک صفحه بیشتر شود . این محدودیت برای سیستمهای قدیمی که دادهها عمدتاً کوچک بودند، طراحی شده بود. اما در دنیای امروز با دادههای حجیم مانند متنهای طولانی، اسناد JSON، آرایههای بزرگ یا فایلهای باینری، چه باید کرد؟
اینجاست که TOAST وارد میشود؛ یکی از هوشمندترین مؤلفههای معماری PostgreSQL که بهصورت خودکار و شفاف، مشکل دادههای بزرگ را حل میکند. TOAST مخفف The Oversized-Attribute Storage Technique است و در مستندات PostgreSQL با طنزی دوستداشتنی «بهترین چیز پس از نان برشخورده» نامیده شده .
در این نوشتار، با سفری از لایههای بالایی تا عمق ذخیرهسازی فیزیکی، خواهیم دید که TOAST چگونه این معجزه را ممکن میکند.
برای درک ضرورت TOAST، باید با دو مفهوم پایه آشنا شویم:
حال اگر بخواهیم ردیفی با یک ستون TEXT به طول ۱۰۰ کیلوبایت ذخیره کنیم، چه اتفاقی میافتد؟ بهطور سنتی، این کار غیرممکن است. اما دادههای مدرن اغلب ویژگیهای زیر را دارند:
اگر بخواهیم همهٔ دادهها را در صفحهٔ اصلی ذخیره کنیم:
راهحل معماری PostgreSQL برای این مسئله، جداسازی دادههای بزرگ از جریان اصلی و مدیریت هوشمند آنهاست.
TOAST یک مکانیزم تطبیقی است که بهصورت خودکار تصمیم میگیرد با هر مقدار بزرگ چگونه رفتار کند:
نکتهٔ مهم این است که TOAST فقط یک جدول کمکی نیست، بلکه یک سیاست مدیریت اندازه در سطح ستون است. هر ستون میتواند استراتژی متفاوتی داشته باشد و سیستم بهصورت هوشمند بهترین تصمیم را میگیرد.
سیستم ابتدا تلاش میکند داده را فشرده کند تا در همان صفحه باقی بماند. از PostgreSQL 14 به بعد، دو الگوریتم فشردهسازی قابل انتخاب هستند :
-- تنظیم الگوریتم فشردهسازی پیشفرض برای نشست جاری
SET default_toast_compression = 'lz4';
-- تنظیم برای یک ستون خاص در زمان ایجاد جدول
CREATE TABLE documents (
id SERIAL,
content TEXT COMPRESSION lz4
);
اگر حتی پس از فشردهسازی، داده هنوز بزرگ باشد (بیش از آستانهٔ تعریفشده)، به سراغ مرحلهٔ بعد میرویم:
در زمان خواندن داده، اگر کاربر ستون بزرگ را انتخاب کرده باشد، PostgreSQL بهصورت خودکار:
همهٔ این مراحل برای کاربر و حتی برنامهنویس کاملاً شفاف است.
برای درک TOAST، باید با نحوهٔ نمایش دادههای با طول متغیر در PostgreSQL آشنا شویم. هر مقدار از نوع TEXT یا BYTEA در حافظه با قالبی به نام varlena ذخیره میشود .
چهار حالت مختلف برای یک مقدار varlena وجود دارد:
| نوع | توضیح |
|---|---|
| غیر TOAST شده | داده بهصورت عادی با هدر ۴ بایتی ذخیره شده |
| یکبایتی | برای مقادیر کوچک (زیر ۱۲۷ بایت) با هدر ۱ بایتی |
| فشرده شده | داده فشرده شده، نیاز به decompress دارد |
| اشارهگر TOAST | داده خارج از صفحه ذخیره شده |
هر جدول که دارای ستونهای بزرگ باشد، یک جدول TOAST اختصاصی در schema سیستمی pg_toast خواهد داشت. ساختار این جدول ساده اما هوشمندانه است :
| ستون | نوع | توضیح |
|---|---|---|
| chunk_id | OID | شناسهٔ یکتای مقدار اصلی |
| chunk_seq | integer | شمارهٔ توالی قطعه |
| chunk_data | bytea | دادهٔ واقعی قطعه |
یک ایندکس منحصربهفرد روی (chunk_id, chunk_seq) بازیابی سریع را ممکن میکند.
بیایید با مثالهای عملی، رفتار TOAST را از نزدیک ببینیم.
CREATE TABLE toast_demo (
id SERIAL PRIMARY KEY,
title TEXT, -- ستون کوچک
content TEXT -- ستون بزرگ
);
-- درج یک ردیف با دادهٔ کوچک
INSERT INTO toast_demo (title, content)
VALUES ('مطلب کوتاه', 'سلام دنیا!');
-- درج یک ردیف با دادهٔ بسیار بزرگ
INSERT INTO toast_demo (title, content)
SELECT 'مطلب بلند', repeat('PostgreSQL TOAST ', 10000);
-- یافتن OID جدول TOAST مرتبط
SELECT reltoastrelid::regclass AS toast_table
FROM pg_class
WHERE relname = 'toast_demo';
خروجی نمونه:
toast_table
-------------------------------
pg_toast.pg_toast_16395
-- مشاهدهٔ چگونگی تقسیم داده به قطعات
SELECT
chunk_id,
chunk_seq,
pg_size_pretty(length(chunk_data)::bigint) AS chunk_size
FROM pg_toast.pg_toast_16395
ORDER BY chunk_id, chunk_seq
LIMIT 5;
SELECT
pg_size_pretty(pg_relation_size('toast_demo')) AS main_table_size,
pg_size_pretty(pg_total_relation_size('toast_demo')) AS total_size,
pg_size_pretty(pg_total_relation_size('toast_demo') - pg_relation_size('toast_demo')) AS toast_size;
این تفاوت نشان میدهد چه حجمی از داده به TOAST منتقل شده است.
چهار استراتژی برای مدیریت ستونهای بزرگ وجود دارد که میتوانید بسته به نیاز خود انتخاب کنید :
| استراتژی | فشردهسازی | ذخیرهٔ خارج از صفحه | کاربرد |
|---|---|---|---|
| PLAIN | ❌ | ❌ | ستونهای همیشه کوچک |
| EXTENDED (پیشفرض) | ✅ | ✅ | تعادل بین فضا و کارایی |
| EXTERNAL | ❌ | ✅ | دسترسی سریع به بخشی از داده (مثل substring) |
| MAIN | ✅ | ❌ (فقط در آخرین راه) | اولویت نگهداری داده در صفحه |
-- تغییر به استراتژی EXTERNAL برای دسترسی سریعتر به substring
ALTER TABLE toast_demo
ALTER COLUMN content SET STORAGE EXTERNAL;
-- تنظیم آستانه برای یک جدول خاص
ALTER TABLE toast_demo SET (toast_tuple_target = 4096); -- 4KB
با معرفی LZ4 در PostgreSQL 14، انتخاب الگوریتم فشردهسازی اهمیت بیشتری یافته است .
| ویژگی | PGLZ | LZ4 |
|---|---|---|
| سرعت فشردهسازی | متوسط | بسیار بالا |
| سرعت decompress | متوسط | فوقالعاده بالا |
| نسبت فشردهسازی | بهتر | کمی پایینتر |
| مصرف CPU | بیشتر | کمتر |
آزمایش benchmark: در یک تست روی مجموعهای از ایمیلها با ستونهای متنی عریض، استفاده از LZ4 باعث بهبود ۳۷٪ در کارایی کلی و ۷۲٪ در بعضی کوئریهای خاص شد .
-- تنظیم LZ4 به عنوان پیشفرض سراسری
ALTER SYSTEM SET default_toast_compression = 'lz4';
SELECT pg_reload_conf();
CREATE EXTENSION pageinspect;
-- مشاهدهٔ محتوای صفحه ۰
SELECT * FROM heap_page_items(get_raw_page('toast_demo', 0));
CREATE EXTENSION pgstattuple;
SELECT * FROM pgstattuple('toast_demo');
CREATE EXTENSION pg_freespacemap;
SELECT * FROM pg_freespace('toast_demo') LIMIT 10;
اگر زیاد دادهٔ بزرگ بهروزرسانی شود، نسخههای قدیمی در جدول TOAST باقی میمانند.
راهحل: نظارت و VACUUM منظم
-- مشاهدهٔ تعداد نسخههای مرده
SELECT
relname,
n_live_tup,
n_dead_tup,
last_vacuum,
last_autovacuum
FROM pg_stat_user_tables
WHERE relname LIKE 'pg_toast%';
انتخاب استراتژی نامناسب میتواند کارایی را کاهش دهد.
راهنما:
substring روی ستونهای بزرگ استفاده میکنید → EXTERNALEXTENDED با PGLZEXTENDED با LZ4PLAINTOAST به تنهایی کار نمیکند، بلکه با سایر مؤلفههای معماری PostgreSQL هماهنگ است:
| لایه | نقش |
|---|---|
| Heap | ذخیرهٔ رکوردهای اصلی و اشارهگرها |
| TOAST | ذخیرهٔ دادههای بزرگ قطعهقطعه شده |
| FSM (Free Space Map) | مدیریت فضای آزاد در صفحات |
| VM (Visibility Map) | بهینهسازی VACUUM |
این تفکیک وظایف، نمونهای از طراحی ماژولار در سیستمهای پایگاه داده است.
EXTERNAL برای ستونهایی که دسترسی جزئی دارند استفاده کنید-- مشاهدهٔ ۱۰ جدول با بیشترین حجم TOAST
SELECT
nspname AS schema,
relname AS table,
pg_size_pretty(pg_total_relation_size(c.oid)) AS total,
pg_size_pretty(pg_relation_size(c.oid)) AS main,
pg_size_pretty(pg_total_relation_size(c.oid) - pg_relation_size(c.oid)) AS toast
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind = 'r' AND nspname NOT IN ('pg_catalog', 'information_schema')
ORDER BY (pg_total_relation_size(c.oid) - pg_relation_size(c.oid)) DESC
LIMIT 10;
-- فعالسازی LZ4 برای ستونهای متنی جدید
ALTER DATABASE mydb SET default_toast_compression = 'lz4';
-- افزایش آستانه برای جدولی با رکوردهای نسبتاً یکسان
ALTER TABLE logs SET (toast_tuple_target = 8192);
TOAST یک نمونهٔ عالی از مهندسی هوشمندانه در PostgreSQL است. این مکانیزم:
از دیدگاه معماری داده، TOAST نمونۀ indirection-based storage optimization است: با افزودن یک لایهٔ انتزاع (اشارهگرها)، محدودیتهای فیزیکی را بهشکل منطقی مدیریت میکند.
جامعهٔ PostgreSQL همواره در حال بهبود TOAST است. از جمله پروژههای در حال بحث:
درک TOAST برای هر کسی که با PostgreSQL کار میکند ضروری است. این مؤلفه، مانند MVCC، بخشی از DNA این پایگاه داده است. شاید هرگز مستقیماً با آن کار نکنید، اما رفتار آن بر عملکرد، فضای دیسک و طراحی schema شما تأثیر میگذارد.
TOAST به ما میآموزد که گاهی بهترین راه برای مدیریت محدودیتها، پذیرش آنها و ایجاد لایهای هوشمند برای دور زدنشان است. همانطور که در ابتدا گفتیم، واقعاً «بهترین چیز پس از نان برشخورده» است.
🎥 محتوای ویدئویی کارگاه در بخش زیر قابل مشاهده است.