الوحدة 2 · التجميع والـ JOIN
الـ INNER JOIN والـ LEFT JOIN
17 دقراءة 4 دتمرين SQL
الدرس في لمحة
اربط الجداول بـ INNER JOIN وLEFT JOIN على المفاتيح الصحيحة، وحدّد الجدول لكل عمود يتكرّر اسمه، واحتفظ بالصفوف غير المطابقة بوضع شروط الجدول الأيمن في عبارة ON.
- 1تتبّع المفاتيح
- 2INNER مقابل LEFT
- 3ON مقابل WHERE في الـ LEFT JOIN
- 4اختيار الـ JOIN، والجدول الذي تبدأ منه
لا فيديو هنا، بل الشيء الحقيقي: درس مكتوب وأمثلة قابلة للتشغيل واختبار وتدريب.
يسأل كريم من المالية: «كم كان إيرادنا في 2025 حسب فئة المنتج؟». الإيراد موجود في order_items، والسنة في orders، واسم الفئة في categories، على بُعد خطوتين عبر products. لا يوجد جدول واحد يجيب عن السؤال. الـ JOIN يجمعها معًا، وضبطه يدور في معظمه حول معرفة الصفوف التي يُبقيها كل JOIN.
تتبّع المفاتيح
كل JOIN يحتاج إلى شرط يقول أيّ الصفوف تنتمي إلى بعضها، وهو في الغالب مفتاح: order_items.order_id يشير إلى orders.order_id، وorder_items.product_id إلى products.product_id، وهكذا. في Cartwheel يُحسب الإيراد من كل الطلبات ما عدا الملغاة.
SELECT
c.name AS category,
COUNT(DISTINCT o.order_id) AS orders,
ROUND(SUM(oi.quantity * oi.unit_price * (1 - oi.discount)), 2) AS revenue
FROM orders AS o
JOIN order_items AS oi ON oi.order_id = o.order_id
JOIN products AS p ON p.product_id = oi.product_id
JOIN categories AS c ON c.category_id = p.category_id
WHERE o.status <> 'cancelled'
AND o.order_date >= '2025-01-01'
AND o.order_date < '2026-01-01'
GROUP BY c.name
ORDER BY revenue DESC;تتصدّر Desks & Chairs بنحو 77,000. لكن صفّين يبدوان غريبين: “Kitchen” و"Outdoor" فئتان في المستوى الأعلى، ويُفترض أن تقع المنتجات في فئات فرعية. احتفظ بهذه الملاحظة؛ إنها اكتشاف في جودة البيانات ستتعقّبه في القسم 4.
عادتان تجعلان الـ queries التي فيها JOIN مقروءة وآمنة. الأسماء المستعارة القصيرة (o، oi، p، c) تُبقي الأسطر قصيرة. ونسبة كل عمود إلى اسمه المستعار تزيل التخمين، وهذا مهم هنا: order_items.unit_price هو السعر المدفوع يوم الطلب، وproducts.unit_price هو سعر القائمة اليوم. كتابة unit_price بلا نسبة في هذا الـ query تعطي خطأ “ambiguous column name”، واختيار العمود الخطأ يغيّر الإيراد بصمت.
وCOUNT(DISTINCT o.order_id) مقصود أيضًا. بعد ربط العناصر، يظهر كل طلب مرة لكل سطر عنصر، فيعدّ COUNT(*) السطور. وهذا التكرار هو موضوع الدرس التالي.
INNER مقابل LEFT
JOIN تعني INNER JOIN: لا يبقى الصف إلا إذا وجد تطابقًا في الجانب الآخر. هذا صحيح للإيراد، حيث لا معنى لعنصر بلا طلب. لكنه خاطئ حين يكون السؤال عن كل ما في جانب واحد، بما في ذلك ما ليس له تطابق.
تطلب جوليا عدد التذاكر لكل موظف في العمليات، «بما في ذلك من لا يتولّون التذاكر، حتى أرى من يمكنه المساعدة في موسم الذروة»:
SELECT
e.employee_id,
e.first_name,
e.title,
COUNT(t.ticket_id) AS tickets
FROM employees AS e
LEFT JOIN support_tickets AS t
ON t.agent_id = e.employee_id
WHERE e.department = 'Operations'
GROUP BY e.employee_id
ORDER BY tickets DESC;ثمانية صفوف. لدى كل واحد من الوكلاء الأربعة نحو 200 تذكرة؛ ولدى مارك وسمير وبن وآدم صفر. يُبقي الـ LEFT JOIN كل صف من الجدول الأيسر، وهو هنا employees، وحيث لا تطابقه أي تذكرة تكون أعمدة التذاكر NULL. يتخطّى COUNT(t.ticket_id) هذه القيم، ولهذا يظهر لغير الوكلاء 0 لا 1.
ON مقابل WHERE في الـ LEFT JOIN
الآن تضيّق جوليا الطلب: «القائمة نفسها، لكن اعدد التذاكر العاجلة فقط». التعديل البديهي يضيف AND t.priority = 'urgent' إلى عبارة WHERE، فيختفي غير الوكلاء الأربعة. صفوفهم فيها t.priority قيمته NULL، فيكون الشرط NULL، ويحذفهم WHERE. الشرط على الجدول الأيمن في WHERE يعيد الـ LEFT JOIN إلى INNER JOIN.
مكان هذا الشرط هو عبارة ON، حيث يحدّد أي التذاكر تتطابق، لا أي الصفوف تبقى:
SELECT
e.first_name,
COUNT(t.ticket_id) AS urgent_tickets
FROM employees AS e
LEFT JOIN support_tickets AS t
ON t.agent_id = e.employee_id
AND t.priority = 'urgent'
WHERE e.department = 'Operations'
GROUP BY e.employee_id
ORDER BY urgent_tickets DESC;ثمانية صفوف من جديد، والأصفار في أماكنها. القاعدة: الشروط على الجدول الأيسر في WHERE؛ والشروط على الجدول الأيمن في ON.
اختيار الـ JOIN، والجدول الذي تبدأ منه
طريقة موثوقة للقرار: ابدأ FROM بالجدول الذي تريد صفوفه في الإجابة، أي الجدول الذي يطابق مستوى التفصيل الذي كتبته. ثم اسأل عن كل جدول آخر: «هل هذه المعلومة إلزامية أم اختيارية؟». المعلومة الإلزامية تأخذ inner join، لأن الصف الذي يفتقدها لا مكان له في الإجابة. والمعلومة الاختيارية تأخذ LEFT JOIN، حتى تظهر التطابقات المفقودة كقيم NULL يمكنك عدّها أو تسميتها أو تحويلها إلى أصفار.
في قائمة جوليا تدور الإجابة حول الموظفين، فيأتي employees أولًا، والتذاكر اختيارية. وفي إيراد كريم تدور الإجابة حول المبيعات، فكل جدول في السلسلة إلزامي.
يدعم SQLite أيضًا RIGHT JOIN وFULL OUTER JOIN (منذ الإصدار 3.39)، وPostgreSQL يدعمهما منذ عقود. نادرًا ما ستحتاج إليهما: الـ right join هو left join مع تبديل الجدولين، ومعظم الفرق تكتب كل شيء بـ LEFT JOIN حتى تُقرأ الـ queries بالطريقة نفسها، من الأعلى إلى الأسفل، من الجدول الرئيسي نحو الخارج.
أخيرًا، افحص عدد الصفوف بعد كل JOIN تضيفه. إذا كان في employees بعد تصفيته على العمليات 8 صفوف، فيجب أن يعيد الـ LEFT JOIN مع GROUP BY ثمانية أيضًا. الرقم الذي يكبر أو يصغر على غير المتوقع يخبرك أن JOIN ما يفعل شيئًا لم تقصده.
كان في إيراد الفئات فخ آخر لم يُصبنا، لأن الإيراد كان في الجدول الأكثر تفصيلًا. حين يقع الرقم في الجانب الأقل تفصيلًا من الـ JOIN، تتضخّم المجاميع. وهذا هو موضوعنا التالي.
أهم النقاط
- الـ INNER JOIN لا يُبقي إلا الصفوف المتطابقة في الجانبين؛ والـ LEFT JOIN يُبقي كل صف من الجدول الأيسر ويملأ أعمدة الجانب الأيمن غير المطابقة بـ NULL.
- أعطِ كل جدول اسمًا مستعارًا قصيرًا وانسب إليه كل عمود، خاصةً الأسماء الموجودة في أكثر من جدول مثل unit_price.
- الشرط على الجدول الأيمن في LEFT JOIN مكانه عبارة ON؛ أما في WHERE فيحذف صفوف NULL ويعيد الـ JOIN إلى inner join.
- بعد LEFT JOIN اعدد عمودًا من الجدول الأيمن لا *، حتى تُحسب الصفوف غير المطابقة 0 لا 1.