دليل شامل لربط الجداول في SQL: فهم أنواع الربط CROSS JOIN، FULL OUTER JOIN، INNER JOIN، LEFT JOIN، و RIGHT JOIN
مقدمة إلى عمليات الربط في SQL
تُعد عمليات الربط (SQL joins) حجر الزاوية الذي يجعل أنظمة إدارة قواعد البيانات العلائقية (relational database management systems) قادرة على إظهار العلاقات بين البيانات. تسمح لنا هذه العمليات بإعادة بناء العلاقات بين جداول قاعدة البيانات المنفصلة، مما يُمكِّن تطبيقاتنا من العمل بكفاءة. في هذا المقال، سنتعمق في كل نوع من أنواع الربط في SQL وكيفية استخدامها بفعالية.
إليك ما سنتناوله:
- ما هي عملية الربط (
join)؟ - إعداد قاعدة البيانات الخاصة بك
- الربط التقاطعي (
CROSS JOIN) - إعداد بيانات الأمثلة (المخرجون والأفلام)
- الربط الخارجي الكامل (
FULL OUTER JOIN) - الربط الداخلي (
INNER JOIN) - الربط الأيسر (
LEFT JOIN) / الربط الأيمن (RIGHT JOIN) - التصفية باستخدام الربط الأيسر (
LEFT JOIN) - عمليات ربط متعددة
- عمليات الربط مع شروط إضافية
- الواقع العملي لكتابة الاستعلامات باستخدام عمليات الربط (تنبيه: سنتناول خمسة أنواع مختلفة، لكنك ستحتاج حقًا إلى معرفة نوعين فقط!)
ما هي عملية الربط (Join)؟
عملية الربط هي عملية تجمع صفين معًا في صف واحد. عادةً ما تكون هذه الصفوف من جدولين مختلفين، ولكن ليس بالضرورة أن تكون كذلك. قبل أن نتعمق في كيفية كتابة عملية الربط نفسها، دعنا نلقي نظرة على الشكل الذي ستبدو عليه نتيجة عملية الربط.
لنأخذ على سبيل المثال نظامًا يخزن معلومات عن المستخدمين وعناوينهم. قد تبدو صفوف الجدول الذي يخزن معلومات المستخدمين كالتالي:
id | name | email | age
----+--------------+---------------------+-----
1 | John Smith | johnsmith@gmail.com | 25
2 | Jane Doe | janedoe@Gmail.com | 28
3 | Xavier Wills | xavier@wills.io | 3
... (7 rows)
وقد تبدو صفوف الجدول الذي يخزن معلومات العناوين كالتالي:
id | street | city | state | user_id
----+-------------------+---------------+-------+---------
1 | 1234 Main Street | Oklahoma City | OK | 1
2 | 4444 Broadway Ave | Oklahoma City | OK | 2
3 | 5678 Party Ln | Tulsa | OK | 3
(3 rows)
يمكننا كتابة استعلامات منفصلة لاسترداد معلومات المستخدم ومعلومات العنوان، ولكن من الناحية المثالية، يمكننا كتابة استعلام واحد وتلقي جميع المستخدمين وعناوينهم في نفس مجموعة النتائج. هذا بالضبط ما تسمح لنا به عملية الربط!
سنتناول كيفية كتابة هذه العمليات قريبًا، ولكن إذا قمنا بربط معلومات المستخدم الخاصة بنا بمعلومات العنوان، يمكننا الحصول على نتيجة كهذه:
id | name | email | age | id | street | city | state | user_id
----+--------------+---------------------+-----+----+-------------------+---------------+-------+---------
1 | John Smith | johnsmith@gmail.com | 25 | 1 | 1234 Main Street | Oklahoma City | OK | 1
2 | Jane Doe | janedoe@Gmail.com | 28 | 2 | 4444 Broadway Ave | Oklahoma City | OK | 2
3 | Xavier Wills | xavier@wills.io | 35 | 3 | 5678 Party Ln | Tulsa | OK | 3
(3 rows)
هنا نرى جميع مستخدمينا وعناوينهم في مجموعة نتائج واحدة أنيقة. بالإضافة إلى إنتاج مجموعة نتائج مدمجة، فإن استخدامًا آخر مهمًا لعمليات الربط هو سحب معلومات إضافية إلى استعلامنا يمكننا التصفية بناءً عليها. على سبيل المثال، إذا أردنا إرسال بعض البريد المادي إلى جميع المستخدمين الذين يعيشون في مدينة أوكلاهوما، يمكننا استخدام مجموعة النتائج المدمجة هذه والتصفية بناءً على عمود city.
الآن بعد أن عرفنا الغرض من عمليات الربط، دعنا نبدأ في كتابة بعضها!
إعداد قاعدة البيانات الخاصة بك
قبل أن نتمكن من كتابة استعلاماتنا، نحتاج إلى إعداد قاعدة البيانات الخاصة بنا. لهذه الأمثلة، سنستخدم PostgreSQL، ولكن الاستعلامات والمفاهيم الموضحة هنا ستترجم بسهولة إلى أي نظام قاعدة بيانات حديث آخر (مثل MySQL، SQL Server، إلخ).
للعمل مع قاعدة بيانات PostgreSQL الخاصة بنا، يمكننا استخدام psql – برنامج سطر أوامر PostgreSQL التفاعلي. إذا كان لديك عميل قاعدة بيانات آخر تستمتع بالعمل معه، فلا بأس بذلك أيضًا.
للبدء، دعنا ننشئ قاعدة بياناتنا. مع تثبيت PostgreSQL بالفعل، يمكننا تشغيل الأمر createdb <database-name> في الطرفية لإنشاء قاعدة بيانات جديدة. لقد أطلقت على قاعدتي اسم fcc:
$ createdb fcc
بعد ذلك، دعنا نبدأ وحدة التحكم التفاعلية باستخدام الأمر psql ونتصل بقاعدة البيانات التي أنشأناها للتو باستخدام \c <database-name>:
$ psql
psql (11.5)
Type "help" for help.
john=# \c fcc
You are now connected to database "fcc" as user "john".
fcc=#
ملاحظة: لقد قمت بتنظيف مخرجات psql في هذه الأمثلة لتسهيل قراءتها، لذا لا تقلق إذا لم يكن الإخراج المعروض هنا هو بالضبط ما رأيته في الطرفية الخاصة بك. أشجعك على متابعة هذه الأمثلة وتشغيل هذه الاستعلامات بنفسك. ستتعلم وتتذكر أكثر بكثير من خلال العمل على هذه الأمثلة بدلاً من مجرد قراءتها.
الآن إلى عمليات الربط!
الربط التقاطعي (CROSS JOIN): المنتج الديكارتي
أبسط أنواع الربط التي يمكننا القيام بها هو CROSS JOIN أو “المنتج الديكارتي” (Cartesian product). يأخذ هذا الربط كل صف من جدول ويربطه بكل صف من الجدول الآخر. إذا كان لدينا قائمتان – إحداهما تحتوي على 1, 2, 3 والأخرى تحتوي على A, B, C – فإن المنتج الديكارتي لهاتين القائمتين سيكون كالتالي:
1 A, 1 B, 1 C
2 A, 2 B, 2 C
3 A, 3 B, 3 C
يتم إقران كل قيمة من القائمة الأولى بكل قيمة من القائمة الثانية. دعنا نكتب هذا المثال نفسه كاستعلام SQL. أولاً، دعنا ننشئ جدولين بسيطين جدًا وندرج بعض البيانات فيهما:
CREATE TABLE letters(
letter TEXT
);
INSERT INTO letters(letter) VALUES
( 'A' ),
( 'B' ),
( 'C' );
CREATE TABLE numbers(
number TEXT
);
INSERT INTO numbers( number ) VALUES
( 1 ),
( 2 ),
( 3 );
جدولانا، letters و numbers، يحتويان على عمود واحد فقط: حقل نصي بسيط. الآن دعنا نربطهما معًا باستخدام CROSS JOIN:
SELECT * FROM letters CROSS JOIN numbers;
letter | number
--------+--------
A | 1
A | 2
A | 3
B | 1
B | 2
B | 3
C | 1
C | 2
C | 3
(9 rows)
هذا هو أبسط نوع من الربط يمكننا القيام به، ولكن حتى في هذا المثال البسيط، يمكننا رؤية الربط في العمل: تم ربط الصفين المنفصلين (واحد من letters وواحد من numbers) معًا لتشكيل صف واحد. بينما يُناقش هذا النوع من الربط غالبًا كمثال أكاديمي بحت، إلا أن له على الأقل حالة استخدام جيدة واحدة: تغطية نطاقات التواريخ.
استخدام CROSS JOIN مع نطاقات التواريخ
إحدى حالات الاستخدام الجيدة لـ CROSS JOIN هي أخذ كل صف من جدول وتطبيقه على كل يوم ضمن نطاق زمني. لنفترض على سبيل المثال أنك كنت تبني تطبيقًا يتتبع المهام اليومية، مثل تنظيف أسنانك أو تناول الإفطار أو الاستحمام. إذا أردت إنشاء سجل لكل مهمة ولكل يوم من أيام الأسبوع الماضي، يمكنك استخدام CROSS JOIN مقابل نطاق زمني.
لإنشاء هذا النطاق الزمني، يمكننا استخدام الدالة generate_series:
SELECT generate_series(
( CURRENT_DATE - INTERVAL '5 day' ),
CURRENT_DATE,
INTERVAL '1 day'
)::DATE AS day;
تأخذ الدالة generate_series ثلاثة معلمات. المعلمة الأولى هي القيمة البدائية. في هذا المثال، نستخدم CURRENT_DATE - INTERVAL '5 day'. هذا يعيد التاريخ الحالي مطروحًا منه خمسة أيام – أو “قبل خمسة أيام”. المعلمة الثانية هي التاريخ الحالي (CURRENT_DATE). المعلمة الثالثة هي “فترة الزيادة” (step interval) – أو مقدار ما نريد زيادة القيمة في كل مرة. نظرًا لأن هذه مهام يومية، سنستخدم فترة يوم واحد (INTERVAL '1 day'). بجمع كل ذلك معًا، ينتج هذا سلسلة من التواريخ تبدأ قبل خمسة أيام، وتنتهي اليوم، وتتقدم يومًا واحدًا في كل مرة. أخيرًا، نزيل جزء الوقت بتحويل مخرجات هذه القيم إلى تاريخ باستخدام ::DATE، ونطلق على هذا العمود اسمًا مستعارًا باستخدام AS day لجعل الإخراج أجمل قليلاً.
مخرجات هذا الاستعلام هي الأيام الخمسة الماضية بالإضافة إلى اليوم:
day
------------
2020-08-19
2020-08-20
2020-08-21
2020-08-22
2020-08-23
2020-08-24
(6 rows)
بالعودة إلى مثال المهام اليومية، دعنا ننشئ جدولًا بسيطًا للاحتفاظ بالمهام التي نريد إكمالها وندرج بعض المهام:
CREATE TABLE tasks(
name TEXT
);
INSERT INTO tasks( name ) VALUES
( 'Brush teeth' ),
( 'Eat breakfast' ),
( 'Shower' ),
( 'Get dressed' );
يحتوي جدول tasks الخاص بنا على عمود واحد فقط، name، وقد أدرجنا أربع مهام في هذا الجدول. الآن دعنا نُجري CROSS JOIN لمهامنا مع الاستعلام لإنشاء التواريخ:
SELECT tasks.name, dates.day
FROM tasks
CROSS JOIN (
SELECT generate_series(
( CURRENT_DATE - INTERVAL '5 day' ),
CURRENT_DATE,
INTERVAL '1 day'
)::DATE AS day
) AS dates;
(نظرًا لأن استعلام إنشاء التواريخ لدينا ليس جدولًا فعليًا، فإننا نكتبه كاستعلام فرعي (subquery) فقط.) من هذا الاستعلام، نعيد اسم المهمة واليوم، وتبدو مجموعة النتائج كالتالي:
name | day
--------------+------------
Brush teeth | 2020-08-19
Brush teeth | 2020-08-20
Brush teeth | 2020-08-21
Brush teeth | 2020-08-22
Brush teeth | 2020-08-23
Brush teeth | 2020-08-24
Eat breakfast| 2020-08-19
Eat breakfast| 2020-08-20
Eat breakfast| 2020-08-21
Eat breakfast| 2020-08-22
...
(24 rows)
كما توقعنا، نحصل على صف لكل مهمة لكل يوم في نطاق التواريخ الخاص بنا. يُعد CROSS JOIN أبسط ربط يمكننا القيام به، ولكن للنظر في الأنواع القليلة التالية، سنحتاج إلى إعداد جدول أكثر واقعية.
إنشاء جداول المخرجين والأفلام
لتوضيح أنواع الربط التالية، سنستخدم مثال الأفلام ومخرجي الأفلام. في هذا السيناريو، الفيلم له مخرج واحد، ولكن ليس مطلوبًا أن يكون للفيلم مخرج – تخيل الإعلان عن فيلم جديد ولكن لم يتم تأكيد اختيار المخرج بعد.
سيقوم جدول directors بتخزين اسم كل مخرج، وسيخزن جدول movies اسم الفيلم بالإضافة إلى مرجع لمخرج الفيلم (إذا كان لديه واحد). دعنا ننشئ هذين الجدولين وندرج بعض البيانات فيهما:
CREATE TABLE directors(
id SERIAL PRIMARY KEY,
name TEXT NOT NULL
);
INSERT INTO directors( name ) VALUES
( 'John Smith' ),
( 'Jane Doe' ),
( 'Xavier Wills' ),
( 'Bev Scott' ),
( 'Bree Jensen' );
CREATE TABLE movies(
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
director_id INTEGER REFERENCES directors
);
INSERT INTO movies( name , director_id) VALUES
( 'Movie 1' , 1 ),
( 'Movie 2' , 1 ),
( 'Movie 3' , 2 ),
( 'Movie 4' , NULL ),
( 'Movie 5' , NULL );
لدينا خمسة مخرجين، وخمسة أفلام، وثلاثة من هذه الأفلام تم تعيين مخرجين لها. المخرج ذو المعرف ID 1 لديه فيلمان، والمخرج ذو المعرف ID 2 لديه فيلم واحد.
الربط الخارجي الكامل (FULL OUTER JOIN)
الآن بعد أن أصبح لدينا بعض البيانات للعمل بها، دعنا نلقي نظرة على FULL OUTER JOIN. يتميز FULL OUTER JOIN ببعض أوجه التشابه مع CROSS JOIN، ولكنه يختلف عنه في نقطتين رئيسيتين.
الاختلاف الأول هو أن FULL OUTER JOIN يتطلب شرط ربط (join condition). يحدد شرط الربط كيفية ارتباط الصفوف بين الجدولين ببعضها البعض وعلى أي معيار يجب ربطها معًا. في مثالنا، يحتوي جدول movies الخاص بنا على مرجع للمخرج عبر عمود director_id، ويتطابق هذا العمود مع عمود id في جدول directors. هذان هما العمودان اللذان سنستخدمهما كشرط للربط.
إليك كيفية كتابة هذا الربط بين جدولينا:
SELECT * FROM movies FULL OUTER JOIN directors ON directors.id = movies.director_id;
لاحظ شرط الربط الذي حددناه والذي يطابق الفيلم بمخرجه: ON movies.director_id = directors.id. تبدو مجموعة النتائج الخاصة بنا كمنتج ديكارتي غريب من نوعه:
id | name | director_id | id | name
------+---------+-------------+------+--------------
1 | Movie 1 | 1 | 1 | John Smith
2 | Movie 2 | 1 | 1 | John Smith
3 | Movie 3 | 2 | 2 | Jane Doe
4 | Movie 4 | NULL | NULL | NULL
5 | Movie 5 | NULL | NULL | NULL
NULL | NULL | NULL | 5 | Bree Jensen
NULL | NULL | NULL | 4 | Bev Scott
NULL | NULL | NULL | 3 | Xavier Wills
(8 rows)
الصفوف الأولى التي نراها هي تلك التي كان للفيلم فيها مخرج، وتم تقييم شرط الربط لدينا على أنه صحيح. ومع ذلك، بعد تلك الصفوف، نرى كل صف من الصفوف المتبقية من كل جدول – ولكن مع قيم NULL حيث لم يكن للجدول الآخر تطابق.
ملاحظة: إذا لم تكن على دراية بقيم NULL، فراجع الشرح هنا في هذا البرنامج التعليمي لمشغلات SQL.
نرى أيضًا اختلافًا آخر بين CROSS JOIN و FULL OUTER JOIN هنا. يعيد FULL OUTER JOIN صفًا مميزًا واحدًا من كل جدول – على عكس CROSS JOIN الذي يحتوي على صفوف متعددة.
الربط الداخلي (INNER JOIN)
النوع التالي من الربط، INNER JOIN، هو أحد أكثر أنواع الربط استخدامًا. يعيد الربط الداخلي فقط الصفوف التي يكون فيها شرط الربط صحيحًا. في مثالنا، سيعيد الربط الداخلي بين جدولي movies و directors السجلات التي تم فيها تعيين مخرج للفيلم فقط.
الصيغة هي نفسها أساسًا كما كانت من قبل:
SELECT * FROM movies INNER JOIN directors ON directors.id = movies.director_id;
تُظهر نتيجتنا الأفلام الثلاثة التي لها مخرج:
id | name | director_id | id | name
----+---------+-------------+----+------------
1 | Movie 1 | 1 | 1 | John Smith
2 | Movie 2 | 1 | 1 | John Smith
3 | Movie 3 | 2 | 2 | Jane Doe
(3 rows)
نظرًا لأن الربط الداخلي يتضمن فقط الصفوف التي تتطابق مع شرط الربط، فإن ترتيب الجدولين في الربط لا يهم. إذا عكسنا ترتيب الجداول في الاستعلام، نحصل على نفس النتيجة:
SELECT * FROM directors INNER JOIN movies ON movies.director_id = directors.id;
id | name | id | name | director_id
----+------------+----+---------+-------------
1 | John Smith | 1 | Movie 1 | 1
1 | John Smith | 2 | Movie 2 | 1
2 | Jane Doe | 3 | Movie 3 | 2
(3 rows)
نظرًا لأننا أدرجنا جدول directors أولاً في هذا الاستعلام واخترنا جميع الأعمدة (SELECT *)، فإننا نرى بيانات عمود directors أولاً ثم الأعمدة من movies – ولكن البيانات الناتجة هي نفسها. هذه خاصية مفيدة للربط الداخلي، لكنها لا تنطبق على جميع أنواع الربط – مثل نوعنا التالي.
الربط الأيسر (LEFT JOIN) / الربط الأيمن (RIGHT JOIN)
يستخدم هذان النوعان التاليان من الربط معدلاً (LEFT أو RIGHT) يؤثر على بيانات أي جدول يتم تضمينها في مجموعة النتائج. ملاحظة: يمكن الإشارة إلى LEFT JOIN و RIGHT JOIN أيضًا باسم LEFT OUTER JOIN و RIGHT OUTER JOIN.
تُستخدم هذه العمليات في الاستعلامات التي نريد فيها إرجاع جميع بيانات جدول معين، وإذا كانت موجودة، بيانات الجدول المرتبط أيضًا. إذا لم تكن البيانات المرتبطة موجودة، فإننا لا نزال نحصل على جميع بيانات الجدول “الأساسي”. إنه استعلام للحصول على معلومات حول شيء معين ومعلومات إضافية إذا كانت تلك المعلومات الإضافية موجودة.
سيكون هذا بسيطًا للفهم بمثال. دعنا نجد جميع الأفلام ومخرجيها، لكننا لا نهتم إذا كان لديهم مخرج أم لا – إنه أمر إضافي:
SELECT * FROM movies LEFT JOIN directors ON directors.id = movies.director_id;
يتبع الاستعلام نفس النمط السابق – لقد حددنا الربط فقط على أنه LEFT JOIN. في هذا المثال، جدول movies هو الجدول “الأيسر”. إذا كتبنا الاستعلام في سطر واحد، فسيجعل هذا الأمر أسهل قليلاً:
... FROM movies LEFT JOIN directors ...
يعيد الربط الأيسر جميع السجلات من الجدول “الأيسر”. يعيد الربط الأيسر أي صفوف من الجدول “الأيمن” تتطابق مع شرط الربط. يتم إرجاع الصفوف من الجدول “الأيمن” التي لا تتطابق مع شرط الربط على أنها NULL.
id | name | director_id | id | name
----+---------+-------------+------+------------
1 | Movie 1 | 1 | 1 | John Smith
2 | Movie 2 | 1 | 1 | John Smith
3 | Movie 3 | 2 | 2 | Jane Doe
4 | Movie 4 | NULL | NULL | NULL
5 | Movie 5 | NULL | NULL | NULL
(5 rows)
بالنظر إلى مجموعة النتائج هذه، يمكننا أن نرى لماذا هذا النوع من الربط مفيد لاستعلامات من نوع “كل هذا، وإذا كان موجودًا، فبعض من ذلك”.
الربط الأيمن (RIGHT JOIN)
يعمل RIGHT JOIN تمامًا مثل LEFT JOIN – باستثناء أن القواعد المتعلقة بالجدولين معكوسة. في الربط الأيمن، يتم إرجاع جميع الصفوف من الجدول “الأيمن”. يتم إرجاع الجدول “الأيسر” بشكل مشروط بناءً على شرط الربط.
دعنا نستخدم نفس الاستعلام أعلاه ولكن نستبدل LEFT JOIN بـ RIGHT JOIN:
SELECT * FROM movies RIGHT JOIN directors ON directors.id = movies.director_id;
id | name | director_id | id | name
------+---------+-------------+----+--------------
1 | Movie 1 | 1 | 1 | John Smith
2 | Movie 2 | 1 | 1 | John Smith
3 | Movie 3 | 2 | 2 | Jane Doe
NULL | NULL | NULL | 5 | Bree Jensen
NULL | NULL | NULL | 4 | Bev Scott
NULL | NULL | NULL | 3 | Xavier Wills
(6 rows)
تعيد مجموعة النتائج الآن كل صف من جدول directors، وإذا كانت موجودة، بيانات movies. كل ما فعلناه هو تبديل الجدول الذي نعتبره “الأساسي” – الجدول الذي نريد رؤية جميع البيانات منه بغض النظر عما إذا كانت بياناته المرتبطة موجودة.
LEFT JOIN / RIGHT JOIN في التطبيقات الإنتاجية
في تطبيق إنتاجي، أستخدم دائمًا LEFT JOIN ولا أستخدم أبدًا RIGHT JOIN. أفعل ذلك لأنه، في رأيي، يجعل LEFT JOIN الاستعلام أسهل في القراءة والفهم. عندما أكتب استعلامات، أحب أن أفكر في البدء بمجموعة نتائج “أساسية”، على سبيل المثال جميع الأفلام، ثم جلب (أو طرح) مجموعات من الأشياء من تلك القاعدة.
نظرًا لأنني أحب البدء بقاعدة، فإن LEFT JOIN يناسب هذا النمط من التفكير. أريد جميع الصفوف من جدولي الأساسي (الجدول “الأيسر”)، وأريد بشكل مشروط الصفوف من الجدول “الأيمن”. في الممارسة العملية، لا أعتقد أنني رأيت أبدًا RIGHT JOIN في تطبيق إنتاجي. لا يوجد خطأ في RIGHT JOIN – أعتقد فقط أنه يجعل الاستعلام أكثر صعوبة في الفهم.
إعادة كتابة RIGHT JOIN
إذا أردنا عكس السيناريو أعلاه وبدلاً من ذلك إرجاع جميع المخرجين وأفلامهم بشكل مشروط، يمكننا بسهولة إعادة كتابة RIGHT JOIN إلى LEFT JOIN. كل ما نحتاجه هو عكس ترتيب الجداول في الاستعلام، وتغيير RIGHT إلى LEFT:
SELECT * FROM directors LEFT JOIN movies ON movies.director_id = directors.id;
ملاحظة: أحب وضع الجدول الذي يتم الربط عليه (الجدول “الأيمن” – في المثال أعلاه movies) أولاً في شرط الربط (ON movies.director_id = ...) – ولكن هذا مجرد تفضيلي الشخصي.
التصفية باستخدام LEFT JOIN
هناك حالتا استخدام لاستخدام LEFT JOIN (أو RIGHT JOIN). لقد غطينا حالة الاستخدام الأولى بالفعل: لإرجاع جميع الصفوف من جدول واحد وبشكل مشروط من جدول آخر.
حالة الاستخدام الثانية هي إرجاع الصفوف من الجدول الأول حيث لا توجد بيانات من الجدول الثاني. سيبدو السيناريو كالتالي: البحث عن المخرجين الذين لا ينتمون إلى فيلم. للقيام بذلك، سنبدأ بـ LEFT JOIN وسيكون جدول directors هو الجدول الأساسي أو “الأيسر”:
SELECT * FROM directors LEFT JOIN movies ON movies.director_id = directors.id;
بالنسبة للمخرج الذي لا ينتمي إلى فيلم، تكون الأعمدة من جدول movies هي NULL:
id | name | id | name | director_id
----+------------+------+---------+-------------
1 | John Smith | 1 | Movie 1 | 1
1 | John Smith | 2 | Movie 2 | 1
2 | Jane Doe | 3 | Movie 3 | 2
5 | Bree Jensen| NULL | NULL | NULL
4 | Bev Scott | NULL | NULL | NULL
3 | Xavier Wills| NULL | NULL | NULL
(6 rows)
في مثالنا، المخرجون ذوو المعرفات 3 و 4 و 5 لا ينتمون إلى فيلم. لتصفية مجموعة النتائج الخاصة بنا لهذه الصفوف فقط، يمكننا إضافة عبارة WHERE لإرجاع الصفوف التي تكون فيها بيانات الفيلم NULL فقط:
SELECT * FROM directors LEFT JOIN movies ON movies.director_id = directors.id WHERE movies.id IS NULL;
id | name | id | name | director_id
----+------------+------+--------+-------------
5 | Bree Jensen| NULL | NULL | NULL
4 | Bev Scott | NULL | NULL | NULL
3 | Xavier Wills| NULL | NULL | NULL
(3 rows)
وهؤلاء هم مخرجونا الثلاثة الذين لا يملكون أفلامًا! من الشائع استخدام عمود id للجدول للتصفية مقابله (WHERE movies.id IS NULL)، ولكن جميع الأعمدة من جدول movies هي NULL – لذا أي منها سيعمل. (نظرًا لأننا نعلم أن جميع الأعمدة من جدول movies ستكون NULL، في الاستعلام أعلاه يمكننا فقط كتابة SELECT directors.* بدلاً من SELECT * لإرجاع جميع معلومات المخرج فقط.)
استخدام LEFT JOIN للعثور على التطابقات
في استعلامنا السابق، وجدنا المخرجين الذين لا ينتمون إلى أفلام. باستخدام نفس الهيكل، يمكننا العثور على المخرجين الذين ينتمون إلى أفلام عن طريق تغيير شرط WHERE للبحث عن الصفوف التي لا تكون فيها بيانات الفيلم NULL:
SELECT * FROM directors LEFT JOIN movies ON movies.director_id = directors.id WHERE movies.id IS NOT NULL;
id | name | id | name | director_id
----+------------+----+---------+-------------
1 | John Smith | 1 | Movie 1 | 1
1 | John Smith | 2 | Movie 2 | 1
2 | Jane Doe | 3 | Movie 3 | 2
(3 rows)
قد يبدو هذا مفيدًا، لكننا في الواقع قمنا للتو بإعادة تطبيق INNER JOIN!
عمليات ربط متعددة
لقد رأينا كيفية ربط جدولين معًا، ولكن ماذا عن عمليات الربط المتعددة على التوالي؟ الأمر بسيط جدًا في الواقع، ولكن لتوضيح ذلك، نحتاج إلى جدول ثالث: tickets. سيمثل هذا الجدول التذاكر المباعة لفيلم:
CREATE TABLE tickets(
id SERIAL PRIMARY KEY,
movie_id INTEGER REFERENCES movies NOT NULL
);
INSERT INTO tickets(movie_id) VALUES
( 1 ),
( 1 ),
( 3 );
يحتوي جدول tickets على عمود id فقط ومرجع للفيلم: movie_id. لقد أدرجنا أيضًا تذكرتين تم بيعهما للفيلم ذي المعرف ID 1، وتذكرة واحدة تم بيعها للفيلم ذي المعرف ID 3.
الآن، دعنا نربط directors بـ movies – ثم movies بـ tickets!
SELECT * FROM directors INNER JOIN movies ON movies.director_id = directors.id INNER JOIN tickets ON tickets.movie_id = movies.id;
نظرًا لأن هذه عمليات ربط داخلية (INNER JOIN)، فإن الترتيب الذي نكتب به عمليات الربط لا يهم. كان بإمكاننا البدء بـ tickets، ثم الربط بـ movies، ثم الربط بـ directors. يعود الأمر مرة أخرى إلى ما تحاول الاستعلام عنه وما يجعل الاستعلام أكثر قابلية للفهم.
في مجموعة النتائج الخاصة بنا، سنلاحظ أننا قمنا بتضييق نطاق الصفوف التي يتم إرجاعها:
id | name | id | name | director_id | id | movie_id
----+------------+----+---------+-------------+----+----------
1 | John Smith | 1 | Movie 1 | 1 | 1 | 1
1 | John Smith | 1 | Movie 1 | 1 | 2 | 1
2 | Jane Doe | 3 | Movie 3 | 2 | 3 | 3
(3 rows)
هذا منطقي لأننا أضفنا INNER JOIN آخر. في الواقع، يضيف هذا شرط “و” (AND) آخر إلى استعلامنا. يقول استعلامنا أساسًا: “أعد جميع المخرجين الذين ينتمون إلى أفلام لديها أيضًا مبيعات تذاكر”.
إذا أردنا بدلاً من ذلك العثور على المخرجين الذين ينتمون إلى أفلام قد لا يكون لديها مبيعات تذاكر بعد، يمكننا استبدال INNER JOIN الأخير بـ LEFT JOIN:
SELECT * FROM directors JOIN movies ON movies.director_id = directors.id LEFT JOIN tickets ON tickets.movie_id = movies.id;
يمكننا أن نرى أن Movie 2 عاد الآن إلى مجموعة النتائج:
id | name | id | name | director_id | id | movie_id
----+------------+----+---------+-------------+------+----------
1 | John Smith | 1 | Movie 1 | 1 | 1 | 1
1 | John Smith | 1 | Movie 1 | 1 | 2 | 1
2 | Jane Doe | 3 | Movie 3 | 2 | 3 | 3
1 | John Smith | 2 | Movie 2 | 1 | NULL | NULL
(4 rows)
لم يكن لهذا الفيلم أي مبيعات تذاكر، لذلك تم استبعاده سابقًا من مجموعة النتائج بسبب INNER JOIN. سأترك هذا كتمرين للقارئ، ولكن كيف يمكنك العثور على المخرجين الذين ينتمون إلى أفلام ليس لديها أي مبيعات تذاكر؟
ترتيب تنفيذ الربط
في النهاية، لا يهمنا حقًا بأي ترتيب يتم تنفيذ عمليات الربط. أحد الاختلافات الرئيسية بين SQL ولغات البرمجة الحديثة الأخرى هو أن SQL هي لغة تصريحية (declarative language). هذا يعني أننا نحدد النتيجة التي نريدها، لكننا لا نحدد تفاصيل التنفيذ – هذه التفاصيل تُترك لمخطط استعلام قاعدة البيانات (database query planner).
نحدد عمليات الربط التي نريدها والشروط عليها، ويتولى مخطط الاستعلام الباقي. ولكن، في الواقع، لا تقوم قاعدة البيانات بربط ثلاثة جداول معًا في نفس الوقت. بدلاً من ذلك، من المرجح أن تربط الجدولين الأولين معًا في نتيجة وسيطة واحدة، ثم تربط تلك المجموعة الوسيطة من النتائج بالجدول الثالث. (ملاحظة: هذا شرح مبسط إلى حد ما.)
لذا، بينما نعمل مع عمليات ربط متعددة في الاستعلامات، يمكننا فقط التفكير فيها كسلسلة من عمليات الربط بين جدولين – على الرغم من أن أحد هذه الجداول يمكن أن يصبح كبيرًا جدًا.
عمليات الربط مع شروط إضافية
الموضوع الأخير الذي سنتناوله هو الربط بشروط إضافية. على غرار عبارة WHERE، يمكننا إضافة العديد من الشروط التي نريدها إلى شروط الربط الخاصة بنا. على سبيل المثال، إذا أردنا العثور على الأفلام التي لها مخرجون ليس اسمهم “John Smith”، يمكننا إضافة هذا الشرط الإضافي إلى ربطنا باستخدام AND:
SELECT * FROM movies INNER JOIN directors ON directors.id = movies.director_id AND directors.name <> 'John Smith';
يمكننا استخدام أي عوامل تشغيل (operators) نضعها في عبارة WHERE في شرط الربط هذا. نحصل أيضًا على نفس النتيجة من هذا الاستعلام إذا وضعنا الشرط في عبارة WHERE بدلاً من ذلك:
SELECT * FROM movies INNER JOIN directors ON directors.id = movies.director_id WHERE directors.name <> 'John Smith';
هناك بعض الاختلافات الدقيقة التي تحدث تحت الغطاء هنا، ولكن لغرض هذا المقال، مجموعة النتائج هي نفسها. (إذا لم تكن على دراية بجميع الطرق التي يمكنك من خلالها تصفية استعلام SQL، فراجع المقال المذكور سابقًا هنا.)
الواقع العملي لكتابة الاستعلامات باستخدام عمليات الربط
في الواقع، أجد نفسي أستخدم عمليات الربط بثلاث طرق مختلفة فقط:
-
الربط الداخلي (
INNER JOIN)حالة الاستخدام الأولى هي السجلات التي توجد فيها العلاقة بين جدولين. يتم تحقيق ذلك بواسطة
INNER JOIN. هذه هي حالات مثل البحث عن “الأفلام التي لها مخرجون” أو “المستخدمين الذين لديهم منشورات”. -
الربط الأيسر (
LEFT JOIN)حالة الاستخدام الثانية هي السجلات من جدول واحد – وإذا كانت العلاقة موجودة – سجلات من جدول ثانٍ. يتم تحقيق ذلك بواسطة
LEFT JOIN. هذه هي حالات مثل “الأفلام مع المخرجين إذا كان لديهم واحد” أو “المستخدمين مع المنشورات إذا كان لديهم بعضها”. -
استبعاد الربط الأيسر (
LEFT JOIN exclusion)حالة الاستخدام الثالثة الأكثر شيوعًا هي حالة الاستخدام الثانية لـ
LEFT JOIN: البحث عن السجلات في جدول واحد لا توجد لها علاقة في الجدول الثاني. هذه هي حالات مثل “الأفلام بدون مخرجين” أو “المستخدمين بدون منشورات”.
نوعان مفيدان جدًا من الربط
لا أعتقد أنني استخدمت FULL OUTER JOIN أو RIGHT JOIN في تطبيق إنتاجي على الإطلاق. حالة الاستخدام لا تظهر غالبًا بما يكفي أو يمكن كتابة الاستعلام بطريقة أوضح (في حالة RIGHT JOIN). لقد استخدمت أحيانًا CROSS JOIN لأشياء مثل توزيع السجلات عبر نطاق زمني (كما نظرنا في البداية)، ولكن هذا السيناريو أيضًا لا يظهر كثيرًا.
لذا، أخبار جيدة! هناك نوعان فقط من عمليات الربط تحتاج حقًا إلى فهمهما لـ 99.9% من حالات الاستخدام التي ستواجهها: INNER JOIN و LEFT JOIN!
إذا أعجبك هذا المنشور، يمكنك متابعتي على تويتر حيث أتحدث عن أمور قواعد البيانات وجميع الموضوعات الأخرى المتعلقة بالتطوير. شكرًا على القراءة!
جون
ملاحظة: نصيحة إضافية لمن قرأ حتى النهاية: ستسمح لك معظم أنظمة قواعد البيانات بكتابة JOIN بدلاً من INNER JOIN – سيوفر عليك ذلك القليل من الكتابة الإضافية. 🙂
الخلاصة التقنية
تُعد عمليات الربط في SQL أدوات لا غنى عنها لأي مطور أو محلل بيانات يتعامل مع قواعد البيانات العلائقية. بينما توفر SQL مجموعة متنوعة من أنواع الربط مثل CROSS JOIN و FULL OUTER JOIN و INNER JOIN و LEFT JOIN و RIGHT JOIN، فإن الفهم العميق لـ INNER JOIN و LEFT JOIN سيكفي لتغطية الغالبية العظمى من السيناريوهات العملية. يبرز INNER JOIN في استرداد السجلات المتطابقة بين الجداول، بينما يتألق LEFT JOIN في استرداد جميع السجلات من الجدول “الأيسر” مع البيانات المطابقة من الجدول “الأيمن”، بالإضافة إلى قدرته الفائقة على تحديد السجلات غير المتطابقة. إن التركيز على هذين النوعين وتبني ممارسات كتابة استعلامات واضحة ومنطقية، مثل تفضيل LEFT JOIN على RIGHT JOIN، يساهم بشكل كبير في بناء أنظمة قواعد بيانات فعالة وسهلة الصيانة.