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

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

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

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

المنصة

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

الدعم

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

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

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

Materialized Views في PostgreSQL للمتوسط: حوّل تقرير 4 دقائق لاستعلام 30 مللي ثانية

📅 ٨ مايو ٢٠٢٦⏱ 5 دقائق قراءة
Materialized Views في PostgreSQL للمتوسط: حوّل تقرير 4 دقائق لاستعلام 30 مللي ثانية

المستوى المطلوب: متوسط — مفترض إن عندك إلمام بـ SQL أساسي و JOIN و INDEX، وعملت deploy لـ PostgreSQL مرة واحدة على الأقل.

لو dashboard المبيعات عندك بياخد 4 دقائق يفتح، المشكلة مش في حجم البيانات. المشكلة إن نفس الـ JOIN على 18 مليون صف بيتحسب من الصفر كل ما حد يضغط refresh. Materialized View بسطر SQL واحد بيخزّن نتيجة الاستعلام على القرص، فالاستعلام التاني بيرد في 30ms بدل 240,000ms. ركز على الفرق ده — ده 8000x تحسين من غير ما تلمس الكود.

Materialized Views: الفرق بين الـ View العادي وحفظ النتيجة على القرص

شاشة تعرض رسوم بيانية تحليلية ولوحة تقارير PostgreSQL لمعدلات الاستجابة قبل وبعد Materialized Views

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

عندك جدول orders فيه 18 مليون صف، وجدول customers فيه 240 ألف، وجدول products فيه 12 ألف. الـ dashboard بيعرض إجمالي المبيعات لكل تصنيف منتج لكل دولة آخر 90 يوم. الاستعلام بيتعمل JOIN ثلاثي + GROUP BY + window function. الزمن: 240 ثانية في الذروة، 90 ثانية في الـ off-peak.

المدير بيفتح الـ dashboard 18 مرة في اليوم. يعني إنت بتحرق 72 دقيقة من الـ DB يومياً على نفس الحساب اللي بيرجّع نفس النتيجة. لو 4 مدراء بيفتحوا في نفس اللحظة، الـ DB بتقفل والـ API بيرجع 504 timeout.

الفكرة بمثال محل العصير

تخيل إن في محل عصير، الزبون كل ما يطلب كوب برتقال، البائع بيعصر 8 برتقالات قدامه على الفور. المحل ساعة الذروة بياخد طلب واحد كل دقيقتين. الحل اللي البائع الذكي بيعمله: يعصر 50 كوب الصبح، يحطهم في الفريزر، وكل ما حد يطلب يديله الكوب جاهز في 5 ثواني.

الـ View العادي زي البائع اللي بيعصر كل مرة. الـ Materialized View زي البائع اللي عصر مسبقاً وحط الكوب في الفريزر. الفرق: مع Materialized View النتيجة محفوظة على القرص، مش بتتحسب من جديد لما حد يستعلم منها.

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

الـ View العادي في SQL ده عبارة عن استعلام محفوظ بالاسم. لما تستعلم منه، PostgreSQL بيستبدل اسمه بنص الاستعلام كامل و ينفّذه. Materialized View بالظبط هو snapshot من نتيجة الاستعلام، محفوظة كجدول حقيقي على القرص (relation type 'm' في pg_class). بيتحدّث صراحةً بأمر REFRESH MATERIALIZED VIEW. الفكرة دي اتقدمت أول مرة في ورقة Larson و Yang 1985 وProtocols الأكاديمية للـ deferred view maintenance، واتضافت لـ PostgreSQL في إصدار 9.3 سنة 2013.

الكود: من 240 ثانية لـ 30 مللي ثانية

الاستعلام البطيء الأصلي:

SQL
SELECT 
    p.category,
    c.country,
    SUM(o.total_amount) AS revenue,
    COUNT(*) AS orders_count
FROM orders o
JOIN customers c ON c.id = o.customer_id
JOIN products p ON p.id = o.product_id
WHERE o.created_at >= NOW() - INTERVAL '90 days'
GROUP BY p.category, c.country;
-- Execution time: 241,847 ms على PostgreSQL 16، 16GB RAM

تحويله لـ Materialized View:

SQL
CREATE MATERIALIZED VIEW mv_sales_by_category_country AS
SELECT 
    p.category,
    c.country,
    SUM(o.total_amount) AS revenue,
    COUNT(*) AS orders_count,
    NOW() AS last_refreshed_at
