الرئيسيةمن أناالدوراتالمدونةسوق الأوامرالمناهج والباقاتالشركاء

دورات عربية متخصصة في التقنية والبرمجة والذكاء الاصطناعي.

المنصة مبنية على الوضوح، التطبيق، والنتيجة النافعة: شرح مرتب يساعدك تفهم الأدوات، تكتب كودًا أفضل، وتستخدم الذكاء الاصطناعي بوعي داخل العمل الحقيقي.

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • المناهج والباقات
  • سوق الأوامر
  • المدونة

الدعم

  • الأسئلة الشائعة
  • تواصل معنا
  • سياسة الخصوصية
  • شروط استخدام التطبيق
  • سياسة الاسترجاع

© 2026 أحمد حايس. جميع الحقوق محفوظة.

الرئيسيةالدوراتالمناهجالمدونةالدخول
Optimizing بالعربي

ليه SELECT COUNT(*) بطيء على الملايين، وإزاي تعدّه في مللي ثانية

محترف15 أغسطس 20265 دقائق قراءة
ليه SELECT COUNT(*) بطيء على الملايين، وإزاي تعدّه في مللي ثانية

المستوى المطلوب: محترف — يفترض المقال إنك مرتاح مع PostgreSQL و EXPLAIN ANALYZE و triggers، وإن جدولك فيه ملايين الصفوف والـ autovacuum شغّال.

ليه SELECT COUNT(*) بطيء على الملايين، وإزاي تعدّه في مللي ثانية

لو صفحة لوحة التحكم بتعلّق ثانيتين أو تلاتة عشان بتعرض إجمالي عدد الصفوف، المشكلة مش في السيرفر ولا في الشبكة. المشكلة إن SELECT COUNT(*) على جدول فيه 50 مليون صف بيعمل فحص تسلسلي كامل. هتتعلّم هنا تنزّل الزمن من ~9.4 ثانية لـ ~3 مللي ثانية، والأهم: إمتى التقدير ده مقبول وإمتى لأ.

المشكلة باختصار

العدّ الإجمالي بيظهر في أماكن كتير: ترويسة صفحة إدارة، عدّاد نتائج بحث، أو حساب عدد الصفحات في الـ pagination. كل ما الجدول يكبر، العدّ بيبطأ خطيًا. النتيجة: صفحة بتحمّل في 9 ثواني بدل جزء من الثانية، والمستخدم بيفتكر الموقع واقع.

ليه COUNT(*) بطيء أصلًا

خلّينا نقرّبها بمثال الأول. تخيّل استاد فيه 50 ألف متفرج، وحد سألك: كام واحد قاعد دلوقتي بالظبط؟ مفيش رقم مخزّن جاهز، فمضطر تعدّ راس راس. لكن شبّاك التذاكر عنده رقم التذاكر المباعة، وده تقدير قريب جدًا في جزء من الثانية.

علميًا، PostgreSQL بيعمل نفس الحكاية. مفيش رقم إجمالي واحد مخزّن للجدول، والسبب هو نموذج التزامن MVCC. كل معاملة (transaction) ممكن تشوف مجموعة صفوف مختلفة حسب الـ snapshot بتاعها. فعلشان يجاوب على COUNT(*) بدقة، لازم يمرّ على كل صف ويتأكد إنه مرئي للمعاملة الحالية. ده بيخلّي التكلفة O(n). حتى لو استخدم index-only scan، هو لسه بيقرأ الفهرس كله، فبيبقى أسرع من فحص الـ heap بس لسه خطّي.

الحلول مرتّبة بالأولوية

  1. تقدير فوري بـ reltuples للأعداد الإجمالية اللي تقبل خطأ بسيط.
  2. تقدير المخطِّط (Plan Rows) لعدد صفوف استعلام مُفلتر بدون ما تنفّذه.
  3. عدّاد مُصان بـ trigger لما تحتاج رقم دقيق بقراءة O(1).

1) التقدير الفوري من إحصائيات المخطِّط

SQL
-- الطريقة البطيئة: فحص كامل على 50 مليون صف
EXPLAIN ANALYZE SELECT COUNT(*) FROM events;
-- Seq Scan on events ... actual time=9411.802..9411.803

-- التقدير الفوري: رقم reltuples من كتالوج النظام
SELECT reltuples::bigint AS estimated_rows
FROM pg_class
WHERE relname = 'events';
-- ~50000000   (زمن التنفيذ أقل من 3ms)

-- reltuples بيتحدّث مع ANALYZE و autovacuum. لو الرقم قديم، حدّثه:
ANALYZE events;

الرقم ده بيتحدّث كل ما autovacuum يشغّل ANALYZE. دقته عادة في حدود 1% إلى 5% على جدول نشِط، بس ممكن ينحرف كتير مباشرة بعد إدخال دفعة كبيرة قبل ما الإحصائيات تتحدّث.

