PostgreSQL: أساسيات فهارس PostgreSQL وتحسينها
آخر تحديث: 2026-08-26
1. ما ستتعلمه
- أنواع الفهارس الستة في PostgreSQL: B-Tree / Hash / GIN / GiST / BRIN / SP-GiST
- CREATE INDEX / UNIQUE INDEX / CONCURRENTLY
- الفهارس الجزئية (شرط WHERE) — ميزة خاصة بـ PostgreSQL
- فهارس التعبيرات
- الفهارس المركبة وقاعدة البادئة اليسرى
- قراءة خطط التنفيذ باستخدام EXPLAIN / EXPLAIN ANALYZE
- صيانة الفهارس باستخدام REINDEX
- السيناريوهات الشائعة التي تفشل فيها الفهارس في الاستخدام
2. القصة
Bob هو مسؤول قاعدة بيانات لمنصة تجارة إلكترونية. يبلغ المستخدمون أن صفحة بحث المنتجات تزداد بطئًا — حيث يستغرق المسح الكامل لعمود JSONB 3 ثوانٍ للاستعلام.
بعد أن أضاف Bob فهرس GIN على عمود JSONB، انخفض وقت البحث إلى 300 مللي ثانية. ثم لاحظ أن استعلامات المستخدمين النشطين بطيئة أيضًا، ومع ذلك فإن 90% من المستخدمين قد تم تعطيلهم بالفعل، لذا فإن بناء فهرس على الجدول بأكمله يهدر المساحة. استخدم فهرسًا جزئيًا يفهرس فقط الصفوف حيث status = 'active'، مما قلص حجم الفهرس بنسبة 80% وجعل الاستعلامات أسرع.
3. المفهوم: نظرة عامة على أنواع الفهارس
(1) أنواع الفهارس الستة
| نوع الفهرس | الاسم الكامل | أنواع البيانات المناسبة | حالة الاستخدام النموذجية |
|---|---|---|---|
| B-Tree | شجرة متوازنة | جميع الأنواع القابلة للترتيب | المساواة، النطاق، الترتيب، LIKE البادئة |
| Hash | جدول التجزئة | جميع الأنواع | استعلامات المساواة البسيطة |
| GIN | الفهرس المعكوس المعمم | المصفوفات، JSONB، البحث النصي الكامل | استعلامات الاحتواء، البحث النصي الكامل |
| GiST | شجرة البحث المعممة | الهندسة، النطاقات، النص الكامل | الاستعلامات المكانية، أقرب جار |
| BRIN | فهرس نطاق الكتل | الجداول الكبيرة ذات الأعمدة المرتبة | بيانات السلاسل الزمنية، مسح النطاقات الزمنية |
| SP-GiST | GiST المقسمة مكانيًا | هياكل التقسيم غير المنتظمة | أرقام الهواتف، التوجيه |
(1) ▶ مثال
SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = 'orders';
indexname | indexdef
------------------------+--------------------------------------------------
orders_pkey | CREATE UNIQUE INDEX ... ON orders USING btree (order_id)
idx_orders_customer_id | CREATE INDEX ... ON orders USING btree (customer_id)
(2) قرار اختيار نوع الفهرس
flowchart TD
A{نوع الاستعلام؟} -->|مساواة/نطاق/ترتيب| B[B-Tree]
A -->|مساواة بسيطة| C[Hash]
A -->|مصفوفة/JSONB يحتوي| D[GIN]
A -->|مكاني/هندسي/أقرب| E[GiST]
A -->|مسح تسلسلي لجدول كبير| F[BRIN]
A -->|هيكل تقسيم غير منتظم| G[SP-GiST]
B --> B1["نوع الفهرس الافتراضي<br/>90% من الحالات"]
C --> C1["نادر الاستخدام<br/>B-Tree أفضل عادة"]
D --> D1["JSONB @> ?<br/>مصفوفة @> <br/>tsvector @@ "]
E --> E1["PostGIS<br/>تداخل النطاق"]
F --> F1["10M+ صف سلاسل زمنية<br/>حجم صغير جدًا"]
G --> G1["بادئة هاتف<br/>توجيه IP"]
style A fill:#e1f5fe
style B fill:#c8e6c9
style D fill:#fff9c4
style F fill:#fff9c4
4. المفهوم: فهرس B-Tree
(1) ميزات وحالات استخدام B-Tree
B-Tree هو نوع الفهرس الافتراضي في PostgreSQL؛ يدعم استعلامات المساواة والنطاق والترتيب و IS NULL و LIKE البادئة.
| المعامل المدعوم | مثال |
|---|---|
| المساواة | WHERE col = 100 |
| النطاق | WHERE col > 100 AND col < 200 |
| الترتيب | ORDER BY col |
| IS NULL | WHERE col IS NULL |
| LIKE البادئة | WHERE col LIKE 'abc%' |
| BETWEEN | WHERE col BETWEEN 1 AND 10 |
| غير مدعوم | السبب |
|---|---|
LIKE '%abc' |
لا يمكن لحرف البدل البادئ استخدام ترتيب B-Tree |
col::text = '100' |
عدم تطابق النوع؛ يحتاج فهرس تعبير |
LOWER(col) = 'abc' |
نتيجة الدالة غير مفهرسة؛ تحتاج فهرس تعبير |
(2) ▶ مثال
CREATE INDEX idx_orders_amount ON orders (amount);
CREATE INDEX idx_orders_date_amount ON orders (order_date, amount DESC);
Output:
CREATE TABLE
(3) ▶ مثال
CREATE UNIQUE INDEX idx_users_email ON users (email);
Output:
CREATE TABLE
يضمن فهرس UNIQUE تفرد البيانات وأداء الاستعلام معًا.
5. المفهوم: فهرس GIN
(1) مبدأ فهرس GIN
GIN (الفهرس المعكوس المعمم) هو فهرس معكوس: تعيين من عنصر إلى الصفوف التي تحتويه. يناسب استعلامات نمط "يحتوي".
| النوع | المعامل | مثال |
|---|---|---|
| JSONB | @> ? `? |
?&` |
| مصفوفة | @> <@ && |
WHERE tags @> ARRAY['sale'] |
| tsvector | @@ |
WHERE body @@ to_tsquery('postgres') |
(4) ▶ مثال
CREATE INDEX idx_products_attrs ON products USING GIN (attrs);
SELECT product_id, name
FROM products
WHERE attrs @> '{"category": "electronics"}';
product_id | name
------------+------------
101 | Laptop Pro
205 | Smart Watch
(5) ▶ مثال
CREATE INDEX idx_products_tags ON products USING GIN (tags);
SELECT product_id, name
FROM products
WHERE tags @> ARRAY['summer', 'sale'];
Output:
result
----------
42.50
(1 row)
(6) ▶ مثال
CREATE INDEX idx_articles_body ON articles USING GIN (to_tsvector('english', body));
SELECT id, title
FROM articles
WHERE to_tsvector('english', body) @@ to_tsquery('postgresql & index');
Output:
CREATE TABLE
(2) مقارنة GIN و B-Tree
| البعد | B-Tree | GIN |
|---|---|---|
| نوع الاستعلام | مساواة / نطاق | احتواء / بحث |
| سرعة الكتابة | سريعة | بطيئة (يجب تحديث القوائم المعكوسة) |
| حجم الفهرس | متوسط | أكبر |
| الأنواع المناسبة | عددية | مصفوفة / JSONB / نص كامل |
| دعم الترتيب | نعم | لا |
6. المفهوم: GiST / BRIN / SP-GiST / Hash
(1) فهرس GiST
GiST هو إطار شجرة بحث معمم يدعم استراتيجيات تقسيم مخصصة. استخدامه النموذجي هو البيانات المكانية.
| حالة الاستخدام | المعامل | الامتداد |
|---|---|---|
| البيانات الهندسية | && @ <@ |
PostGIS |
| أنواع النطاق | && @> <@ |
مدمج |
| البحث النصي الكامل | @@ |
مدمج |
(7) ▶ مثال
CREATE INDEX idx_events_time_range ON events USING GiST (time_range);
SELECT event_id, title
FROM events
WHERE time_range && daterange('2025-01-01', '2025-03-01');
Output:
CREATE TABLE
(2) فهرس BRIN
BRIN (فهرس نطاق الكتل) يخزن معلومات ملخصة لكل كتلة بيانات (قيم الحد الأدنى/الأقصى). إنه مضغوط للغاية ويناسب الجداول الكبيرة المرتبة فعليًا.
| البعد | B-Tree | BRIN |
|---|---|---|
| حجم الفهرس | كبير | صغير جدًا (حوالي 1/1000) |
| الدقة | دقيقة | تقريبية (قد تفحص بضع كتل إضافية) |
| تكلفة الصيانة | مرتفعة | منخفضة جدًا |
| حالة الاستخدام | استعلامات عشوائية | مسح نطاقات السلاسل الزمنية |
(8) ▶ مثال
CREATE INDEX idx_logs_created_at ON logs USING BRIN (created_at)
WITH (pages_per_range = 32);
SELECT count(*) FROM logs
WHERE created_at BETWEEN '2025-06-01' AND '2025-06-30';
Output:
count
-------
5
(1 row)
(3) فهرس SP-GiST
يناسب SP-GiST هياكل التقسيم غير المتوازنة، مثل بادئات أرقام الهواتف وتوجيه IP.
(9) ▶ مثال
-- قم بتمكين امتداد btree_gist أولاً: CREATE EXTENSION IF NOT EXISTS btree_gist;
CREATE INDEX idx_customers_phone ON customers USING SP-GiST (phone prefix_range);
Output:
CREATE TABLE
(4) فهرس Hash
فهرس Hash يدعم فقط استعلامات المساواة البسيطة؛ لا يدعم النطاق أو الترتيب. قبل PostgreSQL 10، كانت فهارس Hash تعاني من مشكلة WAL (تم إصلاحها الآن)، لكن B-Tree هو الخيار الأفضل عمومًا.
| البعد | B-Tree | Hash |
|---|---|---|
| استعلام المساواة | سريع | سريع |
| استعلام النطاق | مدعوم | غير مدعوم |
| الترتيب | مدعوم | غير مدعوم |
| WAL | كامل | كامل منذ PostgreSQL 10 |
| التوصية | الخيار الافتراضي | نادر الاستخدام |
7. المفهوم: ميزات الفهارس المتقدمة
(1) الفهرس الجزئي
يحتوي الفهرس الجزئي فقط على الصفوف التي تحقق شرط WHERE، مما يقلل حجم الفهرس وتكلفة الصيانة. هذه ميزة خاصة بـ PostgreSQL.
| البعد | فهرس كامل | فهرس جزئي |
|---|---|---|
| الصفوف المضمنة | جميع الصفوف | الصفوف التي تحقق الشرط |
| حجم الفهرس | كبير | صغير |
| تكلفة الصيانة | يُحدث عند كل كتابة | فقط الصفوف ذات الصلة تُحدث |
| حالة الاستخدام | استعلامات عام�� | استعلامات تهتم فقط بمجموعة فرعية |
(10) ▶ مثال
CREATE INDEX idx_users_active_email ON users (email)
WHERE status = 'active';
Output:
CREATE TABLE
SELECT email FROM users WHERE status = 'active' AND email = 'alice@example.com';
هذا الاستعلام يضرب الفهرس الجزئي. الاستعلام بدون شرط status = 'active' لن يضربه.
(11) ▶ مثال
CREATE INDEX idx_orders_pending ON orders (created_at)
WHERE shipped = false;
Output:
CREATE TABLE
(2) فهرس التعبير
عندما يلف شرط الاستعلام عمودًا بدالة أو عملية حسابية، لا يمكن استخدام الفهرس العادي. يبني فهرس التعبير الفهرس على النتيجة المحسوبة.
(12) ▶ مثال
CREATE INDEX idx_users_email_lower ON users (LOWER(email));
SELECT * FROM users WHERE LOWER(email) = 'alice@example.com';
Output:
CREATE TABLE
(13) ▶ مثال
CREATE INDEX idx_orders_date_trunc ON orders (DATE_TRUNC('day', created_at));
SELECT COUNT(*) FROM orders
WHERE DATE_TRUNC('day', created_at) = '2025-06-15'::date;
Output:
count
-------
5
(1 row)
(3) الفهرس المركب والبادئة اليسرى
أنماط الاستعلام التي يمكن للفهرس المركب (a, b, c) خدمتها:
| شرط الاستعلام | يضرب؟ | السبب |
|---|---|---|
WHERE a = 1 |
نعم | البادئة اليسرى |
WHERE a = 1 AND b = 2 |
نعم | البادئة اليسرى |
WHERE a = 1 AND b = 2 AND c = 3 |
نعم | تطابق كامل |
WHERE b = 2 |
لا | العمود الأيسر مفقود |
WHERE b = 2 AND c = 3 |
لا | العمود الأيسر مفقود |
WHERE a = 1 AND c = 3 |
جزئي | فقط العمود a مستخدم؛ c غير متصل |
(14) ▶ مثال
CREATE INDEX idx_orders_customer_date
ON orders (customer_id, order_date DESC);
Output:
CREATE TABLE
(4) بناء الفهرس عبر الإنترنت باستخدام CONCURRENTLY
بناء الفهرس يأخذ قفلًا حصريًا افتراضيًا، مما يمنع الكتابة. CONCURRENTLY لا يمنع الكتابة، لكنه يبني بشكل أبطأ.
| الطريقة | القفل | يمنع الكتابة | السرعة | داخل معاملة؟ |
|---|---|---|---|---|
| CREATE INDEX | قفل حصري | يمنع | سريع | نعم |
| CREATE INDEX CONCURRENTLY | قفل مشترك | لا يمنع | بطيء | لا |
(15) ▶ مثال
CREATE INDEX CONCURRENTLY idx_orders_region
ON orders (region);
Output:
CREATE TABLE
8. المفهوم: خطة تنفيذ EXPLAIN
(1) أساسيات EXPLAIN
| الأمر | الوصف | ينفذ الاستعلام؟ |
|---|---|---|
| EXPLAIN | عرض خطة التنفيذ | لا |
| EXPLAIN ANALYZE | تنفيذ وعرض التوقيت الفعلي | نعم |
| EXPLAIN BUFFERS | عرض إصابات المخزن المؤقت | نعم |
| EXPLAIN (FORMAT JSON) | إخراج بتنسيق JSON | لا |
(16) ▶ مثال
EXPLAIN
SELECT * FROM orders WHERE customer_id = 1;
QUERY PLAN
----------------------------------------------------------------------
Index Scan using idx_orders_customer_id on orders (cost=0.29..8.31 rows=1 width=72)
Index Cond: (customer_id = 1)
(17) ▶ مثال
EXPLAIN ANALYZE
SELECT * FROM orders WHERE customer_id = 1;
QUERY PLAN
----------------------------------------------------------------------
Index Scan using idx_orders_customer_id on orders
(cost=0.29..8.31 rows=1 width=72) (actual time=0.015..0.016 rows=2 loops=1)
Index Cond: (customer_id = 1)
Planning Time: 0.085 ms
Execution Time: 0.032 ms
(2) أنواع المسح الرئيسية
| نوع المسح | المعنى | يستخدم الفهرس؟ |
|---|---|---|
| Seq Scan | مسح تسلسلي كامل للجدول | لا |
| Index Scan | مسح الفهرس (جلب صفوف الجدول) | نعم |
| Index Only Scan | مسح الفهرس فقط (بدون بحث في الجدول) | نعم (فهرس مغطٍ) |
| Bitmap Scan | مسح الصورة النقطية (دفعات كبيرة) | جزئي |
| Parallel Seq Scan | مسح تسلسلي متوازي للجدول | لا |
(18) ▶ مثال
EXPLAIN
SELECT customer_id FROM orders WHERE customer_id = 1;
Index Only Scan using idx_orders_customer_id on orders
Index Cond: (customer_id = 1)
يتم قراءة الفهرس فقط — بدون بحث في الجدول عن صفوف البيانات — مما يعطي أفضل أداء.
9. صيانة الفهارس وفشلها
(1) إعادة بناء REINDEX
بعد العديد من عمليات الإدر��ج/الحذف مع مرور الوقت، قد ينتفخ الفهرس؛ REINDEX يعيد بنائه لاستعادة المساحة.
| الطريقة | الوصف | القفل |
|---|---|---|
| REINDEX INDEX idx | إعادة بناء فهرس واحد | قفل حصري |
| REINDEX TABLE tbl | إعادة بناء جميع فهارس الجدول | قفل حصري |
| REINDEX INDEX CONCURRENTLY idx | إعادة بناء عبر الإنترنت (PG 12+) | لا يمنع |
(19) ▶ مثال
REINDEX INDEX idx_orders_customer_id;
REINDEX INDEX CONCURRENTLY idx_orders_customer_id;
Output:
-- SQL statement executed successfully
(2) سيناريوهات فشل الفهرس الشائعة
| السيناريو | مثال | الإصلاح |
|---|---|---|
| دالة تلف عمودًا | WHERE LOWER(col) = 'x' |
فهرس تعبير |
| تحويل نوع ضمني | WHERE varchar_col = 123 |
استخدام أنواع متسقة |
| حرف بدل بادئ | WHERE col LIKE '%abc' |
GIN / pg_trgm |
| شرط OR | WHERE a=1 OR b=2 |
فهارس منفصلة أو UNION |
| إحصائيات قدي��ة | بعد تغيير كبير في البيانات | ANALYZE |
| مخالفة البادئة اليسرى | فهرس مركب (a,b) يُستعلم بـ b | ضبط الفهرس أو الاستعلام |
| عدم تطابق شرط الفهرس الجزئي | WHERE status='active' يستعلم الكل |
إسقاط WHERE أو بناء فهرس كامل |
(20) ▶ مثال
SELECT * FROM users WHERE phone = 13800138000;
SELECT * FROM users WHERE phone = '13800138000';
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
phone هو VARCHAR. التحويل الضمني في السطر الأول يتسبب في عدم استخدام الفهرس؛ السطر الثاني يضرب الفهرس.
10. مثال شامل
خطة تحسين الفهرس لـ Bob — بحث منتجات JSONB + فهرس جزئي للمستخدمين النشطين + فهرس مركب + فهرس BRIN للسلاسل الزمنية:
CREATE INDEX CONCURRENTLY idx_products_attrs_gin
ON products USING GIN (attrs);
CREATE INDEX CONCURRENTLY idx_users_active_email
ON users (email, last_login_at)
WHERE status = 'active';
CREATE INDEX CONCURRENTLY idx_orders_customer_date
ON orders (customer_id, order_date DESC);
CREATE INDEX CONCURRENTLY idx_audit_log_created_brin
ON audit_log USING BRIN (created_at)
WITH (pages_per_range = 32);
EXPLAIN ANALYZE
SELECT p.product_id, p.name, p.attrs
FROM products p
WHERE p.attrs @> '{"category": "electronics", "in_stock": true}';
EXPLAIN ANALYZE
SELECT user_id, email
FROM users
WHERE status = 'active'
AND email LIKE 'alice%'
ORDER BY last_login_at DESC
LIMIT 10;
11. تدفق التنفيذ
تدفق اختيار نوع الفهرس وتحسينه:
flowchart TD
A[وجدت استعلامًا بطيئًا] --> B[EXPLAIN ANALYZE]
B --> C{نوع المسح؟}
C -->|Seq Scan| D{هل يوجد فهرس مناسب؟}
C -->|Index Scan| E[الفهرس يضرب<br/>حسّن الاستعلام/الفهرس]
D -->|لا| F{نوع البيانات؟}
D -->|نعم لكن لا يضرب| G[تحقق من إبطال الفهرس]
F -->|عددي/ترتيب| H[أنشئ B-Tree]
F -->|JSONB/مصفوفة| I[أنشئ GIN]
F -->|هندسي/نطاق| J[أنشئ GiST]
F -->|جدول كبير تسلسلي| K[أنشئ BRIN]
F -->|مجموعة فرعية فقط| L[أنشئ فهرس جزئي]
F -->|دالة/محسوب| M[أنشئ فهرس تعبير]
H --> N[نشر CONCURRENTLY]
I --> N
J --> N
K --> N
L --> N
M --> N
style B fill:#e1f5fe
style G fill:#ffccbc
style N fill:#c8e6c9
❓ أسئلة شائعة
SELECT indexname FROM pg_indexes WHERE indexdef LIKE '%INVALID%' أو \d+ tbl، ثم أسقطه باستخدام DROP INDEX.LIKE 'abc%' يمكنه استخدام B-Tree؛ LIKE '%abc' بحرف بدل بادئ لا يمكنه — يحتاج GIN بالإضافة إلى امتداد pg_trgm.WHERE a=1 ORDER BY b؟a ثم b، لذا فهو يلبي كلاً من مرشح WHERE و ORDER BY، متجنبًا فرزًا إضافيًا.📖 ملخص
- يدعم PostgreSQL 6 أنواع من الفهارس؛ B-Tree يناسب حوالي 90% من السيناريوهات
- فهارس GIN تناسب استعلامات الاحتواء على JSONB / المصفوفات / البحث النصي الكامل
- فهارس BRIN مضغوطة للغاية وتناسب مسح النطاقات على الجداول الكبيرة المرتبة فعليًا
- الفهارس الجزئية تفهرس فقط الصفوف التي تحقق شرطًا، مما يقلل الحجم وتكلفة الصيانة
- فهارس التعبير تفهرس نتيجة دالة/حساب، لإصلاح فشل الفهارس الناتج عن الأعمدة الملفوفة بدوال
- الفهارس المركبة تتبع قاعدة البادئة اليسرى؛ ترتيب الأعمدة يؤثر على ما إذا كان الاستعلام يضرب الفهرس
- CONCURRENTLY يبني الفهارس عبر الإنترنت دون منع الكتابة، لكن لا يمكن تشغيله داخل معاملة
- EXPLAIN ANALYZE هو الأداة الأساسية لتشخيص الاستعلامات البطيئة
- الأسباب الشائعة لفشل الفهرس: لف الدوال، عدم تطابق النوع، الإحصائيات القديمة
📝 تمارين
- ⭐ أنشئ فهرس B-Tree على عمود
customer_idفي جدولorders، وتحقق باستخدام EXPLAIN من أن الاستعلام يضرب الفهرس. - ⭐ أنشئ فهرس GIN على عمود JSONB في جدول
products، ولاحظ كيف تتغير خطة التنفيذ لاستعلام@>. - ⭐⭐ أنشئ فهرسًا جزئيًا: فهرس فقط عمود
created_atللطلبات حيثstatus = 'pending'، وقارن فرق الحجم مع فهرس كامل. - ⭐⭐ أنشئ فهرس تعبير على
LOWER(email)، وتحقق من أن استعلامًا غير حساس لحالة الأحرف يضرب الفهرس�� ثم أنشئ فهرسًا مركبًا(region, created_at DESC)لدعم الاستعلامات المرتبة حسب المنطقة والوقت. - ⭐⭐⭐ لجدول سجلات بعشرات الملايين من الصفوف، صمم خطة مدمجة من فهرس BRIN + فهرس B-Tree مركب + فهرس جزئي، واستخدم EXPLAIN ANALYZE لمقارنة وقت الاستعلام قبل وبعد التحسين.