الربط الذاتي SELF JOIN
مقدمة حول SELF JOIN في SQL
الـ SELF JOIN (الربط الذاتي) هو تقنية في SQL حيث يتم ربط الجدول بنفسه. على الرغم من أن الجدول واحد، نتعامل معه كأنه جدولان مختلفان باستخدام الأسماء المستعارة (Aliases).
هذه التقنية قوية جداً وتُستخدم في حالات متعددة مثل العلاقات الهرمية (الموظفين والمدراء)، المقارنات داخل نفس الجدول، وإيجاد الأزواج أو التسلسلات.
لماذا نحتاج SELF JOIN؟
- تمثيل العلاقات الهرمية (مثل: موظف ومديره)
- مقارنة الصفوف ببعضها داخل نفس الجدول
- إيجاد الأزواج أو المجموعات المرتبطة
- تحليل التسلسلات والعلاقات المتصلة
- بناء الهياكل التنظيمية والأشجار
ملاحظة مهمة: SELF JOIN ليس نوعاً خاصاً من JOIN، بل هو استخدام لأنواع JOIN العادية (INNER, LEFT, RIGHT) لكن على نفس الجدول. الفرق الوحيد هو أننا نربط الجدول بنفسه.
الصيغة الأساسية لـ SELF JOIN
الصيغة العامة:
SELECT columns
FROM table_name AS alias1
JOIN table_name AS alias2
ON alias1.column = alias2.column;
عناصر SELF JOIN الأساسية:
- نفس الجدول مرتين: نستخدم نفس الجدول في FROM و JOIN
- أسماء مستعارة مختلفة: يجب استخدام aliases مختلفة (مثل: e1, e2)
- شرط الربط: نحدد كيف نربط الصفوف ببعضها
- نوع JOIN: يمكن استخدام INNER, LEFT, RIGHT حسب الحاجة
مثال بسيط:
-- مثال: جدول الموظفين مع معرف المدير
CREATE TABLE employees (
emp_id INT PRIMARY KEY,
emp_name VARCHAR(100),
manager_id INT, -- يشير إلى emp_id للمدير
salary DECIMAL(10, 2)
);
-- ربط الموظف بمديره
SELECT
e.emp_name AS الموظف,
m.emp_name AS المدير
FROM employees e
INNER JOIN employees m ON e.manager_id = m.emp_id;
المثال الكلاسيكي: الهيكل التنظيمي
إنشاء جدول الموظفين:
CREATE TABLE employees (
emp_id INT PRIMARY KEY,
emp_name VARCHAR(100),
position VARCHAR(50),
manager_id INT,
salary DECIMAL(10, 2),
department VARCHAR(50)
);
-- إدراج بيانات الموظفين
INSERT INTO employees VALUES
(1, 'أحمد محمد', 'المدير العام', NULL, 25000.00, 'إدارة'),
(2, 'فاطمة علي', 'مدير تطوير', 1, 18000.00, 'تطوير'),
(3, 'خالد حسن', 'مدير تسويق', 1, 17000.00, 'تسويق'),
(4, 'سارة أحمد', 'مطور أول', 2, 12000.00, 'تطوير'),
(5, 'محمد عبدالله', 'مطور', 2, 9000.00, 'تطوير'),
(6, 'نورا خالد', 'مسوق أول', 3, 11000.00, 'تسويق'),
(7, 'علي حسين', 'مسوق', 3, 8000.00, 'تسويق'),
(8, 'ليلى سعيد', 'مطور', 4, 8500.00, 'تطوير');
عرض الموظفين مع مدرائهم:
-- INNER JOIN: فقط الموظفين الذين لديهم مدراء
SELECT
e.emp_id,
e.emp_name AS الموظف,
e.position AS المنصب,
e.salary AS الراتب,
m.emp_name AS المدير_المباشر,
m.position AS منصب_المدير
FROM employees e
INNER JOIN employees m ON e.manager_id = m.emp_id
ORDER BY e.emp_id;
| emp_id | الموظف | المنصب | الراتب | المدير_المباشر | منصب_المدير |
|---|---|---|---|---|---|
| 2 | فاطمة علي | مدير تطوير | 18000.00 | أحمد محمد | المدير العام |
| 3 | خالد حسن | مدير تسويق | 17000.00 | أحمد محمد | المدير العام |
| 4 | سارة أحمد | مطور أول | 12000.00 | فاطمة علي | مدير تطوير |
| 5 | محمد عبدالله | مطور | 9000.00 | فاطمة علي | مدير تطوير |
| 6 | نورا خالد | مسوق أول | 11000.00 | خالد حسن | مدير تسويق |
| 7 | علي حسين | مسوق | 8000.00 | خالد حسن | مدير تسويق |
| 8 | ليلى سعيد | مطور | 8500.00 | سارة أحمد | مطور أول |
لاحظ: المدير العام (أحمد محمد) لا يظهر في النتيجة لأن manager_id الخاص به هو NULL ونحن استخدمنا INNER JOIN.
عرض جميع الموظفين (بما في ذلك المدير العام):
-- LEFT JOIN: جميع الموظفين حتى بدون مدير
SELECT
e.emp_id,
e.emp_name AS الموظف,
e.position AS المنصب,
COALESCE(m.emp_name, 'لا يوجد (المدير الأعلى)') AS المدير_المباشر
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.emp_id
ORDER BY e.emp_id;
المقارنات داخل نفس الجدول
مثال 1: إيجاد الموظفين في نفس القسم
-- إيجاد أزواج الموظفين الذين يعملون في نفس القسم
SELECT
e1.emp_name AS الموظف_الأول,
e2.emp_name AS الموظف_الثاني,
e1.department AS القسم
FROM employees e1
INNER JOIN employees e2
ON e1.department = e2.department
AND e1.emp_id < e2.emp_id -- لتجنب التكرار
ORDER BY e1.department, e1.emp_name;
شرح الشرط e1.emp_id < e2.emp_id:
- يمنع ظهور نفس الموظف مع نفسه (e1 = e2)
- يمنع التكرار (إذا ظهر أحمد مع فاطمة، لن يظهر فاطمة مع أحمد)
- يضمن أن كل زوج يظهر مرة واحدة فقط
مثال 2: إيجاد الموظفين برواتب أعلى من زملائهم
-- الموظفين الذين رواتبهم أعلى من زملائهم في نفس القسم
SELECT
e1.emp_name AS الموظف,
e1.salary AS راتبه,
e2.emp_name AS زميله,
e2.salary AS راتب_الزميل,
e1.department AS القسم,
(e1.salary - e2.salary) AS الفرق
FROM employees e1
INNER JOIN employees e2
ON e1.department = e2.department
AND e1.salary > e2.salary
ORDER BY e1.department, e1.salary DESC;
مثال 3: مقارنة الموظفين بمتوسط قسمهم
-- الموظفين الذين رواتبهم أعلى من متوسط قسمهم
SELECT
e.emp_name AS الموظف,
e.salary AS الراتب,
e.department AS القسم,
AVG(e2.salary) AS متوسط_القسم,
e.salary - AVG(e2.salary) AS الفرق_عن_المتوسط
FROM employees e
INNER JOIN employees e2 ON e.department = e2.department
GROUP BY e.emp_id, e.emp_name, e.salary, e.department
HAVING e.salary > AVG(e2.salary)
ORDER BY الفرق_عن_المتوسط DESC;
أمثلة متقدمة
مثال 1: الهيكل التنظيمي متعدد المستويات
-- عرض الموظف ومديره ومدير مديره
SELECT
e.emp_name AS الموظف,
e.position AS المنصب,
m1.emp_name AS المدير_المباشر,
m2.emp_name AS مدير_المدير
FROM employees e
LEFT JOIN employees m1 ON e.manager_id = m1.emp_id
LEFT JOIN employees m2 ON m1.manager_id = m2.emp_id
ORDER BY e.emp_id;
مثال 2: عدد المرؤوسين لكل مدير
-- حساب عدد الموظفين تحت إدارة كل مدير
SELECT
m.emp_id,
m.emp_name AS المدير,
m.position AS المنصب,
COUNT(e.emp_id) AS عدد_المرؤوسين,
AVG(e.salary) AS متوسط_رواتب_الفريق,
SUM(e.salary) AS إجمالي_رواتب_الفريق
FROM employees m
LEFT JOIN employees e ON m.emp_id = e.manager_id
GROUP BY m.emp_id, m.emp_name, m.position
HAVING COUNT(e.emp_id) > 0
ORDER BY عدد_المرؤوسين DESC;
| emp_id | المدير | المنصب | عدد_المرؤوسين | متوسط_رواتب_الفريق | إجمالي_رواتب_الفريق |
|---|---|---|---|---|---|
| 2 | فاطمة علي | مدير تطوير | 2 | 10500.00 | 21000.00 |
| 1 | أحمد محمد | المدير العام | 2 | 17500.00 | 35000.00 |
| 3 | خالد حسن | مدير تسويق | 2 | 9500.00 | 19000.00 |
| 4 | سارة أحمد | مطور أول | 1 | 8500.00 | 8500.00 |
مثال 3: المسارات والاتصالات
-- مثال: جدول الرحلات الجوية
CREATE TABLE flights (
flight_id INT PRIMARY KEY,
from_city VARCHAR(50),
to_city VARCHAR(50),
price DECIMAL(10, 2)
);
INSERT INTO flights VALUES
(1, 'الرياض', 'جدة', 300),
(2, 'جدة', 'الدمام', 400),
(3, 'الرياض', 'الدمام', 500),
(4, 'الدمام', 'مكة', 350),
(5, 'جدة', 'مكة', 200);
-- إيجاد الرحلات المتصلة (رحلة + رحلة متصلة)
SELECT
f1.from_city AS المدينة_الأولى,
f1.to_city AS المدينة_الوسطى,
f2.to_city AS المدينة_النهائية,
f1.price + f2.price AS السعر_الإجمالي,
f1.flight_id AS رحلة_1,
f2.flight_id AS رحلة_2
FROM flights f1
INNER JOIN flights f2 ON f1.to_city = f2.from_city
WHERE f1.from_city = 'الرياض'
ORDER BY السعر_الإجمالي;
حالات استخدام عملية إضافية
مثال 4: مقارنة المنتجات
CREATE TABLE products (
product_id INT PRIMARY KEY,
product_name VARCHAR(100),
category VARCHAR(50),
price DECIMAL(10, 2),
rating DECIMAL(3, 2)
);
INSERT INTO products VALUES
(1, 'لابتوب Dell', 'إلكترونيات', 3500, 4.5),
(2, 'لابتوب HP', 'إلكترونيات', 3200, 4.3),
(3, 'لابتوب Lenovo', 'إلكترونيات', 2800, 4.2),
(4, 'هاتف iPhone', 'إلكترونيات', 4000, 4.7),
(5, 'هاتف Samsung', 'إلكترونيات', 3500, 4.4);
-- مقارنة المنتجات في نفس الفئة
SELECT
p1.product_name AS المنتج,
p1.price AS السعر,
p1.rating AS التقييم,
p2.product_name AS منتج_للمقارنة,
p2.price AS سعره,
p2.rating AS تقييمه,
CASE
WHEN p1.price < p2.price AND p1.rating >= p2.rating THEN 'أفضل قيمة'
WHEN p1.price > p2.price AND p1.rating > p2.rating THEN 'أفضل جودة'
WHEN p1.price < p2.price THEN 'أرخص'
WHEN p1.rating > p2.rating THEN 'تقييم أعلى'
ELSE 'متشابه'
END AS المقارنة
FROM products p1
INNER JOIN products p2
ON p1.category = p2.category
AND p1.product_id < p2.product_id
WHERE p1.category = 'إلكترونيات'
ORDER BY p1.product_name;
مثال 5: إيجاد التكرارات
CREATE TABLE customers (
customer_id INT PRIMARY KEY,
customer_name VARCHAR(100),
email VARCHAR(100),
phone VARCHAR(20)
);
-- إيجاد العملاء المكررين (نفس البريد أو نفس الهاتف)
SELECT
c1.customer_id AS معرف_1,
c1.customer_name AS اسم_1,
c2.customer_id AS معرف_2,
c2.customer_name AS اسم_2,
CASE
WHEN c1.email = c2.email THEN 'بريد مكرر'
WHEN c1.phone = c2.phone THEN 'هاتف مكرر'
END AS نوع_التكرار,
c1.email AS البريد
FROM customers c1
INNER JOIN customers c2
ON (c1.email = c2.email OR c1.phone = c2.phone)
AND c1.customer_id < c2.customer_id;
مثال 6: التسلسل الزمني
CREATE TABLE sales (
sale_id INT PRIMARY KEY,
sale_date DATE,
amount DECIMAL(10, 2)
);
-- مقارنة كل عملية بيع بالعملية السابقة
SELECT
s1.sale_date AS التاريخ,
s1.amount AS المبيعات,
s2.sale_date AS التاريخ_السابق,
s2.amount AS المبيعات_السابقة,
s1.amount - s2.amount AS الفرق,
ROUND((s1.amount - s2.amount) * 100.0 / s2.amount, 2) AS نسبة_التغير
FROM sales s1
LEFT JOIN sales s2 ON s2.sale_date = (
SELECT MAX(sale_date)
FROM sales
WHERE sale_date < s1.sale_date
)
ORDER BY s1.sale_date;
أخطاء شائعة وكيفية تجنبها
1. نسيان استخدام الأسماء المستعارة
خطأ شائع: عدم استخدام aliases مختلفة
-- خطأ: غير واضح أي employees نقصد
SELECT emp_name, manager_id
FROM employees
JOIN employees ON manager_id = emp_id;
-- Error: Column 'emp_name' is ambiguous
-- الصحيح: استخدم aliases واضحة
SELECT e.emp_name, m.emp_name AS manager_name
FROM employees e
JOIN employees m ON e.manager_id = m.emp_id;
2. عدم تجنب التكرار في المقارنات
خطأ شائع: الحصول على نتائج مكررة
-- خطأ: سيظهر كل زوج مرتين
SELECT e1.emp_name, e2.emp_name
FROM employees e1
JOIN employees e2 ON e1.department = e2.department
WHERE e1.emp_id != e2.emp_id;
-- أحمد-فاطمة و فاطمة-أحمد (تكرار!)
-- الصحيح: استخدم < أو >
SELECT e1.emp_name, e2.emp_name
FROM employees e1
JOIN employees e2 ON e1.department = e2.department
WHERE e1.emp_id < e2.emp_id;
3. نسيان معالجة القيم NULL
خطأ شائع: عدم التعامل مع الصفوف التي ليس لها مطابق
-- قد يفوتك المدير الأعلى
SELECT e.emp_name, m.emp_name AS manager
FROM employees e
INNER JOIN employees m ON e.manager_id = m.emp_id;
-- المدير الأعلى لن يظهر!
-- الأفضل: استخدم LEFT JOIN
SELECT
e.emp_name,
COALESCE(m.emp_name, 'المدير الأعلى') AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.emp_id;
4. شروط ربط خاطئة
خطأ شائع: شرط ربط يؤدي لنتائج غير متوقعة
-- خطأ: سيربط كل موظف بكل موظف!
SELECT e1.emp_name, e2.emp_name
FROM employees e1
JOIN employees e2 ON e1.department = e2.department;
-- نتيجة ضخمة جداً (Cartesian product جزئي)
-- الصحيح: أضف شرط إضافي
SELECT e1.emp_name, e2.emp_name
FROM employees e1
JOIN employees e2
ON e1.department = e2.department
AND e1.emp_id < e2.emp_id;
أفضل الممارسات
1. استخدم أسماء مستعارة واضحة ومعبرة
-- جيد: أسماء واضحة
SELECT
emp.emp_name AS الموظف,
mgr.emp_name AS المدير
FROM employees emp
LEFT JOIN employees mgr ON emp.manager_id = mgr.emp_id;
-- أفضل: أسماء أكثر وضوحاً
SELECT
employee.emp_name AS الموظف,
manager.emp_name AS المدير
FROM employees employee
LEFT JOIN employees manager ON employee.manager_id = manager.emp_id;
2. اختر نوع JOIN المناسب
- INNER JOIN: عندما تريد فقط الصفوف المتطابقة
- LEFT JOIN: عندما تريد جميع الصفوف من الجدول الأول
- RIGHT JOIN: نادراً ما يُستخدم في SELF JOIN
3. تجنب التكرار في المقارنات
-- استخدم دائماً < أو > لتجنب التكرار
SELECT p1.product_name, p2.product_name
FROM products p1
JOIN products p2
ON p1.category = p2.category
AND p1.product_id < p2.product_id;
4. استخدم COALESCE للقيم الافتراضية
-- معالجة NULL بشكل صحيح
SELECT
e.emp_name,
COALESCE(m.emp_name, 'لا يوجد مدير') AS المدير,
COALESCE(m.position, 'N/A') AS منصب_المدير
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.emp_id;
5. انتبه للأداء
SELF JOIN يمكن أن يكون بطيئاً على الجداول الكبيرة:
- تأكد من وجود فهارس على أعمدة الربط
- استخدم WHERE لتقليل عدد الصفوف
- فكر في استخدام CTEs أو Subqueries للاستعلامات المعقدة
-- تأكد من وجود فهرس
CREATE INDEX idx_manager_id ON employees(manager_id);
CREATE INDEX idx_department ON employees(department);
ملخص الدرس
في هذا الدرس، تعلمنا عن SELF JOIN في SQL:
- SELF JOIN هو ربط الجدول بنفسه
- يتطلب استخدام أسماء مستعارة (aliases) مختلفة
- مفيد جداً للعلاقات الهرمية (موظف-مدير)
- يُستخدم لمقارنة الصفوف ببعضها داخل نفس الجدول
- يمكن استخدام INNER, LEFT, أو RIGHT JOIN
- استخدم e1.id < e2.id لتجنب التكرار في المقارنات
- استخدم LEFT JOIN لتضمين الصفوف بدون مطابق (مثل المدير الأعلى)
- مفيد لبناء الهياكل التنظيمية والأشجار
- يمكن استخدامه لإيجاد المسارات والاتصالات
- انتبه للأداء وأضف فهارس على أعمدة الربط
حالات الاستخدام الشائعة لـ SELF JOIN:
- الهياكل التنظيمية (الموظفين والمدراء)
- العلاقات الاجتماعية (الأصدقاء، المتابعون)
- الشبكات والمسارات (الرحلات، الطرق)
- المقارنات (المنتجات، الأسعار)
- التسلسلات الزمنية (المقارنة بالفترة السابقة)
- إيجاد التكرارات والأزواج
نصيحة مهمة: SELF JOIN تقنية قوية لكن يجب استخدامها بحذر. تأكد من أن شروط الربط صحيحة لتجنب النتائج الضخمة أو غير المتوقعة.
في الدرس القادم: سنبدأ في تعلم الاستعلامات الفرعية (Subqueries) في SQL، وهي تقنية قوية لكتابة استعلامات متداخلة، مع شرح الأنواع المختلفة وحالات الاستخدام العملية.
الخطوة التالية: مقدمة إلى الاستعلامات الفرعية (Subqueries)
أكمل رحلتك التعليمية وانتقل إلى الدرس التالي لتعلم مقدمة إلى الاستعلامات الفرعية (Subqueries) وتطوير مهاراتك في قواعد البيانات.
الانتقال إلى الدرس التالي