تخطَّ إلى المحتوى
ARAB CODEمجتمع المطورين العرب

الفهارس والمعاملات في PostgreSQL: استعلامات سريعة وبيانات متسقة

درسنُشر ٢٤ أكتوبر ٢٠٢٦ · 0 رد · 1 مشاهدة#postgresql#فهارس#معاملات#أداء
قبل ساعتين

مقدمة

قاعدة بيانات صغيرة تبدو سريعة مهما كتبت. ثم يصل الجدول إلى مليون صف فيصبح الاستعلام نفسه يستغرق ثواني، أو يضغط مستخدمان الزر نفسه في اللحظة نفسها فتُصرف المكافأة مرتين. الفهارس تعالج الأولى، والمعاملات تعالج الثانية.

أهداف التعلم

  • فهم ما يفعله الفهرس وتكلفته.
  • قراءة EXPLAIN ANALYZE.
  • اختيار فهرس مركب وفهرس جزئي.
  • استخدام المعاملات ومنع حالات السباق.

1. ما الفهرس؟

الفهرس (B-tree افتراضياً) بنية مرتبة تتيح إيجاد الصفوف دون قراءة الجدول كله، مثل فهرس كتاب. التكلفة: مساحة إضافية، وكل INSERT/UPDATE يحدّث الفهارس أيضاً. لذلك لا تفهرس كل عمود.

2. اقرأ خطة التنفيذ

EXPLAIN ANALYZE
SELECT id, title FROM topics
WHERE author_id = 42
ORDER BY created_at DESC
LIMIT 20;

إن رأيت Seq Scan on topics على جدول كبير ووقتاً مرتفعاً، فالقاعدة تقرأ كل الصفوف. أضف فهرساً:

CREATE INDEX CONCURRENTLY topics_author_created_idx ON topics (author_id, created_at DESC);

ستتحول الخطة إلى Index Scan ويقرأ الاستعلام نحو 20 صفاً فقط. CONCURRENTLY ينشئ الفهرس دون قفل الكتابة على الجدول في الإنتاج.

3. الفهرس المركب وترتيب الأعمدة

الفهرس (author_id, created_at) يخدم:

  • WHERE author_id = ?
  • WHERE author_id = ? ORDER BY created_at

لكنه لا يخدم جيداً WHERE created_at > ? وحدها، لأن العمود الأول هو المفتاح الأساسي للترتيب. ضع أولاً العمود الذي تصفي عليه بالمساواة.

4. الفهرس الجزئي

-- معظم الاستعلامات على المواضيع غير المحذوفة فقط
CREATE INDEX topics_live_idx ON topics (created_at DESC) WHERE deleted_at IS NULL;

أصغر وأسرع لأنه يتجاهل الصفوف المحذوفة.

5. المعاملات وACID

المعاملة تجمع عدة عمليات بحيث تنجح كلها أو تفشل كلها:

BEGIN;
UPDATE wallets SET balance = balance - 50 WHERE user_id = 1 AND balance >= 50;
UPDATE wallets SET balance = balance + 50 WHERE user_id = 2;
INSERT INTO wallet_transactions (from_user, to_user, amount) VALUES (1, 2, 50);
COMMIT;

إن فشلت أي خطوة نفذ ROLLBACK ولن يُخصم شيء. لاحظ الشرط balance >= 50 داخل UPDATE نفسه: هو الذي يمنع الرصيد السالب، لا فحص منفصل في الكود قبله.

6. حالات السباق: لا «اقرأ ثم اكتب» دون حماية

مثال خاطئ لاستلام مكافأة يومية:

1. هل استلم اليوم؟ (SELECT) ← لا
2. أضف المكافأة (INSERT)

طلبان متزامنان قد يقرآن «لا» معاً فيستلمان مرتين. الحلول:

-- أ) قيد فريد يجعل القاعدة ترفض التكرار
CREATE UNIQUE INDEX checkins_user_day ON checkins (user_id, day);
INSERT INTO checkins (user_id, day) VALUES (1, current_date) ON CONFLICT DO NOTHING;
-- ب) قفل الصف المعني داخل المعاملة
BEGIN;
SELECT balance FROM wallets WHERE user_id = 1 FOR UPDATE;
-- ... التحقق والتعديل
COMMIT;

القيد الفريد هو الضمان الأقوى والأبسط عادةً.

7. مستويات العزل باختصار

الافتراضي في PostgreSQL هو READ COMMITTED. للعمليات التي تحتاج رؤية متسقة صارمة يوجد REPEATABLE READ وSERIALIZABLE، لكن الأخير قد يُفشل المعاملات المتعارضة فيجب أن يعيد الكود المحاولة.

تمرين عملي

  1. أنشئ جدولاً بمليون صف: INSERT INTO topics (author_id, title) SELECT (random()*1000)::int, 'عنوان موضوع رقم ' || g FROM generate_series(1, 1000000) g;
  2. قِس استعلام آخر 20 موضوعاً لمؤلف معين قبل الفهرس وبعده بـ EXPLAIN ANALYZE.
  3. اكتب سكربتاً يرسل 10 طلبات متزامنة لاستلام مكافأة، وتحقق أن القيد الفريد يسمح بواحدة فقط.

أخطاء شائعة

  • فهرس لكل عمود «احتياطاً».
  • إنشاء فهرس على جدول إنتاجي كبير دون CONCURRENTLY.
  • فحص الشرط في الكود ثم التحديث في استعلام منفصل دون قيد أو قفل.
  • معاملات طويلة تبقي الأقفال مفتوحة أثناء طلب شبكة خارجي.

الخلاصة

قِس بـ EXPLAIN ANALYZE ثم فهرس ما يستحق بترتيب أعمدة صحيح. واجعل الاتساق مسؤولية القاعدة: معاملات للعمليات المتعددة، وقيود فريدة أو أقفال لمنع حالات السباق.

مصادر مقترحة

  • توثيق PostgreSQL: «Indexes» و«Transaction Isolation».
  • موقع Use The Index, Luke.
  • أداة explain.depesz.com لقراءة خطط التنفيذ.

الرد على الموضوع

سجّل الدخول للمشاركة في النقاش.

نقاشات مشابهة