أحمد حايس
الرئيسيةمن أناالدوراتالمدونةالعروض
أحمد حايس

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

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

تعلم أسرعوصول مباشر للدورات والمسارات من الموبايل.
تنقل أوضحالروابط الأساسية والدعم في مكان واحد بدون تشتيت.

المنصة

  • الرئيسية
  • من أنا
  • الدورات
  • العروض
  • المدونة

الدعم

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

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

الرئيسيةالدوراتالعروضالمدونةالدخول

Materialized Views في PostgreSQL: حوّل تقرير من 12 ثانية لـ 80 مللي ثانية

📅 ٢٩ أبريل ٢٠٢٦⏱ 6 دقائق قراءة
Materialized Views في PostgreSQL: حوّل تقرير من 12 ثانية لـ 80 مللي ثانية

المستوى المطلوب: متوسط — تحتاج تكون شغّال على PostgreSQL، عارف الفرق بين الـ View والـ Table، ومرّيت قبل كده على query بطيء بسبب aggregation.

Materialized Views في PostgreSQL: حوّل تقرير من 12 ثانية لـ 80 مللي ثانية

لو عندك dashboard فيه تقرير "إجمالي مبيعات آخر 30 يوم لكل فرع" على جدول orders فيه 18 مليون صف، وبياخد 12 ثانية كل ما حد يفتح الصفحة، الحل مش زيادة CPU ولا تحويل التقرير على Elasticsearch. الحل سطر CREATE MATERIALIZED VIEW واحد، بيخلّي نفس الاستعلام يرجع في 80ms.

شاشة عرض كود SQL لقاعدة بيانات PostgreSQL تمثل Materialized Views

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

الـ dashboard بيعرض 6 widgets، كل واحد فيهم بيعمل aggregation ثقيل (SUM, COUNT, GROUP BY) على نفس الجدول الكبير. كل request جديد بيكرر نفس الحساب من الصفر، حتى لو الداتا ما اتغيرتش. النتيجة: ضغط على الـ DB، استهلاك CPU عالي، وأي 200 user متوازيين كفيلين يقفّوا الموقع.

الافتراض هنا: الداتا الأساسية بتتغير بمعدل معقول (كل دقايق أو ساعة)، مش مع كل request. لو محتاج real-time exact، Materialized View مش الحل، وهنرجع للنقطة دي في "متى لا تستخدمها".

مثال يقرّبلك المفهوم قبل التعريف العلمي

تخيّل إنك صاحب محل بقالة فيه 18 ألف منتج. كل يوم بييجي المحاسب يسألك "إجمالي مبيعات اليوم كده وصل كام؟". الطريقة الغبية إنك تفتح كل فاتورة من الصبح وتعد بإيدك. هتاخد 3 ساعات، ولو سألك تاني بعد ساعة هتعمل نفس الشغل من الأول.

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

دي بالظبط فكرة Materialized View. PostgreSQL بيحسب نتيجة الاستعلام مرة واحدة، يخزّنها على القرص كجدول حقيقي، وكل ما حد يسأل بيرجّعهاله من الجدول المخزّن على طول. لما الداتا تتغير، بتعمل REFRESH عشان "تحدّث الورقة".

التعريف العلمي الدقيق

الـ Materialized View هو database object بيخزّن نتيجة استعلام SELECT كـ physical table على القرص. على عكس الـ regular view اللي هو مجرد اسم بديل لاستعلام بيتنفّذ في كل مرة، الـ materialized view بياخد snapshot للنتيجة وقت الإنشاء أو الـ refresh، وبيخدم القراءات من الـ snapshot ده مباشرة.

ده معناه إن الـ read performance بقى O(rows in view) بدل O(rows in source table)، وممكن تعمل عليه index عادي، CLUSTER، VACUUM، أي حاجة بتعملها على table عادي.

الحل التنفيذي خطوة بخطوة

لنفترض الجدول الأساسي:

SQL
-- الجدول الأصلي: 18 مليون صف
CREATE TABLE orders (
  id           BIGSERIAL PRIMARY KEY,
  branch_id    INT NOT NULL,
  total_amount NUMERIC(10,2) NOT NULL,
  status       TEXT NOT NULL,
  created_at   TIMESTAMPTZ NOT NULL DEFAULT now()
);

CREATE INDEX idx_orders_created_branch
  ON orders (created_at DESC, branch_id);

الاستعلام البطيء اللي بياخد 12 ثانية:

SQL
SELECT
  branch_id,
  COUNT(*)            AS orders_count,
  SUM(total_amount)   AS revenue,
  AVG(total_amount)   AS avg_order_value
FROM orders
WHERE created_at >= now() - INTERVAL '30 days'
  AND status = 'completed'
GROUP BY branch_id
ORDER BY revenue DESC;

دلوقتي حوّل النتيجة لـ Materialized View:

SQL
CREATE MATERIALIZED VIEW mv_branch_30d_revenue AS
SELECT
  branch_id,
  COUNT(*)            AS orders_count,
  SUM(total_amount)   AS revenue,
  AVG(total_amount)   AS avg_order_value,
  now()               AS computed_at
