المستوى المطلوب: متوسط — تحتاج تكون شغّال على PostgreSQL، عارف الفرق بين الـ View والـ Table، ومرّيت قبل كده على query بطيء بسبب aggregation.
Materialized Views في PostgreSQL: حوّل تقرير من 12 ثانية لـ 80 مللي ثانية
لو عندك dashboard فيه تقرير "إجمالي مبيعات آخر 30 يوم لكل فرع" على جدول orders فيه 18 مليون صف، وبياخد 12 ثانية كل ما حد يفتح الصفحة، الحل مش زيادة CPU ولا تحويل التقرير على Elasticsearch. الحل سطر CREATE MATERIALIZED VIEW واحد، بيخلّي نفس الاستعلام يرجع في 80ms.
المشكلة باختصار
الـ 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 عادي.
الحل التنفيذي خطوة بخطوة
لنفترض الجدول الأساسي:
-- الجدول الأصلي: 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);