طراحی اسکیمای دیتابیس برای اپلیکیشن‌های مقیاس‌پذیر PostgreSQL

طراحی اسکیمای دیتابیس برای اپلیکیشن‌های مقیاس‌پذیر PostgreSQL

۱۴۰۵/۲/۲
10 دقیقه

راهنمای عملی طراحی schema در PostgreSQL برای ساختارهای مقیاس‌پذیر؛ از query path و partitioning تا JSONB، observability و migration امن.

طراحی اسکیمای دیتابیس معمولاً در شروع ساده به نظر می‌رسد. چند جدول، چند کلید، و همه‌چیز روان پیش می‌رود؛ گاهی هم سریع‌تر از چیزی که باید. در ابتدا کار می‌کند. بعد ترافیک بالا می‌رود و همان طراحی اولیه شروع می‌کند به مقاومت. کوئری‌ها کند می‌شوند، رفتار ایندکس‌ها عوض می‌شود، و migrationها پرریسک‌تر می‌شوند. مقیاس‌پذیری از آن چیزی که قرار بود باشد دردناک‌تر می‌شود. همان تصمیم‌های اولیه می‌مانند، مخصوصاً در PostgreSQL. طراحی خوب schema ارزشش را بعدتر نشان می‌دهد؛ معمولاً وقتی چیزی می‌شکند یا timeout می‌خورد.

برای تیم‌هایی که با TypeScript، Next.js، NestJS یا سیستم‌های توزیع‌شده کار می‌کنند، دیتابیس اغلب مهم‌تر از چیزی است که در ابتدا به نظر می‌رسد. دیتابیس بین منطق اپلیکیشن و دادهٔ ذخیره‌شده قرار می‌گیرد و مسیر حرکت requestها و محل بروز خطاها را شکل می‌دهد. این فشار آرام، به‌مرور جمع می‌شود. انتخاب‌های ضعیف در schema، قبل از رسیدن به scale واقعی، به reliability و performance ضربه می‌زنند. تصمیم‌های درست می‌توانند سال‌ها کار پاکسازی را حذف کنند؛ چیزی که تقریباً هیچ‌کس از آن لذت نمی‌برد.

این راهنما روی طراحی عملی schema در PostgreSQL تمرکز دارد و الگوهایی را پوشش می‌دهد که تیم‌ها واقعاً با آن‌ها روبه‌رو می‌شوند. نشان می‌دهد schemaها چطور رشد می‌کنند، چطور observability را پشتیبانی می‌کنند، و چطور انعطاف‌پذیر می‌مانند؛ ذهنیتی که در نوشته‌های مهندسانی مثل Bytezen هم دیده می‌شود، جایی که scalability به یک عادت روزمره تبدیل شده است.

PostgreSQL یکی از قابل‌اعتمادترین دیتابیس‌هایی است که امروز در production استفاده می‌شود و تیم‌ها برای کارهای جدی و بلندمدت روی آن حساب می‌کنند. استفاده از آن همچنان در حال رشد است، مخصوصاً برای سیستم‌هایی که قرار است سال‌ها دوام بیاورند و بدون rewriteهای مداوم scale شوند. این رشد فقط هیاهو نیست. نظرسنجی‌های اخیر نشان می‌دهند که استفادهٔ حرفه‌ای از PostgreSQL همچنان بالاست و تیم‌های زیادی هر روز در محیط واقعی از آن استفاده می‌کنند.

میزان adoption و احساس مثبت نسبت به PostgreSQL

شاخصمقدارسال
adoption توسعه‌دهندگان PostgreSQL55.6%2025
استفادهٔ حرفه‌ای توسعه‌دهندگان58.2%2025
امتیاز محبوب‌ترین دیتابیس65.5%2025
رشد سال‌به‌سال adoption+7 واحد2024، 2025

منبع: Stack Overflow

این رشد مهم است چون PostgreSQL اغلب برای سیستم‌هایی انتخاب می‌شود که باید در طول زمان scale شوند. وقتی به آن نقطه می‌رسید، طراحی schema تقریباً روی همه‌چیز اثر می‌گذارد، حتی اگر در ابتدا این‌قدر مهم به نظر نرسد. اندازهٔ ایندکس‌ها از همین انتخاب‌ها شکل می‌گیرد. الگوهای I/O به نحوهٔ سازمان‌دهی داده وابسته‌اند. هزینهٔ vacuum و رفتار replication هم از سطح schema شروع می‌شوند، معمولاً زودتر از چیزی که بیشتر تیم‌ها انتظار دارند.