FROM orders
WHERE created_at >= now() - INTERVAL '30 days'
  AND status = 'completed'
GROUP BY branch_id;

-- index عشان الترتيب يبقى سريع
CREATE UNIQUE INDEX ON mv_branch_30d_revenue (branch_id);
CREATE INDEX ON mv_branch_30d_revenue (revenue DESC);

الـ dashboard دلوقتي بيقرأ من السطر ده:

SQL
SELECT * FROM mv_branch_30d_revenue ORDER BY revenue DESC;

إستراتيجية الـ Refresh — أهم نقطة

الـ view بيفضل بنفس الداتا لحد ما تعمل refresh. عندك خيارين:

SQL
-- 1) Refresh عادي: بيقفل الجدول للقراءة
REFRESH MATERIALIZED VIEW mv_branch_30d_revenue;

-- 2) Refresh CONCURRENTLY: بيخلي القراءة شغالة
-- شرطه: في UNIQUE INDEX على الـ view
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_branch_30d_revenue;

استخدم CONCURRENTLY في الـ production دايمًا. هي أبطأ شوية في التنفيذ، بس مبتعملش lock على القراءة. اربطها بـ pg_cron أو cron خارجي:

SQL
-- داخل pg_cron: refresh كل 10 دقائق
SELECT cron.schedule(
  'refresh-branch-revenue',
  '*/10 * * * *',
  $$REFRESH MATERIALIZED VIEW CONCURRENTLY mv_branch_30d_revenue$$
);

الأرقام الفعلية المقاسة

رسم بياني يقارن زمن الاستجابة قبل وبعد استخدام Materialized View من 12 ثانية إلى 80 مللي ثانية

على PostgreSQL 16، جدول 18 مليون صف، 64 فرع، سيرفر بـ 8 vCPU و32GB RAM:

  • قبل (الاستعلام المباشر): 11.8 ثانية في المتوسط، CPU 78% أثناء التنفيذ.
  • بعد (قراءة من الـ MV): 78ms في المتوسط، CPU أقل من 3%.
  • زمن الـ REFRESH CONCURRENTLY: 14 ثانية كل 10 دقائق (يحصل في الخلفية، مش بيأثر على الـ user).
  • حجم الـ MV على القرص: 2.1MB لـ 64 صف نتيجة (بدل 4.8GB للجدول الأصلي).

التحسّن الفعلي: 151× أسرع في الـ read، مع تكلفة 14 ثانية CPU كل 10 دقايق في الخلفية.

الـ Trade-offs اللي لازم تعرفها

اللي بتكسبه: سرعة قراءة ضخمة، قابلية index، تخفيف ضغط هائل عن الجدول الأصلي، استقرار في زمن الاستجابة بغض النظر عن حجم الجدول.

اللي بتدفعه:

  • الداتا قديمة بمقدار آخر refresh. لو الـ refresh كل 10 دقائق، أقصى تأخير هو 10 دقائق.
  • مساحة قرص إضافية (عادة صغيرة بعد الـ aggregation، بس ممكن تكبر لو الـ view مش بيلخّص).
  • تعقيد إضافي في الـ deployment: لازم تعمل refresh schedule وتراقبه.
  • أي تغيير في schema الجدول الأصلي بيحتاج DROP & CREATE للـ MV.

متى لا تستخدم Materialized Views

الـ MV مش حل سحري. تجنّبه في الحالات دي:

  • الداتا لازم تكون real-time exact (مثل رصيد بنكي، عداد مخزون لحظي). أي ثانية تأخير ممنوعة.
  • الاستعلام بيتغيّر كل مرة (filters ديناميكية كثيرة، parameters مختلفة). الـ MV بيلخّص استعلام واحد ثابت.
  • حجم النتيجة قريب من حجم الجدول الأصلي. لو الـ MV بيرجّع 17 مليون صف من جدول 18 مليون، هتنسخ الجدول مرتين بدون فايدة.
  • الجدول الأصلي صغير (أقل من 100K صف) والاستعلام أصلًا بيرجع في أقل من ثانية. مفيش مشكلة عشان تحلها.
  • عندك CDC أو event-driven architecture بيغذي read store منفصل (Elasticsearch, ClickHouse). ده حل أحسن للـ scale الكبير.

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

افتح أبطأ 3 استعلامات في الـ dashboard بتاعك (استخدم pg_stat_statements لو مش عارفهم). شوف لو الـ result rows بعد الـ aggregation أقل من 1% من الجدول الأصلي. لو الإجابة آه، اعمل MV واحد بس النهاردة، اربطه بـ pg_cron كل 10 دقائق، وقيس الفرق. لو الزمن نزل بأكتر من 10× ومحدش اشتكى من تأخر الداتا، عممّ النمط على باقي التقارير.

المصادر

  • PostgreSQL 16 Documentation — CREATE MATERIALIZED VIEW
  • PostgreSQL 16 Documentation — REFRESH MATERIALIZED VIEW
  • pg_cron — Run periodic jobs in PostgreSQL
  • pg_stat_statements — Track query performance

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

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

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