PostgreSQL: استعلامات الربط متعددة الجداول في PostgreSQL
آخر تحديث: 2026-08-26
1. ما ستتعلمه
- INNER JOIN (الربط الداخلي)
- LEFT / RIGHT / FULL OUTER JOIN (الربط الخارجي)
- CROSS JOIN (الربط المتقاطع)
- NATURAL JOIN (استخدم بحذر)
- الربط الذاتي
- صيغة USING المختصرة
- الربط متعدد الجداول (3+ جداول)
- ميزة PostgreSQL: LATERAL JOIN
- أساسيات أداء JOIN (مقدمة EXPLAIN)
2. القصة
Charlie هو مهندس خلفية في منصة SaaS. يطلب منه مدير المنتج إنشاء تقرير تفاصيل مشتريات المستخدم الذي يحتاج إلى دمج 4 جداول:
- users — معلومات المستخدم
- orders — الطلبات
- order_items — عناصر سطر الطلب
- products — المنتجات
قد يكون لدى المستخدم طلبات متعددة؛ كل طلب له عناصر متعددة؛ كل عنصر يعين لمنتج واحد. يجب على Charlie اختيار نوع JOIN الصحيح لضمان عدم إسقاط المستخدمين بدون طلبات وعدم إنشاء جداء ديكارتي غير متوقع.
3. المفهوم
(1) نظرة عامة على أنواع JOIN
| نوع JOIN | المعنى | أي جانب محتفظ به | السيناريو النموذجي |
|---|---|---|---|
| INNER JOIN | الاحتفاظ فقط بالصفوف المتطابقة | لا جانب | يتطلب تطابقًا للإدراج |
| LEFT JOIN | الاحتفاظ بكل الجدول الأيسر | الأيسر | جدول رئيسي + صفوف مرتبطة اختيارية |
| RIGHT JOIN | الاحتفاظ بكل الجدول الأيمن | الأيمن | نادر الاستخدام |
| FULL JOIN | الاحتفاظ بكلا الجانبين | كلاهما | إيجاد الاختلافات / المطابقة |
| CROSS JOIN | جداء ديكارتي | غير مشروط | التباديل / التوليفات |
| LATERAL JOIN | استعلام فرعي يشير للجدول الأيسر | — | Top-N لكل صف |
flowchart LR
subgraph Inner
A1((A)) --- B1((B))
end
subgraph Left
A2((A)) --- B2((B))
A3((A)) -.-> B3((∅))
end
subgraph Full
A4((A)) --- B4((B))
A5((A)) -.-> B6((∅))
B5((∅)) -.-> A6((A))
end
style A1 fill:#c8e6c9
style B1 fill:#c8e6c9
style A2 fill:#c8e6c9
style A3 fill:#c8e6c9
style B2 fill:#c8e6c9
style A4 fill:#c8e6c9
style A5 fill:#c8e6c9
style B4 fill:#c8e6c9
(2) INNER JOIN
يعيد فقط الصفوف التي تتطابق في كلا الجدولين.
(1) ▶ مثال
SELECT
u.user_id,
u.name,
o.order_id,
o.amount
FROM users u
INNER JOIN orders o ON u.user_id = o.user_id;
user_id | name | order_id | amount
---------+-------+----------+--------
1 | Alice | 101 | 15000
1 | Alice | 102 | 8000
2 | Bob | 201 | 25000
المستخدمون الذين لم يقدموا طلبًا أبدًا لا يظهرون.
(3) LEFT JOIN
يحتفظ بجميع صفوف الجدول الأيسر؛ يملأ NULL على اليمين عندما لا يوجد تط��بق.
(2) ▶ مثال
SELECT
u.user_id,
u.name,
o.order_id,
o.amount
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id;
user_id | name | order_id | amount
---------+---------+----------+--------
1 | Alice | 101 | 15000
1 | Alice | 102 | 8000
2 | Bob | 201 | 25000
3 | Charlie | |
Charlie ليس لديه طلبات، لذا order_id و amount هما NULL.
(3) ▶ مثال
SELECT u.user_id, u.name
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
WHERE o.order_id IS NULL;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
(4) RIGHT JOIN و FULL JOIN
(4) ▶ مثال
SELECT
u.user_id,
u.name,
o.order_id,
o.user_id AS order_user_id
FROM users u
FULL JOIN orders o ON u.user_id = o.user_id
ORDER BY u.user_id NULLS LAST, o.order_id NULLS LAST;
user_id | name | order_id | order_user_id
---------+---------+----------+---------------
1 | Alice | 101 | 1
2 | Bob | 201 | 2
3 | Charlie | |
| | 999 | 99
الطلب بـ user_id=99 ليس له مستخدم مطابق، والمستخدم بـ user_id=3 ليس له طلب — كلا الاختلافين مرئيان.
(5) ▶ مثال
SELECT o.order_id, o.user_id, u.name
FROM users u
RIGHT JOIN orders o ON u.user_id = o.user_id
WHERE u.user_id IS NULL;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
| نوع JOIN | السلوك | مقارنة الاستخدام |
|---|---|---|
| LEFT JOIN | الاحتفاظ بكل الجدول الأيسر | استعلام رئيسي + مرتبط؛ إيجاد فجوات الجانب الأيسر |
| RIGHT JOIN | الاحتفاظ بكل الجدول الأيمن | يمكن إعادة كتابته كـ LEFT JOIN بتبديل الجوانب |
| FULL JOIN | الاحتفاظ بكلا الجانبين | المطابقة، إيجاد الاختلافات |
(5) CROSS JOIN و NATURAL JOIN
(6) ▶ مثال
SELECT
d.department_name,
p.project_name
FROM departments d
CROSS JOIN projects p;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
3 أقسام × 5 مشاريع = 15 صفًا.
(7) ▶ مثال
SELECT * FROM users
NATURAL JOIN orders;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
NATURAL JOIN يربط تلقائيًا على الأعمدة التي لها نفس الاسم في كلا الجدولين. خطير: إذا شارك الجدولان عدة أعمدة بنفس الاسم (مثل created_at)، ينتج بصمت شروط AND إضافية. فضّل ON أو USING الصريحين.
| الصيغة | المزايا | العيوب |
|---|---|---|
| NATURAL JOIN | موجز | تغيير اسم عمود قد يغير منطق الربط |
| USING(col) | موجز وصريح | فقط للربط المتساوي على أعمدة بنفس الاسم |
| ON a.col = b.col | تحكم كامل | مطول |
(6) صيغة USING
(8) ▶ مثال
SELECT
u.name,
o.order_id,
o.amount
FROM users u
JOIN orders o USING (user_id);
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
USING يدمج الأعمدة بنفس الاسم في عمود واحد؛ SELECT * لا يخرج user_id مرتين.
(7) الربط الذاتي
جدول مربوط مع نفسه، مفيد للهرميات أو مقارنات نفس الجدول.
(9) ▶ مثال
SELECT
e.name AS employee,
m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.employee_id;
employee | manager
----------+---------
Alice | David
Bob | Alice
Charlie | Alice
David |
(10) ▶ مثال
SELECT
a.product_name AS product_a,
b.product_name AS product_b,
a.price - b.price AS price_diff
FROM products a
JOIN products b ON a.category_id = b.category_id
AND a.product_id < b.product_id
AND ABS(a.price - b.price) < 100;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
4. النقاط الرئيسية
(1) الربط متعدد الجداول (3+ جداول)
متطلب Charlie: ربط users → orders → order_items → products.
(11) ▶ مثال
SELECT
u.name AS user_name,
o.order_id,
o.created_at AS order_date,
p.product_name,
oi.quantity,
oi.unit_price,
oi.quantity * oi.unit_price AS line_total
FROM users u
JOIN orders o ON u.user_id = o.user_id
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.product_id
ORDER BY u.name, o.order_id, oi.order_item_id;
user_name | order_id | order_date | product_name | quantity | unit_price | line_total
-----------+----------+----------------------+--------------+----------+------------+------------
Alice | 101 | 2025-03-15 10:30:00 | Widget Pro | 2 | 15000 | 30000
Alice | 101 | 2025-03-15 10:30:00 | Gadget Mini | 5 | 3000 | 15000
Alice | 102 | 2025-04-02 14:20:00 | Widget Pro | 1 | 15000 | 15000
Bob | 201 | 2025-05-10 09:00:00 | Server Rack | 1 | 80000 | 80000
(2) LATERAL JOIN (ميزة PostgreSQL)
LATERAL يسمح لاستعلام فرعي بالإشارة إلى أعمدة من الجدول الأيسر — عمليًا، يعمل الاستعلام الفرعي مرة واحدة لكل صف من الجدول الأيسر.
(12) ▶ مثال
SELECT
u.name,
recent.order_id,
recent.amount,
recent.created_at
FROM users u
LEFT JOIN LATERAL (
SELECT o.order_id, o.amount, o.created_at
FROM orders o
WHERE o.user_id = u.user_id
ORDER BY o.created_at DESC
LIMIT 3
) recent ON true
ORDER BY u.name, recent.created_at DESC;
Output:
CREATE TABLE
| النهج | يمكنه الإشارة للجدول الأيسر | دعم Top-N | الأداء |
|---|---|---|---|
| استعلام فرعي عادي | لا | يحتاج ROW_NUMBER نافذة | مسح واحد |
| LATERAL | نعم | LIMIT مباشر | استعلام فرعي يعمل لكل صف |
| دالة نافذة | — | ROW_NUMBER + مرشح | مسح واحد |
(13) ▶ مثال
SELECT
p.product_name,
r.review_text,
r.created_at
FROM products p
LEFT JOIN LATERAL (
SELECT review_text, created_at
FROM reviews r
WHERE r.product_id = p.product_id
ORDER BY created_at DESC
LIMIT 1
) r ON true;
Output:
CREATE TABLE
(3) أساسيات أداء JOIN
(14) ▶ مثال
EXPLAIN
SELECT u.name, o.order_id
FROM users u
JOIN orders o ON u.user_id = o.user_id
WHERE o.amount > 50000;
Hash Join
Hash Cond: (o.user_id = u.user_id)
-> Seq Scan on orders
Filter: (amount > 50000)
-> Hash
-> Seq Scan on users
| استراتيجية JOIN | متى تستخدم | الخصائص |
|---|---|---|
| Nested Loop | جدول صغير يقود جدول كبير | جيد للشروط المتساوية المفهرسة |
| Hash Join | ربط متساوٍ، بدون فهرس | يبني جدول تجزئة؛ مفضل للأحجام الكبيرة |
| Merge Join | بيانات مرتبة بالفعل | يحتاج كلا الجانبين مرتبين |
(15) ▶ مثال
EXPLAIN ANALYZE
SELECT u.name, o.amount
FROM users u
JOIN orders o ON u.email = o.customer_email;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
إذا لم يكن عمود email مفهرسًا، قد يتدهور إلى Nested Loop مسح جدول كامل.
(16) ▶ مثال
CREATE INDEX idx_orders_user_id ON orders(user_id);
Output:
CREATE TABLE
5. تطبيق عملي
(17) ▶ مثال
SELECT
u.user_id,
u.name,
COUNT(o.order_id) AS order_count
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
GROUP BY u.user_id, u.name
ORDER BY order_count DESC;
Output:
count
-------
5
(1 row)
(18) ▶ مثال
SELECT
a.user_id AS system_a_id,
a.email AS system_a_email,
b.user_id AS system_b_id,
b.email AS system_b_email
FROM system_a_users a
FULL JOIN system_b_users b ON a.email = b.email
ORDER BY a.user_id NULLS LAST, b.user_id NULLS LAST;
Output:
id | name | value
----+----------+-------
1 | example | 42
(1 row)
(19) ▶ مثال
SELECT DISTINCT
a.name AS user_a,
b.name AS user_b
FROM orders oa
JOIN users a ON oa.user_id = a.user_id
JOIN orders ob ON DATE(oa.created_at) = DATE(ob.created_at)
JOIN users b ON ob.user_id = b.user_id
WHERE a.user_id < b.user_id;
Output:
CREATE TABLE
6. مثال شامل
تقرير تفاصيل مشتريات المستخدم لـ Charlie — ربط 4 جداول + LATERAL للطلبات الحديثة:
SELECT
u.name AS user_name,
u.email,
coalesce(order_summary.total_orders, 0) AS total_orders,
coalesce(order_summary.total_spent, 0) AS total_spent,
recent.order_id AS latest_order_id,
recent.created_at AS latest_order_date,
recent.amount AS latest_amount
FROM users u
LEFT JOIN LATERAL (
SELECT
COUNT(*) AS total_orders,
SUM(amount) AS total_spent
FROM orders o
WHERE o.user_id = u.user_id
) order_summary ON true
LEFT JOIN LATERAL (
SELECT order_id, created_at, amount
FROM orders o
WHERE o.user_id = u.user_id
ORDER BY created_at DESC
LIMIT 1
) recent ON true
ORDER BY total_spent DESC NULLS LAST;
user_name | email | total_orders | total_spent | latest_order_id | latest_order_date | latest_amount
-----------+--------------------+--------------+-------------+-----------------+-----------------------+--------------
Bob | bob@example.com | 5 | 320000 | 205 | 2025-11-20 16:00:00 | 85000
Alice | alice@example.com | 3 | 180000 | 102 | 2025-04-02 14:20:00 | 15000
Charlie | charlie@example.com| 0 | 0 | | |
7. شجرة قرار اختيار JOIN
flowchart TD
A[تحتاج ربط جداول متعددة؟] -->|لا| Z[لا حاجة لـ JOIN]
A -->|نعم| B{تحتاج الاحتفاظ<br/>بصفوف غير متطابقة؟}
B -->|لا| C[INNER JOIN]
B -->|نعم، احتفظ بالأيسر| D[LEFT JOIN]
B -->|نعم، احتفظ بالأيمن| E[RIGHT JOIN]
B -->|نعم، احتفظ بكليهما| F[FULL JOIN]
C --> G{تحتاج Top-N<br/>أو إشارة لأعمدة يسرى؟}
G -->|نعم| H[LATERAL JOIN]
G -->|لا| I[INNER JOIN عادي]
D --> G
style H fill:#c8e6c9
style F fill:#fff9c4
❓ أسئلة شائعة
📖 ملخص
- INNER JOIN يحتفظ فقط بالصفوف المتطابقة؛ LEFT JOIN يحتفظ بكل الجدول الأيسر
- RIGHT JOIN يمكن إعادة كتابته كـ LEFT JOIN بتبديل الجوانب؛ FULL JOIN يحتفظ بكلا الجانبين
- CROSS JOIN ينتج جداء ديكارتي؛ NATURAL JOIN الربط الضمني يحتاج حذرًا
- الربط الذاتي يستخدم للهرميات ومقارنات نفس الجدول؛ أعط كل جدول اسمًا مستعارًا مميزًا
- USING يبسط الربط المتساوي على أعمدة بنفس الاسم؛ ON يدعم شروطًا اعتباطية
- LATERAL JOIN يمكنه الإشارة لأعمدة الجدول الأيسر، مثالي لسيناريوهات Top-N
- اربط جداول متعددة مستوى بمستوى على طول العلاقات؛ انتبه لترتيب LEFT JOIN
- EXPLAIN يكشف استراتيجية JOIN؛ الفهارس هي مفتاح الأداء
📝 تمارين
- ⭐ اكتب استعلام INNER JOIN يربط orders و order_items، ويخرج الكمية الإجمالية (SUM quantity) لكل طلب.
- ⭐ استخدم LEFT JOIN لإيجاد المنتجات التي لم يتم شراؤها أبدًا (products LEFT JOIN order_items، مرشح لـ NULL).
- ⭐⭐ اربط users → orders → order_items → products وأخرج تفاصيل مشتريات كل مستخدم، بما في ذلك اسم المنتج ومبلغ السطر.
- ⭐⭐ استخدم LATERAL JOIN لإيجاد أغلى منتجين في كل فئة منتجات.
- ⭐⭐⭐ اكتب عبارة SQL باستخدام FULL JOIN لمقارنة جداول مستخدمي نظامين (system_a_users / system_b_users)، مع وضع علامة على السجلات الموجودة فقط في A، فقط في B، أو في كليهما، وعد كم يقع في كل فئة.