بهینه‌سازی طراحی schema برای مقیاس‌پذیری PostgreSQL بنیادی است. یک schema خوب به شما کمک می‌کند به query performance بهتر برسید، عملیات I/O را کاهش دهید، مصرف CPU و memory را بهینه کنید، و حتی فضای ذخیره‌سازی را کمتر کنید.

— Support Engineer, TigerData

Diagramهای تمیزِ entity معمولاً نقطهٔ شروع هستند: users، orders، products، relations. در مراحل اولیه، این روش خوب جواب می‌دهد چون راه سریعی برای هم‌نظر کردن همه است. اما با رشد سیستم، access patternها از normalization سخت‌گیرانه مهم‌تر می‌شوند. Diagram هنوز مفید است، اما به‌مرور دیگر راهنمای اصلی نیست؛ دست‌کم نه به‌تنهایی. این تغییر کاملاً طبیعی است.

چیزی که معمولاً مهم‌تر می‌شود، queryهای کلیدی است. خیلی زود می‌بینید که یک مجموعهٔ کوچک از queryها تقریباً در هر request اجرا می‌شوند. کدام queryها checkout یا sign-in را کند می‌کنند؟ کدام‌ها بی‌سروصدا dashboardها یا jobهای background را تغذیه می‌کنند و هیچ‌کس تا روز خراب شدن، به آن‌ها فکر نمی‌کند؟ این‌ها معمولاً دردناک‌ترین بخش‌ها هستند. schema باید این مسیرها را سریع و قابل‌پیش‌بینی نگه دارد، حتی اگر ساختار نهایی از بیرون کمی نامرتب به نظر برسد.

این موضوع اغلب به denormalization کنترل‌شده می‌رسد. یک راه رایج، تکرار کردن بخش‌های کوچک داده برای جلوگیری از joinهای سنگین است، یا ذخیرهٔ مقدارهای محاسبه‌شده‌ای که بازتولید آن‌ها هزینه‌بر است. این یک تصمیم آگاهانه بر پایهٔ usage واقعی است، نه زیبایی بصری؛ چیزی که در production اهمیت کمتری دارد.

feedهای کاربرمحور مثال خوبی هستند. joinهای پنج‌جدولی در development خوب به نظر می‌رسند، اما ترافیک واقعی همه‌چیز را عوض می‌کند. flatten کردن داده می‌تواند latency را کم کند و فشار lock را پایین بیاورد، و معمولاً هم خیلی زود اثرش را می‌بینید.

طراحی schema در PostgreSQL در scale یعنی پذیرفتن tradeoffها و انتخاب compromiseهایی که با traffic واقعی سازگارند. همان‌طور که Jake Saunders از تجربهٔ واقعی خودش توضیح می‌دهد:

PostgreSQL به ما کنترل و شفافیت داد، اما در عوض طراحی دقیق schema و indexing را طلب می‌کرد.

— Jake Saunders, JakeSaunders.dev

شفافیت زمانی بیشترین ارزش را دارد که بعداً شخص دیگری مجبور شود این tradeoffها را بفهمد.

یکی از رایج‌ترین اشتباه‌ها این است که فرض کنیم یک table کوچک می‌ماند. در سیستم‌های واقعی، این فرض معمولاً زیاد دوام نمی‌آورد. logها، eventها، transactionها و audit tableها معمولاً با گذشت زمان رشد می‌کنند؛ اغلب سریع‌تر از چیزی که انتظار می‌رود. وقتی partitioning دیر اضافه می‌شود، تبدیل به یک cleanup پرتنش می‌شود که تیم‌ها ترجیح می‌دهند اصلاً سراغش نروند.

PostgreSQL حالا native partitioning قابل‌اعتمادی دارد، پس استفادهٔ زودهنگام از آن برای tableهایی که سقف رشد مشخصی ندارند منطقی است. time-based partitioning خیلی رایج است و در سیستم‌های multi-tenant SaaS هم زیاد استفاده می‌شود. در این نقطه، دیگر یک pattern استاندارد است، نه یک استثنا.

شروع با طراحی partition-first در عمل کمک می‌کند. ایندکس‌ها کوچک‌تر می‌مانند و مدیریتشان آسان‌تر می‌شود. vacuum jobها معمولاً سریع‌تر اجرا می‌شوند. policyهای retention ساده‌تر می‌شوند و آرشیو کردن دادهٔ قدیمی، با بزرگ شدن tableها، امن‌تر به نظر می‌رسد. performance خواندن و نوشتن هم معمولاً در طول زمان پیش‌بینی‌پذیرتر می‌ماند.