2) تقدير عدد صفوف استعلام مُفلتر

SQL
-- عايز تقدير لعدد الصفوف اللي status='active' من غير ما تعدّها فعلاً؟
-- خُد رقم "Plan Rows" من خرج المخطِّط:
EXPLAIN (FORMAT JSON)
SELECT * FROM events WHERE status = 'active';
-- "Plan Rows": 1240418   ← تقدير المخطِّط، بيرجع فورًا

3) عدّاد دقيق مُصان بـ trigger

CREATE TABLE row_counts (table_name text PRIMARY KEY, n bigint);
INSERT INTO row_counts VALUES ('events', (SELECT count(*) FROM events));

CREATE FUNCTION bump_events_count() RETURNS trigger AS $$
BEGIN
  IF TG_OP = 'INSERT' THEN
    UPDATE row_counts SET n = n + 1 WHERE table_name = 'events';
  ELSIF TG_OP = 'DELETE' THEN
    UPDATE row_counts SET n = n - 1 WHERE table_name = 'events';
  END IF;
  RETURN NULL;
END; $$ LANGUAGE plpgsql;

CREATE TRIGGER trg_events_count
AFTER INSERT OR DELETE ON events
FOR EACH ROW EXECUTE FUNCTION bump_events_count();

-- القراءة بقت O(1):
SELECT n FROM row_counts WHERE table_name = 'events';  -- أقل من 1ms

المقارنة بالأرقام (50 مليون صف)

SELECT COUNT(*) — Seq Scan
9,400 ms
COUNT(*) بـ index-only scan
2,100 ms
تقدير reltuples من pg_class
3 ms
أرقام تقديرية مقاسة على جدول ~50M صف؛ بتتغير حسب العتاد وإعدادات الـ vacuum.

الـ trade-offs

  • reltuples: بتكسب زمن شبه فوري، بتخسر الدقة المطلقة. الرقم بينحرف بين تشغيلتين لـ ANALYZE، وممكن يبقى بعيد جدًا بعد إدخال دفعي مباشر.
  • index-only scan: بتكسب رقم دقيق أسرع من فحص الـ heap، بتخسر إنه لسه O(n) على الفهرس، ومحتاج الـ visibility map يكون محدّث بـ VACUUM.
  • عدّاد الـ trigger: بتكسب قراءة O(1) دقيقة، بتخسر إن صف العدّاد الواحد بيبقى نقطة تنازع (hotspot) تحت الكتابة المتزامنة العالية، وبتضيف زمن على كل INSERT/DELETE. البديل: عدّادات جزئية لكل قسم (partition) تُجمع، أو trigger على مستوى الجملة بـ transition tables.

متى لا تستخدم هذه الطريقة

لو محتاج رقم دقيق للحظة معيّنة لأغراض فوترة أو محاسبة أو التزام قانوني، التقدير مش مقبول؛ استخدم عدّ فعلي أو عدّاد مُصان بمعاملة متسقة. ولو الجدول صغير (أقل من 100 ألف صف)، COUNT(*) أصلًا بيرجع في أقل من 50ms، فالتعقيد الزيادة مش مبرَّر. ولو محتاج عدّ لاستعلامات مُفلترة بشروط عشوائية متغيّرة، العدّاد المُصان غير عملي؛ اعتمد على تقدير المخطِّط أو اقبل الفحص.

الخطوة التالية

شغّل EXPLAIN ANALYZE SELECT COUNT(*) على أكبر جدول عندك، وقارن الزمن بنتيجة استعلام reltuples من pg_class. لو الصفحة محتاجة إجمالي تقريبي بس، بدّل الاستعلام دلوقتي وسجّل الفرق في زمن تحميل الصفحة.

المصادر

  • PostgreSQL Docs — pg_class (حقل reltuples): https://www.postgresql.org/docs/current/catalog-pg-class.html
  • PostgreSQL Docs — Row Estimation وEXPLAIN: https://www.postgresql.org/docs/current/using-explain.html
  • PostgreSQL Docs — Routine Vacuuming و autovacuum وتحديث الإحصائيات: https://www.postgresql.org/docs/current/routine-vacuuming.html
  • PostgreSQL Docs — نموذج التزامن MVCC: https://www.postgresql.org/docs/current/mvcc-intro.html
  • PostgreSQL Wiki — Slow Counting وطرق التقدير: https://wiki.postgresql.org/wiki/Slow_Counting
  • PostgreSQL Docs — Trigger Functions (transition tables): https://www.postgresql.org/docs/current/plpgsql-trigger.html

هل استفدت من المقال؟

اطّلع على المزيد من المقالات والدروس المجانية من نفس المسار المعرفي.

تصفّح المدونة