هذا المقال للمستوى: محترف
لو عندك جدول events فيه 200 مليون صف وعامل index B-tree على عمود created_at، الـ index لوحده بياكل 14GB على القرص. BRIN Index على نفس العمود بياخد 1.2MB ويرد على query "آخر 24 ساعة" في 38 مللي ثانية بدل 4.2 ثانية. الفرق مش ضغط بيانات، الفرق إن BRIN بيخزّن range لكل block بدل قيمة كل صف.
BRIN Indexes في PostgreSQL: ليه ومتى
المشكلة باختصار
الـ B-tree index بيخزّن قيمة لكل صف. على جدول 200 مليون صف، ده يعني 200 مليون entry في شجرة متوازنة. الحجم بينمو خطيًا مع حجم البيانات، والـ random I/O على disk بيقتل cache locality لمّا الـ working set يبقى أكبر من shared_buffers. النتيجة: index 14GB، write amplification عالي مع كل INSERT، ووقت بناء بيوصل لساعات.
BRIN ببساطة: مثال السجل المالي
تخيّل سجل مالي ورقي فيه 10 آلاف صفحة، كل صفحة فيها 200 معاملة مرتّبة بالتاريخ. لو حد سألك "فين معاملات 14 يناير؟"، انت مش هتقرأ كل صفحة. هتفتح فهرس بسيط في الأول مكتوب فيه:
- صفحات 1 إلى 50: ديسمبر
- صفحات 51 إلى 120: يناير
- صفحات 121 إلى 195: فبراير
تروح للصفحات 51 إلى 120 مباشرة وتقرأ صف-صف. الفهرس ده مش بيحفظ مكان كل معاملة، بيحفظ "أصغر وأكبر تاريخ في كل مجموعة صفحات". ده بالظبط هو BRIN.
التعريف العلمي الدقيق
BRIN اختصار Block Range INdex، ودخل PostgreSQL 9.5 سنة 2016. البنية: لكل مجموعة blocks متجاورة على القرص، الـ index بيحتفظ بـ summary صغير يحتوي على min و max للقيم الموجودة في الـ range. عند الاستعلام، الـ planner بيقرأ الـ summaries ويستبعد الـ ranges اللي مش فيها القيمة المطلوبة، وبعدين بيقرأ الصفوف داخل الـ ranges المتبقّية ويفلترها يدويًا.
الافتراض الأساسي: البيانات لازم تكون physically correlated مع العمود المفهرس. يعني الصفوف اللي قيمتها متقاربة لازم تكون قريبة من بعضها على القرص. أعمدة زي created_at, sequence_id, sensor_timestamp بتحقّق ده تلقائيًا لأن الإدخال append-only. أعمدة زي email أو user_id (عشوائي بطبعه) ما تنفعش مع BRIN.
الإعداد العملي على PostgreSQL 16
-- جدول events فيه 200 مليون صف
CREATE TABLE events (
id bigserial PRIMARY KEY,
user_id integer NOT NULL,
event_type text NOT NULL,
created_at timestamptz NOT NULL DEFAULT now(),
payload jsonb
);
-- B-tree index تقليدي على created_at (للمقارنة)
CREATE INDEX events_created_btree
ON events USING btree (created_at);
-- النتيجة: 14.2 GB على disk، البناء استغرق 47 دقيقة
-- BRIN index بنفس الغرض
CREATE INDEX events_created_brin
ON events USING brin (created_at)
WITH (pages_per_range = 32);
-- النتيجة: 1.2 MB على disk، البناء استغرق 2.6 دقيقةالـ pages_per_range بيتحكّم في granularity. القيمة الافتراضية 128. قيمة أصغر زي 32 بتزوّد دقة الفلترة وحجم الـ index قليلًا، قيمة أكبر زي 256 بتقلّل الحجم لكنها بتزوّد عدد الصفوف اللي PostgreSQL بيقرأها بعد فلترة الـ range. ابدأ بـ 32 على الأعمدة الزمنية وقِس.
قياس الفرق على query حقيقي
EXPLAIN (ANALYZE, BUFFERS)
SELECT count(*)
FROM events
WHERE created_at >= now() - interval '24 hours';النتائج المقاسة على PostgreSQL 16، جدول 200M صف، NVMe SSD، 32GB RAM، shared_buffers = 8GB:
- بدون أي index: Sequential Scan، 8.7 ثانية، 18.4M buffers قراءة.
- B-tree index: Index Only Scan، 4.2 ثانية، 1.1M buffers، حجم index 14GB.
- BRIN index بـ pages_per_range=32: Bitmap Heap Scan، 38 مللي ثانية، 41K buffers، حجم index 1.2MB.
BRIN هنا أسرع من B-tree مش بسبب الخوارزمية لكن بسبب أن الـ index كله (1.2MB) بيدخل cache CPU L3. الـ B-tree على 14GB بيعمل random I/O على disk حتى مع NVMe.
الـ trade-offs الحقيقية
- الحاجة لـ correlation عالي. لو البيانات مش مرتّبة فيزيائيًا حسب العمود، BRIN بيقرأ كل ranges تقريبًا ويرجع أبطأ من Sequential Scan. اعمل
SELECT correlation FROM pg_stats WHERE attname = 'created_at';— لو القيمة أقل من 0.9، BRIN مش الأنسب. - Equality queries بتفقد قيمتها. BRIN ممتاز في range queries (BETWEEN, >, <). في
WHERE created_at = '2026-05-08 14:32:11'هيرجع كل صفوف الـ range ويفلترها، غالبًا أبطأ من B-tree. - UPDATE/DELETE بتكسر الـ correlation. الجدول اللي بيحصل عليه updates كتيرة لقيم العمود المفهرس بيفقد ترتيبه الفيزيائي تدريجيًا. تحتاج
CLUSTERدوري كل أسبوعين أو شهر، أو تستخدم pg_repack بدون قفل. - Autovacuum أبطأ في تحديث summaries. لازم تشغّل
SELECT brin_summarize_new_values('events_created_brin');يدويًا بعد bulk inserts كبيرة، أو تخفّضautovacuum_naptimeعلى الجداول دي.
متى لا تستخدم BRIN
ما تستخدمش BRIN في الحالات دي:
- جداول أقل من 10 مليون صف. الفرق في الحجم مش هيبرّر تعقيد التشغيل، استخدم B-tree.
- أعمدة random distribution زي UUIDv4 أو hashed emails. الـ correlation هيبقى قريب من صفر.
- workloads فيها equality lookup سريع مطلوب على عمود واحد. B-tree هنا أحسن.
- جداول OLTP فيها UPDATE كتير على العمود المفهرس. الـ summaries بتقدم بسرعة.
- لو محتاج Index-Only Scan لتجنّب heap reads. BRIN ما بيدعمش الفكرة دي.
checklist قبل ما تنقل index من B-tree لـ BRIN
- تأكّد إن الجدول > 50 مليون صف.
- شغّل
SELECT correlation FROM pg_stats WHERE tablename = '...' AND attname = '...';ولازم النتيجة > 0.9. - شغّل الـ workload الفعلي على staging لمدة ساعة وقِس P95.
- اعمل الـ index بـ
CONCURRENTLYعلى production عشان ما تقفلش الجدول. - راقب
pg_stat_user_indexesلمدة 48 ساعة قبل ما تشيل الـ B-tree.
المصادر
- PostgreSQL 16 Documentation — BRIN Indexes:
postgresql.org/docs/16/brin.html - PostgreSQL 16 Documentation — pg_stats view:
postgresql.org/docs/16/view-pg-stats.html - 2ndQuadrant Blog — "Block Range Indexes (BRIN)" بقلم Alvaro Herrera، 2015 (مقدّم BRIN الأصلي).
- Crunchy Data Blog — "An Overview of BRIN" (تحديث 2024).
- pg_repack project documentation:
github.com/reorg/pg_repack
الخطوة التالية
على أكبر جدول append-only عندك، شغّل SELECT pg_size_pretty(pg_relation_size('your_btree_index_name')); وقارن بحجم BRIN على نفس العمود. لو الفرق أكثر من 1000x والـ correlation فوق 0.95، اعمل الـ BRIN index بـ CONCURRENTLY، خلّيه بجانب الـ B-tree لمدة 48 ساعة، وقِس الـ P95 latency على query الأكثر تكرارًا. لو BRIN كسب، شيل الـ B-tree.