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

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

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

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

المنصة

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

الدعم

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

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

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

Materialized Views في PostgreSQL للمحترف: من تقرير 8 ثوانٍ لـ 40ms بدون لمس كود التطبيق

📅 ١١ مايو ٢٠٢٦⏱ 8 دقائق قراءة
Materialized Views في PostgreSQL للمحترف: من تقرير 8 ثوانٍ لـ 40ms بدون لمس كود التطبيق

مستوى المقال: محترف (Advanced) — يفترض إنك تعرف PostgreSQL، EXPLAIN ANALYZE، و GROUP BY aggregations. لو لسه بتبدأ مع indexes، ابدأ بمقال B-Tree للمبتدئ الأول.

Materialized Views في PostgreSQL: السلاح الخفي لتحويل تقاريرك من 8 ثوانٍ لـ 40 مللي ثانية

لو dashboard المبيعات الأسبوعي عند المدير بياخد 8 ثوانٍ يفتح، PostgreSQL مش بطيء. المشكلة إن الـ query بيعمل JOIN ثلاثي على 240 مليون سطر و GROUP BY على date_trunc و SUM في كل مرة المدير يضغط refresh. Materialized View بـ 14 سطر SQL بيخزّن النتيجة كجدول فيزيكال على القرص، والـ query التالية بترجع في 38 مللي ثانية بدون لمس سطر كود واحد في تطبيقك.

لوحة تحليلات تعرض رسوم بيانية لأداء قاعدة بيانات PostgreSQL وزمن الاستجابة على شاشة لاب توب

المشكلة باختصار: ليه الـ aggregation بياكل وقتك

السيناريو الحقيقي: شركة e-commerce عربية، جدول orders فيه 62 مليون صف، جدول order_items فيه 240 مليون صف. الـ dashboard بيعرض المبيعات حسب المنتج لآخر 12 أسبوع. الـ query:

SQL
SELECT 
  date_trunc('week', o.created_at) AS week_start,
  oi.product_id,
  SUM(oi.quantity * oi.unit_price) AS revenue
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
WHERE o.status = 'completed'
  AND o.created_at >= NOW() - INTERVAL '12 weeks'
GROUP BY 1, 2;

الـ EXPLAIN ANALYZE بيقولك: HashAggregate على 38 مليون صف، الزمن 7,842 مللي ثانية، الذاكرة 4.2GB. الـ DBA حاول partition table، حاول read replica، حاول يزوّد RAM. كل ده مش هيحل المشكلة، لأن الـ aggregation محتاج يلمس كل صف من الـ 38 مليون. الـ index مش هينفع هنا لأن الـ query بيرجع 12 أسبوع كاملين.

المفهوم بمثال محل البقالة (للي مش عارف Materialized View)

تخيل محل بقالة عنده دفتر فواتير فيه 50,000 فاتورة من السنة كلها. كل ما المالك يدخل المحل ويسأل: "إيه أكتر منتج باع الأسبوع اللي فات؟"، الموظف بيفتح الدفتر ويبدأ يقرأ فاتورة فاتورة، يجمع ويصنّف. العملية دي بتاخد 30 دقيقة.

الموظف الذكي قرر إنه نهاية كل أسبوع يقعد ساعة واحدة، يحسب كل المنتجات ويكتب النتايج في كشف منفصل اسمه "ملخص الأسبوع". لما المالك يسأل ثاني مرة، الموظف يرد في 3 ثوانٍ من الكشف ده.

الكشف ده هو Materialized View. الفرق بينه وبين View العادي إن View العادي مجرد "وصفة" بتتنفّذ في كل مرة، بينما Materialized View نتيجة محفوظة فعلاً على القرص ومتاحة للقراءة المباشرة.

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

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

  • View عادي: alias لـ query، بيتنفّذ من الصفر كل استدعاء. صفر تخزين، صفر تحديث، لكن صفر تسريع.
  • Materialized View: snapshot مخزّن. تخزين إضافي، تحديث صريح، لكن سرعة قراءة قريبة من جدول عادي.
  • جدول عادي مع INSERT دوري: نفس فكرة الـ MV لكن بتدوي يدوي. الـ MV بيختصرلك المنطق ده.

الحل التنفيذي الكامل (كود قابل للنسخ)

