تقنيات وتطبيق
فهارس PostgreSQL وتحسين أداء الاستعلامات في بايثون
١٤ أكتوبر ٢٠٢٦ · 9 دقائق قراءة
عندما ينهار التطبيق تحت الحمل: المشكلة ليست بايثون
في معظم تطبيقات بايثون و FastAPI التي تعاني من بطء شديد تحت الضغط، يكون الاتهام الأول دائمًا موجهًا إلى بايثون: «بايثون بطيئة وغير متزامنة بالشكل الكافي». ولكن عند وضع مسبار القياس والتتبع، نكتشف في 95% من الحالات أن المشكلة الحقيقية تقع في طبقة قاعدة البيانات: استعلامات تفحص ملايين السجلات بالكامل (Sequential Scans)، أو غياب فهارس حاسمة، أو الوقوع في فخ استعلامات N+1 الشهيرة.
1. أداة التشخيص الأولى: قراءة EXPLAIN ANALYZE
قبل إضافة أي فهرس بشكل عشوائي، يجب أن تسأل قاعدة البيانات مباشرة عن خطة التنفيذ الحقيقية:
EXPLAIN (ANALYZE, BUFFERS, COSTS)
SELECT id, title, created_at
FROM orders
WHERE tenant_id = 'tenant_42' AND status = 'completed'
ORDER BY created_at DESC
LIMIT 20;
ما تبحث عنه في المخرجات:
Seq Scan on orders: يعني أن المحرك يقرأ الجدول كاملاً من القرص سطرًا بسطر. إذا كان الجدول يحتوي على نصف مليون سجل، فهذا استنزاف فوري للذاكرة وI/O.Index ScanأوIndex Only Scan: الهدف المثالي؛ المحرك يقرأ فقط شجرة الفهرس (B-Tree) ويصل إلى النتائج مباشرة بزمن يقاس بأجزاء من الملي ثانية.Buffers: shared hit=... read=...: يوضح كم صفحة تم جلبها من الذاكرة المخبأة (RAM) مقابل التي استدعت قراءة بطيئة من القرص.
2. الفهارس المركبة (Composite Indexes): الترتيب يصنع الفارق
إذا كنت تبحث دائمًا بمعيارين معًا:
WHERE tenant_id = 'tenant_42' AND status = 'completed'
فإن إنشاء فهرسين منفصلين على كل عمود لن يمنحك الأداء المطلوب مقارنة بـ فهرس مركب واحد:
CREATE INDEX idx_orders_tenant_status_created
ON orders (tenant_id, status, created_at DESC);
قاعدة ترتيب الأعمدة في الفهرس المركب:
- أعمدة التطابق الدقيق (Equality) أولاً: (
tenant_id,status). - أعمدة النطاق أو الترتيب (Range / Order By) أخيرًا: (
created_at).
بهذا الترتيب، يستطيع المحرك القفز مباشرة إلى قسم المستأجر، ثم تصفية الحالة، واستخراج السجلات مرتبة جاهزة دون الحاجة لعملية فرز في الذاكرة (Sort Method: top-N heapsort).
3. الفهارس الجزئية (Partial Indexes): وفر 80% من مساحة الفهرس
إذا كان 90% من الطلبات في نظامك تستعلم فقط عن الطلبات المعلقة (status = 'pending')، فلماذا تفهرس ملايين الطلبات القديمة المنتهية؟
استخدم الفهرس الجزئي:
CREATE INDEX idx_pending_orders
ON orders (created_at)
WHERE status = 'pending';
هذا الفهرس يزن جزءًا ضئيلاً جدًا من الذاكرة، ويتم تحديثه بسرعة فائقة، ويعطي أقصى سرعة ممكنة لمعالجي الطوابير.
4. القضاء على كابوس N+1 في Async SQLAlchemy
في بايثون، عندما تستعلم عن قائمة المستخدمين وتطلب فواتير كل مستخدم في حلقة دوران:
# كارثة N+1: استعلام لجلب 50 مستخدم، يتبعه 50 استعلام لجلب فواتيرهم!
users = await session.scalars(select(User).limit(50))
for user in users:
print(user.invoices) # يطلق استعلاماً منفرداً لكل مستخدم!
الحل باستخدام التحميل الاستباقي الصريح (selectinload أو joinedload):
from sqlalchemy.orm import selectinload
stmt = select(User).options(selectinload(User.invoices)).limit(50)
users = await session.scalars(stmt)
# تم تنفيذ استعلامين فقط بدلاً من 51 استعلاماً!
الخلاصة
تحسين أداء قواعد البيانات لا يحتاج إلى سحر أو إعدادات خفية؛ بل يعتمد على قياس دقيق عبر EXPLAIN ANALYZE، واستخدام فهارس مركبة وجزئية مرتبة بدقة، والتحكم في سلوك استعلامات الـ ORM في بايثون.