بیشتر دیتابیسهای production نه بهخاطر کوئری بد، بلکه بهخاطر مدیریت بدِ اتصال و تراکنش میخوابند: اتصالهای انباشته، تراکنشهای رها، و الگوی N+1 که در لوکال بیصداست و در مقیاس فاجعه میشود.
اتصال دیتابیس گران است
- ساخت هر connection یعنی TCP handshake + TLS + احراز هویت + تخصیص حافظه سمت دیتابیس (در Postgres هر connection یک process با چند مگابایت RAM).
- Postgres عملاً بعد از چند صد اتصال فعال دچار افت میشود؛ ۲۰ سرویس × ۲۰ connection یعنی ۴۰۰ اتصال — قبل از هر کوئری، باید برای اتصال برنامه ریخت.
- Connection Pool (داخل اپ یا بیرونی مثل PgBouncer) اتصالها را بازاستفاده میکند؛ در transaction mode اتصال فقط برای مدت تراکنش به کلاینت داده میشود — مقیاس اتصال دهها برابر میشود.
- هشدار transaction mode: ویژگیهای session-محور (SET ،prepared statement نامدار، temp table) کار نمیکنند یا باید فعالشان کرد.
N+1: قاتل خاموش
-- بد: ۱ کوئری برای لیست + N کوئری برای جزئیات هر آیتم
SELECT * FROM orders LIMIT 50; -- ۱
SELECT * FROM users WHERE id = ?; -- ×۵۰ بار!
-- خوب: یک کوئری با JOIN یا IN
SELECT * FROM orders JOIN users ON users.id = orders.user_id LIMIT 50;در لوکال با ۱۰ رکورد تست، N+1 یعنی ۱۱ کوئری به loopback — هیچکس نمیفهمد. در production با ۵۰۰ آیتم و RTT واقعی یعنی ۵۰۰ رفتوبرگشت شبکه برای یک صفحه. راهها: JOIN ،IN با batch ،DataLoader در GraphQL، و مانیتورکردن تعداد کوئری هر endpoint.
بهداشت تراکنش
- تراکنش را کوتاه نگه دار: هر لحظه بازماندن، قفلها را نگه میدارد و vacuum/autovacuum را عقب میاندازد.
- Idle in transaction یعنی تراکنشی که رها شده — قفل گرفته و هیچ کاری نمیکند؛ باید timeout داشته باشد (idle_in_transaction_session_timeout).
- ترتیب قفلگرفتن را در همه مسیرها یکسان کن (مثلاً همیشه اول حساب کوچکتر id) تا deadlock نگیری — و retry idempotent برای deadlock های اجتنابناپذیر (فصل ۱۰).
به زبان ساده
اتصال به دیتابیس گران است؛ با استخر اتصال (pool) چند اتصال آماده را بین صدها درخواست شریک میشویم و با دیدن N+1 ،کوئریها را دستهای میکنیم.
مثال واقعی
مثل آسانسور ساختمان: بهجای ساختن آسانسور تازه برای هر مسافر (اتصال تازه)، چند آسانسور ثابت همه را جابهجا میکند؛ و N+1 مثل رفتن به فروشگاه برای هر قلم یکبار است بهجای یک سبد کامل.