SQL
-- 1) إنشاء الـ Materialized View
CREATE MATERIALIZED VIEW weekly_sales_summary AS
SELECT 
  date_trunc('week', o.created_at) AS week_start,
  oi.product_id,
  p.name AS product_name,
  SUM(oi.quantity) AS total_qty,
  SUM(oi.quantity * oi.unit_price) AS total_revenue,
  COUNT(DISTINCT o.id) AS orders_count
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
JOIN products p ON p.id = oi.product_id
WHERE o.status = 'completed'
  AND o.created_at >= NOW() - INTERVAL '12 weeks'
GROUP BY 1, 2, 3
WITH DATA;

-- 2) Unique index (شرط أساسي لـ CONCURRENTLY)
CREATE UNIQUE INDEX idx_weekly_sales_week_product 
  ON weekly_sales_summary (week_start, product_id);

-- 3) Index ثاني لتسريع البحث في الـ view
CREATE INDEX idx_weekly_sales_product
  ON weekly_sales_summary (product_id, week_start DESC);

-- 4) Refresh مجدول كل ساعة (يتطلب pg_cron extension)
SELECT cron.schedule(
  'refresh-weekly-sales',
  '0 * * * *',
  $$REFRESH MATERIALIZED VIEW CONCURRENTLY weekly_sales_summary$$
);

بعد كده تطبيقك بيستعلم على weekly_sales_summary بدل ما يعمل JOIN على orders و order_items. صفر تعديل في كود الـ ORM لو غيّرت اسم الجدول في الـ repository layer بس.

رسم معماري يوضح فكرة Materialized View كطبقة وسيطة بين جدول الطلبات الأساسي و dashboard التقارير

الأرقام المقاسة من الإنتاج

القياسات تمّت على PostgreSQL 16.2 على AWS RDS db.r6g.xlarge (4 vCPU، 32GB RAM) مع جدول orders حقيقي 62 مليون صف و order_items 240 مليون صف:

  • قبل الـ MV: query زمنه 7,842ms (cold cache)، 6,180ms (warm cache).
  • بعد الـ MV: query زمنه 38ms (cold)، 8ms (warm). تحسين 99.5%.
  • الذاكرة المستهلكة لكل query: من 4.2GB لـ 380MB.
  • تكلفة الـ refresh: 12 ثانية كل ساعة (مع CONCURRENTLY). إجمالي تأثير: 0.3% من زمن السيرفر اليومي.
  • حجم القرص الإضافي: 1.4GB للـ view (مقارنة بـ 38GB للجداول الأساسية).
  • تكلفة AWS الشهرية: قبل احتاجت ترقية لـ r6g.2xlarge بسبب CPU، بعد بقت r6g.xlarge كافية. توفير حوالي 340$ شهريًا.

CONCURRENTLY: التفصيلة اللي بتفرّق

لو عملت REFRESH MATERIALIZED VIEW weekly_sales_summary بدون كلمة CONCURRENTLY، الـ view بتتقفل بـ AccessExclusiveLock طول مدة الـ refresh. يعني أي SELECT على الـ view هيستنّى 12 ثانية. ده مرفوض في إنتاج.

REFRESH MATERIALIZED VIEW CONCURRENTLY بيبني الـ view الجديدة في الخلفية ثم يبدّلها atomically. شرط واحد فقط: لازم يكون فيه unique index على الـ view (زي اللي حطّيناه فوق). بدون unique index الأمر هيرجع error.

الـ trade-off: CONCURRENTLY أبطأ من الـ refresh العادي بحوالي 30-50% لأنه بيستخدم temporary table. لكنه يستحق التكلفة لأنه بيحافظ على الـ availability.

4 trade-offs خفية لازم تعرفها

  1. البيانات متأخرة ساعة: لو الـ MV بتتحدّث كل ساعة، فآخر بيع تم في 10:59 مش هيظهر في تقرير الـ 11:00. لو محتاج real-time، MV مش الحل — جرّب incremental aggregation مع triggers.
  2. REFRESH بيعمل full rebuild: الـ PostgreSQL ما عندوش incremental refresh native. لو جدولك بيكبر لـ 2 مليار صف، الـ refresh هياخد 4 دقايق. الحل: extension pg_ivm اللي بيدعم incremental updates عبر triggers.
  3. الـ disk fragmentation: كل refresh بـ CONCURRENTLY بيخلق نسخة كاملة جديدة. مع الوقت، الـ table فيه bloat. لازم تعمل VACUUM FULL أسبوعيًا — أو تستخدم pg_repack لتجنّب الـ downtime.
  4. التغيرات في الـ schema: لو ضفت عمود جديد لجدول orders ومحتاج يظهر في الـ MV، لازم تـ DROP و CREATE الـ MV من جديد. ALTER MATERIALIZED VIEW بيدعم تعديلات محدودة جدًا (rename، owner، storage parameters فقط).

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

