PostgreSQL: الاستعلامات الفرعية و CTEs في PostgreSQL
آخر تحديث: 2026-08-26
1. ما ستتعلمه
- الاستعلامات الفرعية العددية / العمودية / الصفية / الجدولية
- EXISTS / NOT EXISTS
- معاملات ANY / ALL
- أماكن ظهور الاستعلامات الفرعية: FROM / WHERE / SELECT
- CTE (عبارة WITH، ميزة PostgreSQL)
- CTE تكراري (WITH RECURSIVE لاستعلام الشجرة)
- CTE مقابل الاستعلام الفرعي مقابل الجدول المؤقت
2. القصة
Alice هي مطورة نظام موارد بشرية في شركة SaaS. الشركة لديها أكثر من 500 شخص، منظمين في شجرة: الرئيس التنفيذي في القمة، تحته نواب الرئيس، تحتهم المدراء، تحتهم المديرون، تحتهم الموظفون.
مدير المنتج يسأل: بدءاً من أي عقدة، اذكر تلك العقدة وجميع أحفادها (عبر مستويات متعددة)، معروضة بإزاحة لتوضيح التسلسل الهرمي.
الاستعلام العادي يمكنه فقط▶�لنزول مستوى واحداً. تتعلم Alice WITH RECURSIVE CTEs التكرارية وتحل الشجرة الفرعية بالكامل في عبارة SQL واحدة.
3. المفهوم
(1) تصنيف الاستعلامات الفرعية
| النوع | يعيد | يمكن أن يظهر في | مثال |
|---|---|---|---|
| استعلام فرعي عددي | صف واحد، قيمة واحدة | SELECT, WHERE, HAVING | (SELECT MAX(salary) ...) |
| استعلام فرعي عمودي | عمود واحد، صفوف متعددة | WHERE + IN/ANY/ALL | WHERE id IN (SELECT ...) |
| استعلام فرعي صفي | صف واحد، أعمدة متعددة | WHERE | WHERE (a,b) = (SELECT x,y ...) |
| استعلام فرعي جدولي | صفوف متعددة، أعمدة متعددة | FROM | FROM (SELECT ...) AS t |
(1) ▶ مثال
SELECT
name,
salary,
(SELECT AVG(salary) FROM employees) AS company_avg,
salary - (SELECT AVG(salary) FROM employees) AS diff
FROM employees
WHERE department_id = 5;
name | salary | company_avg | diff
---------+--------+-------------+-------
Alice | 95000 | 72000.00 | 23000
Bob | 88000 | 72000.00 | 16000
(2) ▶ مثال
SELECT name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);
Output:
result
----------
42.50
(1 row)
(3) ▶ مثال
SELECT order_id, amount
FROM orders
WHERE customer_id IN (
SELECT customer_id
FROM customers
WHERE region = 'NA'
);
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
(4) ▶ مثال
SELECT name, department_id, salary
FROM employees
WHERE (department_id, salary) = (
SELECT department_id, MAX(salary)
FROM employees
GROUP BY department_id
HAVING department_id = employees.department_id
);
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
(2) EXISTS / NOT EXISTS
EXISTS تتحقق مما إذا كان الاستعلام الفرعي يعيد أي صفوف؛ لا تهتم بالقيم الفعلية، فقط بـ "الوجود".
(5) ▶ مثال
SELECT u.user_id, u.name
FROM users u
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.user_id = u.user_id
);
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
| الطريقة | الصيغة | شرط التوقف | صديقة لـ NULL |
|---|---|---|---|
| IN | WHERE id IN (SELECT ...) |
تمسح الكل | انتبه لـ NULLs |
| EXISTS | WHERE EXISTS (SELECT 1 ...) |
تتوقف عند أول صف | NULL ليس لها تأثير |
| JOIN | JOIN ... |
جميع التطابقات | يعتمد على نوع JOIN |
(6) ▶ مثال
SELECT u.user_id, u.name
FROM users u
WHERE NOT EXISTS (
SELECT 1 FROM orders o
WHERE o.user_id = u.user_id
);
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
NOT EXISTS أكثر أماناً من NOT IN: عندما تحتوي نتيجة الاستعلام الفرعي NULL، NOT IN تعيد نتيجة فارغة للاستعلام بالكامل.
(7) ▶ مثال
SELECT name FROM customers
WHERE region NOT IN ('NA', 'EU', NULL);
(0 rows)
لأن x NOT IN (a, b, NULL) مكافئة لـ x <> a AND x <> b AND x <> NULL، و x <> NULL هي UNKNOWN، التعبير بالكامل FALSE.
(3) ANY / ALL
(8) ▶ مثال
SELECT name, salary
FROM employees
WHERE salary > ANY (
SELECT salary FROM employees WHERE department_id = 3
);
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
مكافئة لـ > MIN(نتيجة الاستعلام الفرعي).
(9) ▶ مثال
SELECT name, salary
FROM employees
WHERE salary > ALL (
SELECT salary FROM employees WHERE department_id = 3
);
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
مكافئة لـ > MAX(نتيجة الاستعلام الفرعي).
| المعامل | المعنى | المكاف�� |
|---|---|---|
> ANY (...) |
أكبر من أي واحد | > MIN(...) |
> ALL (...) |
أكبر من الكل | > MAX(...) |
= ANY (...) |
يساوي أي واحد | IN (...) |
(10) ▶ مثال
SELECT
department_id,
avg_salary,
count
FROM (
SELECT
department_id,
AVG(salary) AS avg_salary,
COUNT(*) AS count
FROM employees
GROUP BY department_id
) AS dept_stats
WHERE avg_salary > 70000;
Output:
count
-------
5
(1 row)
4. نقاط رئيسية
(1) CTE (عبارة WITH)
CTE (تعبير جدول مشترك) تستخدم WITH لتعريف مجموعة نتائج مؤقتة مسماة يمكن الإشارة إليها عدة مرات.
(11) ▶ مثال
WITH regional_sales AS (
SELECT
region,
SUM(amount) AS total_sales
FROM orders
GROUP BY region
),
top_regions AS (
SELECT region
FROM regional_sales
WHERE total_sales > (SELECT AVG(total_sales) FROM regional_sales)
)
SELECT
o.order_id,
o.amount,
o.region
FROM orders o
WHERE o.region IN (SELECT region FROM top_regions)
ORDER BY o.amount DESC;
Output:
result
----------
42.50
(1 row)
| الطريقة | قابلية القراءة | قابلة لإعادة الاستخدام | المحسن يضمن | مجسدة |
|---|---|---|---|---|
| استعلام فرعي متداخل | ضعيفة | لا | نعم | — |
| CTE | جيدة | نعم | PG 12+ تقرر تلقائياً | يمكن فرض MATERIALIZED |
| جدول مؤقت | مقبولة | نعم | لا | تكتب على القرص |
(12) ▶ مثال
WITH expensive_calc AS MATERIALIZED (
SELECT customer_id, COUNT(*) AS order_count
FROM orders
GROUP BY customer_id
)
SELECT * FROM expensive_calc
UNION ALL
SELECT * FROM expensive_calc;
Output:
count
-------
5
(1 row)
MATERIALIZED تفرض الحساب مرة واحدة وتخزنه مؤقتاً، مثالية لـ CTEs المشار إليها عدة مرات بحساب ثقيل.
(13) ▶ مثال
WITH simple_filter AS NOT MATERIALIZED (
SELECT * FROM orders WHERE region = 'NA'
)
SELECT * FROM simple_filter WHERE amount > 50000;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
NOT MATERIALIZED تسمح للمحسن بتضمين التوسيع، مناسبة لسيناريوهات دفع المسندات البسيطة.
(2) CTE تكراري (WITH RECURSIVE)
CTE التكراري هو الطريقة القياسية في SQL لبيانات الشجرة والرسوم البيانية.
هيكل الصيغة:
WITH RECURSIVE cte_name AS (
base_query -- ا��مرساة: بذرة غير تكرارية
UNION ALL
recursive_query -- تشير إلى cte_name نفسها
)
SELECT * FROM cte_name;
(14) ▶ مثال
WITH RECURSIVE subordinates AS (
SELECT
employee_id,
name,
manager_id,
1 AS level,
name::text AS path
FROM employees
WHERE manager_id IS NULL
AND name = 'David'
UNION ALL
SELECT
e.employee_id,
e.name,
e.manager_id,
s.level + 1,
s.path || ' > ' || e.name
FROM employees e
INNER JOIN subordinates s ON e.manager_id = s.employee_id
)
SELECT
level,
REPEAT(' ', level - 1) || name AS org_chart,
path
FROM subordinates
ORDER BY path;
level | org_chart | path
-------+------------------------+--------------------------------
1 | David | David
2 | Alice | David > Alice
3 | Bob | David > Alice > Bob
3 | Charlie | David > Alice > Charlie
2 | Eve | David > Eve
3 | Frank | David > Eve > Frank
(15) ▶ مثال
WITH RECURSIVE subordinates AS (
SELECT
employee_id, name, manager_id, 1 AS level
FROM employees
WHERE manager_id IS NULL
UNION ALL
SELECT
e.employee_id, e.name, e.manager_id, s.level + 1
FROM employees e
JOIN subordinates s ON e.manager_id = s.employee_id
WHERE s.level < 5
)
SELECT * FROM subordinates;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
WHERE s.level < 5 يحدد التكرار بـ 5 مستويات كحد أقصى.
(16) ▶ مثال
WITH RECURSIVE date_series AS (
SELECT '2025-01-01'::date AS dt
UNION ALL
SELECT dt + INTERVAL '1 day'
FROM date_series
WHERE dt < '2025-12-31'
)
SELECT dt FROM date_series;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
5. تطبيق عملي
(17) ▶ مثال
SELECT e.name, e.department_id, e.salary
FROM employees e
WHERE e.salary = (
SELECT MAX(salary)
FROM employees e2
WHERE e2.department_id = e.department_id
)
ORDER BY e.department_id;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
(18) ▶ مثال
WITH customer_orders AS (
SELECT
customer_id,
MAX(created_at) AS last_order_date,
COUNT(*) AS frequency,
SUM(amount) AS monetary
FROM orders
GROUP BY customer_id
)
SELECT
customer_id,
frequency,
monetary,
NTILE(4) OVER (ORDER BY monetary DESC) AS m_quartile
FROM customer_orders;
Output:
count
-------
5
(1 row)
(19) ▶ مثال
WITH RECURSIVE category_tree AS (
SELECT
category_id, parent_id, name, 0 AS depth
FROM categories
WHERE parent_id IS NULL
UNION ALL
SELECT
c.category_id, c.parent_id, c.name, ct.depth + 1
FROM categories c
JOIN category_tree ct ON c.parent_id = ct.category_id
)
SELECT
depth,
REPEAT('──', depth) || name AS tree_view
FROM category_tree
ORDER BY depth, name;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
(20) ▶ مثال
SELECT p.product_name
FROM products p
WHERE NOT EXISTS (
SELECT 1 FROM regions r
WHERE NOT EXISTS (
SELECT 1 FROM order_items oi
JOIN orders o ON oi.order_id = o.order_id
WHERE oi.product_id = p.product_id
AND o.region = r.region_code
)
);
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
(21) ▶ مثال
WITH ranked_orders AS (
SELECT
customer_id,
order_id,
amount,
ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY amount DESC) AS rn
FROM orders
)
SELECT customer_id, order_id, amount
FROM ranked_orders
WHERE rn <= 3;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
6. مثال شامل
استعلام Alice للمخطط التنظيمي — من أي مدير، اذكر جميع المرؤوسين مع إزاحة المستوى والمسار وعدد الفريق:
WITH RECURSIVE org_tree AS (
SELECT
employee_id,
name,
manager_id,
1 AS level,
name::text AS path,
ARRAY[employee_id] AS subtree_ids
FROM employees
WHERE name = 'Alice'
UNION ALL
SELECT
e.employee_id,
e.name,
e.manager_id,
o.level + 1,
o.path || ' > ' || e.name,
o.subtree_ids || e.employee_id
FROM employees e
JOIN org_tree o ON e.manager_id = o.employee_id
)
SELECT
o.level,
REPEAT(' ', o.level - 1) || o.name AS org_chart,
o.path,
ARRAY_LENGTH(o.subtree_ids, 1) AS team_size
FROM org_tree o
ORDER BY o.path;
level | org_chart | path | team_size
-------+----------------------------+---------------------------------+-----------
1 | Alice | Alice | 1
2 | Bob | Alice > Bob | 2
3 | Diana | Alice > Bob > Diana | 3
3 | Eve | Alice > Bob > Eve | 4
2 | Charlie | Alice > Charlie | 5
3 | Frank | Alice > Charlie > Frank | 6
7. تدفق تنفيذ CTE التكراري
flowchart TD
A["استعلام المرساة<br/>(بذرة غير تكرارية)"] --> B["جدول العمل T₀"]
B --> C["استعلام تكراري<br/>(JOIN مع T₀)"]
C --> D{"صفوف جديدة<br/>منتجة؟"}
D -->|نعم| E["جدول العمل T₁"]
E --> F["إلحاق بالنتيجة"]
F --> C
D -->|لا| G["النتيجة النهائية<br/>(جميع التكرارات UNION ALL)"]
style A fill:#e1f5fe
style C fill:#fff9c4
style G fill:#c8e6c9
| الخطوة | العملية | الوصف |
|---|---|---|
| 1 | تنفيذ استعلام المرساة | بذرة غير تكرارية، تولد الصفوف الأولية |
| 2 | وضع في جدول العمل | T₀ = نتيجة المرساة |
| 3 | استعلام تكراري | JOIN جدول العمل مع الجدول الأصلي |
| 4 | التحقق من الصفوف الجديدة | استمر إذا وجدت صفوف جديدة؛ توقف إذا لم توجد |
| 5 | دمج النتائج | UNION ALL جميع نتائج التكرارات |
❓ أسئلة شائعة
x NOT IN (a, NULL) مكافئة لـ x<>a AND x<>NULL، و x<>NULL هي UNKNOWN؛ في سلسلة AND، UNKNOWN تجعل الصف بالكامل FALSE. NOT EXISTS أكثر أماناً بدلاً من ذلك.WITH a AS (...), b AS (SELECT ... FROM a)، b يمكنها الإشارة إلى a، بالإشارة للأسفل حسب ترتيب التعريف.📖 ملخص
- الاستعلامات الفرعية تأتي بأربعة أنواع — عددية / عمودية / صفية / جدولية — لكل منها مكانها المناسب
- EXISTS/NOT EXISTS أكثر أماناً من IN/NOT IN ولا تتأثر بـ NULL
- ANY مكافئة لـ "أكبر من الأدنى"؛ ALL لـ "أكبر من الأقصى"
- CTE تستخدم WITH لتعريف نتيجة مؤقتة مسماة، أكثر قابلية للقراءة بكثير من الاستعلامات الفرعية المتداخلة
- PostgreSQL 12+ تقرر تضمين أو تجسيد CTE تلقائياً؛ يمكن التحكم يدوياً
- WITH RECURSIVE تعالج بيانات الشجرة/الرسم البياني وهي الطريقة القياسية في SQL
- CTE التكراري يحتاج مرساة بالإضافة إلى جزء تكراري، مربوطين بـ UNION ALL
- منع التكرار اللانهائي بحد مستوى أو مسار متتبع
📝 تمارين
- ⭐ استخدم استعلاماً فرعياً عددياً لإيجاد الموظفين الذين رواتبهم أعلى من متوسط الشركة.
- ⭐ استخدم NOT EXISTS لإيجاد العملاء الذين لم يقدموا أي طلب.
- ⭐⭐ أعد كتابة الاستعلام الفرعي المتداخل التالي باستخدام CTE: إيجاد الطلبات التي مبلغها أعلى من متوسط مبلغ طلبات ذلك العميل.
- ⭐⭐ استخدم WITH RECURSIVE لاستعلام مرؤوسي مدير محدد حتى 3 مستويات من جدول employees، مع إزاحة المستوى.
- ⭐⭐⭐ باستخدام CTE تكراري مع التجميع: بدءاً من الرئيس التنفيذي، عد التقارير المباشرة لكل مدير وعدد أفراد الشجرة الفرعية بالكامل، كل ذلك في عبارة SQL واحدة.