یک قاعدهٔ سرانگشتی ساده: اگر یک table ممکن است به ده‌ها میلیون ردیف برسد، partitioning را از ابتدا در نظر بگیرید. حتی یک partition اولیه هم می‌تواند مفید باشد. مهندسان Percona هم اشاره می‌کنند که تیم‌هایی که صبر می‌کنند، بعداً با migrationهای سنگین روبه‌رو می‌شوند (Percona).

JSONB قدرتمند است. انعطاف‌پذیر است و به تیم‌ها کمک می‌کند سریع‌تر حرکت کنند؛ چیزی که در ابتدای پروژه معمولاً خیلی خوب به نظر می‌رسد. خیلی از سیستم‌های مدرن، ستون‌های relational را با فیلدهای JSONB ترکیب می‌کنند. این الگو در پروژه‌های واقعی زیاد دیده می‌شود، چون در شروع، از نظر عملی منطقی به نظر می‌رسد.

نکتهٔ اصلی اینجاست: guardrail لازم است. JSONB برای داده‌هایی بهترین انتخاب است که شکلشان مرتب عوض می‌شود یا زیاد query نمی‌شوند. ستون‌های معمولی برای داده‌هایی بهترند که در queryهای پرتکرار یا ایندکس‌ها استفاده می‌شوند؛ مخصوصاً در مسیرهای read-heavy که performance به‌وضوح اهمیت پیدا می‌کند. این تفکیک کمک می‌کند query planها پیش‌بینی‌پذیر بمانند و ایندکس‌های حجیم و دردسرساز ایجاد نشود؛ چیزی که بعداً پاک‌سازی‌اش آزاردهنده است.

پس تیم‌ها معمولاً کجا اشتباه می‌کنند؟ یک اشتباه رایج این است که همه‌چیز را داخل JSONB می‌گذارند چون ساده‌تر به نظر می‌رسد. در scale، این تصمیم اغلب queryها را کند می‌کند و indexing را پیچیده. انعطاف‌پذیری، نیاز به طراحی دقیق schema را حذف نمی‌کند. میان‌بری واقعی وجود ندارد.

Jonathan Katz اشاره می‌کند که حتی با پیشرفت PostgreSQL، اشتباهات schema هنوز هم از علت‌های اصلی performance problemها هستند (Jonathan Katz). JSONB کمک می‌کند، اما جایگزین مدل‌سازی دقیق داده نیست.

مشکل واقعی معمولاً از اولین schema شروع نمی‌شود. اغلب حوالی دهمین migration خودش را نشان می‌دهد؛ وقتی ریسک‌های کوچک روی هم جمع می‌شوند و تیم‌ها حس می‌کنند همه‌چیز کمی ناپایدار شده است. این creep آرام خیلی راحت از چشم پنهان می‌ماند. چیزی که کمک می‌کند این است که schema evolution را یک فرایند امن و تکرارپذیر ببینید؛ با visibility روشن روی اینکه چه چیزی تغییر می‌کند و چه زمانی اجرا می‌شود.

تغییرات backward-compatible معمولاً آرام‌ترین انتخاب هستند. اول اضافه کردن ستون‌ها، بعد حذف ستون‌های قدیمی، و deploy کردن کدی که بتواند هر دو وضعیت را هم‌زمان مدیریت کند، surpriseها را کم می‌کند؛ حتی اگر کار بیشتری بخواهد. داده‌ها می‌توانند آرام حرکت کنند و بعداً مرحله‌به‌مرحله پاک‌سازی شوند.

قابلیت‌های جدید PostgreSQL هم کمک می‌کنند. UUIDهای جدیدتر معمولاً locality ایندکس را بهتر می‌کنند، declarative partitioning مدیریت تغییرات را ساده‌تر می‌کند، و logical replication معمولاً وقتی schemaها در طول زمان پایدار می‌مانند بهترین عملکرد را دارد. وقتی تیم‌ها از migration نترسند، scalability هم همراهش می‌آید؛ مثلاً اضافه کردن یک ستون بدون downtime، به‌جای رفتن سراغ یک rewrite پرخطر.