FROM orders o
JOIN customers c ON c.id = o.customer_id
JOIN products p ON p.id = o.product_id
WHERE o.created_at >= NOW() - INTERVAL '90 days'
GROUP BY p.category, c.country;

-- Index ضروري عشان REFRESH CONCURRENTLY يشتغل
CREATE UNIQUE INDEX idx_mv_sales_unique 
ON mv_sales_by_category_country (category, country);

-- الاستعلام الجديد من الـ dashboard:
SELECT * FROM mv_sales_by_category_country
WHERE country = 'EG';
-- Execution time: 28 ms

الفرق: 241,847ms → 28ms. التطبيق بياخد نفس البيانات بـ 8636x سرعة. الـ dashboard دلوقتي يقدر يخدم 50 مدير متزامن بدل ما يقفل عند 4.

رسم بياني خطي يقارن زمن استجابة الاستعلام قبل وبعد تطبيق Materialized View على جدول 18 مليون صف

التحديث بدون قفل: REFRESH CONCURRENTLY

الـ Materialized View مش بيتحدّث لوحده. لازم تعمل REFRESH. الطريقة العادية بتقفل الـ view أثناء التحديث، يعني الـ dashboard بيرجع خطأ. الحل الصح:

SQL
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_sales_by_category_country;

الـ CONCURRENTLY بيخلّي القراءات شغالة طول مدة التحديث. شرطه إن في unique index على الـ view (اللي عملناه فوق). شغّل الأمر ده كل ساعة بـ pg_cron:

SQL
SELECT cron.schedule(
    'refresh-sales-mv',
    '0 * * * *',
    $$REFRESH MATERIALIZED VIEW CONCURRENTLY mv_sales_by_category_country$$
);

الافتراض هنا: المدير مش محتاج بيانات أحدث من ساعة. لو محتاج لحظية، Materialized View مش الحل، روح للـ caching على مستوى التطبيق أو streaming aggregations.

الـ trade-offs الحقيقية

  1. مساحة القرص: نتيجة المثال فوق حجمها 4.2MB، لكن لو الـ aggregation أوسع ممكن توصل لـ 8GB. بتدفع disk زيادة عشان توفّر CPU.
  2. البيانات قديمة بمقدار فترة الـ refresh: لو REFRESH كل ساعة، الـ dashboard بيشوف بيانات عمرها بين 0 و 60 دقيقة. ده مقبول للتقارير، مش مقبول لـ inventory في الـ checkout.
  3. زمن الـ REFRESH نفسه: بياخد قريب من زمن الاستعلام الأصلي (240 ثانية في مثالنا). يعني خلال الـ refresh، الـ DB CPU بترتفع. جدوله في ساعات الـ off-peak.
  4. تعقيد الصيانة: لما تتعدّل schema الجداول الأصلية، لازم تعمل DROP و CREATE للـ view. يعني عندك dependency خفية في migrations.

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

الحل ده غلط لو:

  • البيانات لازم تكون real-time (مثلاً عرض رصيد البنك أو الـ stock في e-commerce لحظة الشراء).
  • الاستعلام بسيط ويمكن حله بـ index عادي على عمود واحد. ابدأ بـ EXPLAIN ANALYZE قبل ما تقفز للـ Materialized View.
  • الـ DB صغيرة (أقل من مليون صف). الـ overhead مش يستاهل.
  • الكتابة كثيفة جداً والـ refresh هيتعدّى الـ window المتاح. في الحالة دي روح لـ incremental view maintenance بأدوات زي pg_ivm أو Debezium + materialized cache.

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

افتح أبطأ استعلام في الـ dashboard بتاعك دلوقتي. شغّل عليه EXPLAIN ANALYZE. لو الزمن أكتر من 500ms والنتيجة مش لازم تكون لحظية، حوّله لـ Materialized View بالـ pattern اللي فوق وضيف unique index و pg_cron job. لو الـ refresh بياخد أكتر من 5 دقائق على بياناتك، عندنا مقال تاني عن partitioning بيحل ده.

المصادر

  • توثيق PostgreSQL الرسمي — CREATE MATERIALIZED VIEW
  • توثيق PostgreSQL — REFRESH MATERIALIZED VIEW
  • إضافة pg_cron الرسمية — github.com/citusdata/pg_cron
  • Larson, P., Yang, H. Z. (1985). Computing Queries from Derived Relations. VLDB.
  • إضافة Incremental View Maintenance — pg_ivm
  • PostgreSQL 9.3 Release Notes — إضافة Materialized Views

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

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

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