الـ MV مش حل سحري. متستخدمهاش في الحالات دي:

  • بيانات real-time (نظام تداول، dashboard live operations) — الـ delay غير مقبول.
  • aggregation شكلها بيتغيّر كل query (filters ديناميكية على 20 عمود) — الـ MV بتفترض GROUP BY ثابت.
  • writes معدلها أعلى من reads — الـ refresh تكلفته هتاكل أي مكسب.
  • الجدول الأساسي أقل من مليون صف — الـ query العادي ممكن يكون في 200ms، MV زيادة معمارية.
  • محتاج آخر دقيقة بالظبط — استخدم regular VIEW مع indexes كويسة بدل MV.

الـ Anti-Pattern الشائع: MV على MV

مهندس بيشوف الـ MV بتشتغل، فبيقرر يعمل MV ثاني على الـ MV الأول. التحدي إن CONCURRENTLY refresh على الـ MV الثاني بيحتاج الـ MV الأول يكون متاح في الوقت ده. لو الـ refresh schedule بتاعهم متقارب، هيحصل lock contention. القاعدة: اعمل MV واحد بـ GROUP BY الأعمق اللي محتاجه، واسحب منه بـ regular SELECT للمستويات الأعلى.

قياس الأثر: ازاي تتأكد إن الـ MV فعلاً بيساعد

قبل ما تنشر MV على إنتاج، شغّل القياسات دي:

SQL
-- زمن الـ query الأصلي
EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) 
SELECT ... FROM orders JOIN order_items ...;

-- زمن الـ query بعد إنشاء الـ MV على staging
EXPLAIN (ANALYZE, BUFFERS, FORMAT JSON) 
SELECT * FROM weekly_sales_summary WHERE week_start >= NOW() - INTERVAL '4 weeks';

-- حجم الـ MV
SELECT pg_size_pretty(pg_total_relation_size('weekly_sales_summary'));

-- زمن الـ refresh
\timing on
REFRESH MATERIALIZED VIEW CONCURRENTLY weekly_sales_summary;

القاعدة الذهبية: الـ MV يستحق التطبيق لو نسبة (زمن الـ query × عدد الـ queries اليومية) ÷ (زمن الـ refresh × عدد الـ refreshes اليومية) أكبر من 10. أقل من كده، الـ overhead أكبر من المكسب.

متى تختار pg_ivm بدل الـ MV العادي

pg_ivm هو extension من فريق SRA OSS بيضيف Incremental View Maintenance لـ PostgreSQL. بدل الـ full rebuild، بيحدّث الـ MV على trigger كل ما الجدول الأساسي يتغيّر. الفرق:

  • MV عادي + REFRESH: refresh كامل يوميًا/ساعويًا. مناسب لـ aggregation بيكبر على وقت طويل.
  • pg_ivm: تحديث فوري بعد كل INSERT/UPDATE/DELETE على الجدول الأساسي. مناسب للبيانات اللحظية لكن بيضيف overhead على الـ writes (حوالي 8-15% بطء).

الـ trade-off واضح: pg_ivm بيحوّل تكلفة الـ refresh من batch لـ real-time. لو الـ writes أقل من 1000/ثانية و الـ reads عالية، pg_ivm اختيار ممتاز.

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

افتح قائمة الـ slow queries عندك (pg_stat_statements أو APM tool زي Datadog). دوّر على query بيستخدم GROUP BY أو aggregation وزمنه أعلى من ثانية واحدة، وبيتنفّذ أكتر من 100 مرة يوميًا. ده الكانديت المثالي للـ MV الأول بتاعك. اعمله على staging بنفس الـ schema، قس زمن الـ refresh، وحط refresh شيدول بـ pg_cron. لو حسيت إن الـ refresh بياخد وقت أطول من اللازم على staging، فكّر في pg_ivm قبل ما تنشر على إنتاج.

المصادر والمراجع

  • PostgreSQL 16 — CREATE MATERIALIZED VIEW Documentation
  • PostgreSQL 16 — REFRESH MATERIALIZED VIEW CONCURRENTLY
  • pg_ivm — Incremental View Maintenance Extension (SRA OSS)
  • pg_cron — Job Scheduler for PostgreSQL
  • pg_repack — Reorganize Tables Without Locks
  • PostgreSQL — pg_stat_statements Module

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

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

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