چند مشکل مدام تکرار می‌شوند و احتمالاً برایتان آشنا هستند. یکی از منابع رایج دردسر، tableهایی هستند که بدون محدودیت رشد می‌کنند و هیچ‌وقت تمیز نمی‌شوند؛ مخصوصاً وقتی رشدشان بی‌صدا اتفاق می‌افتد. از آن طرف، tableهای خیلی کوچک و بیش‌ازحد زیاد هم دردسرساز می‌شوند، چون فشار اضافی روی system catalogها می‌گذارند. PostgreSQL معمولاً schemaهای بزرگ را خوب مدیریت می‌کند، اما schemaهای بسیار بزرگ یا شدیداً fragmented هنوز هم می‌توانند در عمل کندی ایجاد کنند.

طراحی schema-per-tenant هم اغلب دست‌کم گرفته می‌شود. بدون tuning دقیق، این مدل باعث سنگین شدن catalog و افزایش هزینهٔ query planning می‌شود. در طول زمان، multi-tenancy در سطح row همراه با indexing مناسب، معمولاً بهتر scale می‌شود؛ دست‌کم از تجربه‌ای که من دیده‌ام.

نبود observability هم یک مشکل رایج دیگر است. slow queryها و execution planهای آن‌ها معمولاً الگوهای واضحی نشان می‌دهند، مخصوصاً وقتی index usage را دنبال می‌کنید. طراحی schema چیزی نیست که یک بار انجام شود و تمام.

بحث‌های طولانی‌مدت در PostgreSQL core، که اغلب از قابل‌اعتمادترین منابع برای محدودیت‌های دنیای واقعی هستند، نشان می‌دهند catalog bloat و تعداد خیلی زیاد tableها چگونه در production دردسر ایجاد کرده‌اند. جزئیات را اینجا ببینید: PostgreSQL Mailing List.

یک schema در PostgreSQL چقدر باید نرمال باشد تا scale شود؟

از حالت normalized شروع کنید و بعد هرجا برای performance لازم شد denormalize کنید. روی hot query pathها تمرکز کنید. duplication کنترل‌شده اغلب ارزشش را دارد.

چه زمانی باید از partitioning در PostgreSQL استفاده کنم؟

برای tableهایی که رشد نامحدود دارند از partitioning استفاده کنید. داده‌های time-series و multi-tenant گزینه‌های بسیار مناسبی هستند. از ابتدا برای آن برنامه‌ریزی کنید.

آیا JSONB برای performance در scale بد است؟

نه، اما استفادهٔ نادرست از آن بد است. JSONB را برای داده‌های انعطاف‌پذیر به کار ببرید. فیلدهای قابل‌جست‌وجو و پرتکرار را relational نگه دارید.

تغییرات schema چه اثری بر مقیاس‌پذیری دیتابیس دارند؟

migrationهای ناامن باعث downtime و bug می‌شوند. evolution سازگار با نسخه‌های قبلی، سیستم را هم‌زمان با رشدش قابل‌اعتماد نگه می‌دارد.

آیا PostgreSQL بدون sharding هم scale می‌شود؟

بله، خیلی هم زیاد. طراحی خوب schema، partitioning و indexing اغلب sharding را به تعویق می‌اندازند یا حتی بی‌نیاز می‌کنند.

چیزی که معمولاً سیستم‌های PostgreSQL مقیاس‌پذیر را از سیستم‌های دردسرساز جدا می‌کند، نیت درست از روز اول است. درست کردن مشکلات بعداً وسوسه‌کننده به نظر می‌رسد، اما معمولاً جواب نمی‌دهد. طراحی schema به‌طور پنهان performance، reliability و سرعت حرکت تیم را شکل می‌دهد؛ اغلب بیشتر از چیزی که تصور می‌شود. خواهید دید که query pathهایی که واقعاً استفاده می‌کنید مهم‌ترین بخش‌اند. یک رویکرد مفید این است که از قبل به partitioning فکر کنید. و دربارهٔ JSONB؟ قدرتمند است، اما به‌سادگی می‌شود بیش‌ازحد از آن استفاده کرد؛ چیزی که بارها دیده‌ام. evolution schema هم اینجا اهمیت دارد، اغلب بیشتر از چیزی که تیم‌ها از قبل برایش برنامه‌ریزی می‌کنند.

اگر بخواهم یک نتیجه‌گیری اصلی بدهم، این است که طراحی schema خودش یک لایهٔ واقعی از architecture است. باید همان‌قدر با دقت دیده شود که APIها و سرویس‌ها دیده می‌شوند. PostgreSQL ثبات و فضای رشد می‌دهد و در طول زمان این ترکیب کمک می‌کند، مخصوصاً وقتی دیتابیس از همان اول با احترام با آن رفتار شده باشد، نه اینکه schema آن بیش‌ازحد روی JSONB تکیه کرده باشد.

۱۴۰۵/۲/۲