SQL: الربط والعلاقات
درس 2 من 5
2SQL: الربط والعلاقات (Joins & Relationships)
الصورة الكبيرةكيف تُقسَّم البيانات على عدة جداول مترابطة بمفاتيح، وكيف تعيد جمعها بـ JOIN والاستعلامات الفرعية للإجابة عن أسئلة لا يجيب عنها جدول واحد.
ماذا تشاهد في الرسم: كل صف في orders يحمل رقم العميل، والربط يبحث عن الصف المطابق في customers، فتخرج نتيجة فيها الطلب واسم صاحبه معًا.
خارطة هذا الجزء
المفاهيم بالترتيب، اضغط أيًّا منها للقفز إليه:
طريقان إلى نفس الفكرة
بدل أن تكتب عنوان العميل في كل فاتورة، تحفظ العملاء في دفتر والفواتير في دفتر آخر، وتكتب في الفاتورة رقم العميل فقط. الربط هو أن تضع الدفترين جنبًا إلى جنب وتطابق الأرقام لتجيب عن سؤال مثل «كم أنفق كل عميل هذا الشهر؟».
المفتاح الأساسي PRIMARY KEY يميّز الصف، والمفتاح الأجنبي FOREIGN KEY يشير إلى صف في جدول آخر (وفي SQLite يُفعّل بـPRAGMA foreign_keys = ON). INNER JOIN يعيد المتطابق فقط، وLEFT JOIN يحتفظ بكل صفوف اليسار، والاستعلام الفرعي يضع نتيجة SELECT داخل آخر، ويُدمج الربط مع GROUP BY.
الأساسيات المطلوبة
هذا الجزء يبني مباشرة على ما سبق:
SELECT و WHERE و GROUP BY (درس SQL: عمليات CRUD)
كل استعلام ربط هو SELECT عادي له مصدر أوسع: بدل جدول واحد في FROM يوجد جدول ناتج عن الربط. لذلك تبقى WHERE و GROUP BY و HAVING و ORDER BY كما هي، وتعمل على الصفوف الناتجة عن الربط.
الحلقات المتداخلة
تصوّر الربط كحلقتين متداخلتين: لكل صف من الجدول الأول تفتّش الجدول الثاني عن الصفوف التي يتحقق معها شرط ON. المحرّك يستخدم الفهارس ليتجنب تفتيش كل شيء، لكن «المعنى» هو معنى الحلقتين، وهذا يفسّر لماذا يكون الربط بلا شرط ضرب ديكارتي.
الدالة التجميعية و NULL
نتيجة الربط الخارجي تحتوي NULL في أعمدة الجدول الذي لم يُطابَق، وسلوك COUNT(*) مقابل COUNT(col) مع NULL (درس SQL: عمليات CRUD) هو ما يجعل أعدادك صحيحة أو خاطئة هنا.
المفاهيم الأساسية
الفكرة المركزية: لا نكرّر المعلومة في أكثر من مكان. اسم الزبون يُكتب مرة واحدة في جدول الزبائن، وكل طلب يكتفي بالإشارة إليه برقمه. الربط هو الطريقة التي نعيد بها جمع المعلومة عند القراءة.
1المفاتيح والعلاقات
المفتاح الأساسي (Primary Key)
عمود (أو أكثر) يعرّف كل صف بتفرّد ولا يقبل NULL. مثل customers.id. يُسمّى الجدول الذي يملكه الجدول الأب (parent) حين تشير إليه جداول أخرى.
المفتاح الأجنبي (Foreign Key)
عمود في الجدول الابن (child) يحمل قيمة مفتاح أساسي من الأب: orders.customer_id REFERENCES customers(id). وجوده يجعل قاعدة البيانات تضمن ألّا يشير طلب إلى زبون غير موجود.
1️⃣ ↔ 1️⃣ واحد إلى واحد
زبون له بطاقة ولاء واحدة على الأكثر. يُنفَّذ بجعل مفتاح الجدول الابن هو نفسه مفتاحًا أجنبيًا: loyalty_cards.customer_id PRIMARY KEY REFERENCES customers(id).
1️⃣ ↔ ♾️ واحد إلى متعدد
زبون له طلبات كثيرة، وكل طلب لزبون واحد. أبسط وأشيع علاقة: يوضع المفتاح الأجنبي في جانب «المتعدد» (orders.customer_id).
↔ ♾️ متعدد إلى متعدد
الطلب فيه مشروبات كثيرة والمشروب في طلبات كثيرة. لا يمكن تمثيلها بعمود واحد، فنضيف جدولًا وسيطًا (junction table) هو order_items بمفتاح أجنبي لكل جانب، ومفتاحه الأساسي مركّب (order_id, drink_id)، وفيه أيضًا الكمية.
104 لضيف بلا حساب، فعموده customer_id يساوي NULL. هذا مسموح ومنطقي: «لا زبون». وهو يفسّر لماذا يحذف الربط الداخلي هذا الطلب من النتيجة بينما يحتفظ به الربط الخارجي.
2سلامة العلاقات في SQLite
تعريف REFERENCES وحده لا يكفي في SQLite: فحص المفاتيح الأجنبية معطّل افتراضيًا لكل اتصال جديد. تفعّله بالأمر PRAGMA foreign_keys = ON; في بداية كل اتصال، ولا يمكن تغييره داخل معاملة (transaction) جارية. بعد تفعيله، يرفض المحرّك إدراج طلب لزبون غير موجود، ويرفض حذف أب له أبناء ما لم يُحدَّد سلوك مختلف بـ ON DELETE.
إجراء ON DELETE | ماذا يحدث للصفوف التابعة عند حذف الأب |
|---|---|
NO ACTION (الافتراضي) | يرفض الحذف أو التعديل إن كانت هناك صفوف تابعة تشير إليه. |
RESTRICT | مثل السابق لكنه يرفض فورًا قبل أي تعديل آخر. |
SET NULL | يضع NULL في مفتاح الصفوف التابعة (يتطلب أن يقبل العمود NULL). |
SET DEFAULT | يضع القيمة الافتراضية للعمود في الصفوف التابعة. |
CASCADE | يحذف الصفوف التابعة تلقائيًا (أو يعدّل مفتاحها) تبعًا لتغيّر الأب. |
PRAGMA foreign_keys = ON: تعرّف علاقات كاملة ثم تكتشف أن الحذف والإدراج يمرّان بلا أي اعتراض وأن في جدولك صفوفًا «يتيمة» تشير إلى أب غير موجود. المفاتيح الأجنبية بلا هذا الأمر مجرد توثيق.
3أنواع الربط
| النوع | ماذا يرجع | مثال في المقهى |
|---|---|---|
INNER JOIN | الصفوف التي وُجد لها مقابل في الجدولين فقط. | الطلبات التي لها زبون مسجّل |
LEFT JOIN | كل صفوف الجدول الأيسر، ومعها NULL حيث لا مقابل. | كل الزبائن، حتى من لم يطلب |
CROSS JOIN | كل تركيبة ممكنة (الضرب الديكارتي): m × n صفًا. | كل زبون مع كل مشروب |
RIGHT JOIN / FULL JOIN | الأيمن / كلا الجانبين، متاحان في SQLite منذ 3.39.0 فقط. | بدائل: اقلب ترتيب الجدولين مع LEFT JOIN |
RIGHT JOIN و FULL JOIN في الإصدار 3.39.0 (يونيو 2022). الإصدارات الأقدم ترفضهما بخطأ، وقد تكون بيئتك منها. الأمان: اكتب كل شيء بـ INNER و LEFT فقط؛ فالأيمن هو أيسر بعد تبديل مكان الجدولين، والكامل هو LEFT JOIN مضافًا إليه الصفوف غير المطابقة من الجانب الآخر بـ UNION ALL.
4تفاصيل كتابة الربط
الأسماء المستعارة (Aliases)
FROM orders o JOIN customers c يختصر الكتابة، وهو إلزامي حين تربط الجدول بنفسه. وعند وجود عمودين بنفس الاسم (id في الجدولين) يجب أن تقول أيّهما تقصد: c.id أو o.id، وإلا ظهر خطأ ambiguous column name.
الربط الذاتي (Self join)
ربط الجدول بنسخة منه باسمين مستعارين، لمقارنة صفوفه ببعضها: أزواج الزبائن من المدينة نفسها a.city = b.city AND a.id < b.id (الشرط الثاني يمنع تكرار الزوج وربط الصف بنفسه).
USING و NATURAL
JOIN … USING (col) اختصار حين يكون اسم العمود واحدًا في الجدولين. أما NATURAL JOIN فيربط على كل الأعمدة المتشابهة الاسم تلقائيًا، فتتغيّر نتيجته إن أضفت عمودًا لاحقًا؛ الأفضل تجنّبه وكتابة ON صريحة.
LEFT JOIN شرط ON يقرّر أيّ الصفوف تُطابَق، أما WHERE فيعمل بعد الربط ويحذف صفوفًا من الناتج. وضع شرط على الجدول الأيمن في WHERE يحذف الصفوف ذات NULL فيه فيتحوّل الربط الخارجي عمليًا إلى ربط داخلي. القاعدة: شروط تحديد المطابقة في ON، وشروط تصفية الناتج النهائي في WHERE.
FROM orders, customers بلا شرط (أو JOIN بلا ON) ينتج كل تركيبة ممكنة: 5 طلبات × 4 زبائن = 20 صفًّا، أغلبها كاذب. علامة التحذير: عدد الصفوف أكبر بكثير مما توقعت.
5الاستعلامات الفرعية
الاستعلام الفرعي هو SELECT داخل SELECT بين قوسين. غالبًا يمكن كتابة السؤال نفسه بـ JOIN أو باستعلام فرعي؛ اختر الأوضح قراءة.
| الشكل | المعنى | مثال |
|---|---|---|
| قيمة مفردة (Scalar) | استعلام يرجع صفًّا واحدًا وعمودًا واحدًا فيُستعمل كأنه قيمة. | WHERE price_halalas > (SELECT AVG(price_halalas) FROM drinks) |
IN (…) | يرجع قائمة قيم؛ الشرط صحيح إن كانت القيمة ضمنها. | WHERE id IN (SELECT customer_id FROM orders) |
EXISTS (…) | صحيح إن أرجع الاستعلام صفًّا واحدًا على الأقل، ولا تهمّ محتوياته. | WHERE EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id) |
| مترابط (Correlated) | يشير إلى عمود من الاستعلام الخارجي، فيُعاد حسابه لكل صف خارجي. | … > (SELECT AVG(price_halalas) FROM drinks WHERE category = d.category) |
| جدول مشتق (Derived) | استعلام فرعي في FROM له اسم مستعار يُعامَل كجدول. | FROM (SELECT … GROUP BY …) AS t |
NOT IN (…) قيمة NULL واحدة فلن يتحقق الشرط لأي صف، وتحصل على نتيجة فارغة دون أي خطأ. وهذا ما يحدث تمامًا مع orders.customer_id لأن فيه طلب الضيف. الحل الأسلم: NOT EXISTS أو ربط خارجي مع IS NULL.
6الربط مع التجميع
بعد الربط تصبح الصفوف «أوسع» وقد تتضاعف: ربط طلب بأصنافه يعطي صفًّا لكل صنف، فعدّ الطلبات بعد هذا الربط يعدّ الطلب الواحد أكثر من مرة. استخدم COUNT(DISTINCT o.id). وبعد LEFT JOIN يعدّ COUNT(*) الصف غير المطابق صفًّا واحدًا، بينما يعطي COUNT(o.id) صفرًا لأن العمود NULL، فاستعمل الثاني لعدّ الأبناء.
كيف يعمل الربط فعليًا؟
خريطة مقهانا
الرسم 23: مخطط علاقات (ER) للمقهى: الخط يربط الأب بالابن، والرمز عند الطرف يبيّن العدد (صفر أو واحد، واحد فقط، صفر أو أكثر).
أي ربط أختار؟
الرسم 24: شجرة قرار لاختيار نوع الربط، وطريقة الوصول إلى الصفوف «اليتيمة» أو غير المطابقة.
ماذا يفعل المحرّك مع كل صف؟
الرسم 25: الربط يسبق WHERE منطقيًا؛ لهذا قد يلغي شرطُ WHERE صفوفَ NULL التي أنتجها LEFT JOIN.
مثال تطبيقي: مقهى بخمسة جداول
نبني قاعدة المقهى كاملة ثم نسألها أسئلة تحتاج أكثر من جدول. الجداول: customers و drinks و orders و order_items (الجدول الوسيط) و loyalty_cards (واحد إلى واحد).
الخطوة 1: المخطط
PRAGMA foreign_keys = ON; -- per connection!
CREATE TABLE customers (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
city TEXT NOT NULL
);
CREATE TABLE drinks (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
category TEXT NOT NULL CHECK (category IN ('hot', 'cold')),
price_halalas INTEGER NOT NULL CHECK (price_halalas > 0)
);
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
customer_id INTEGER REFERENCES customers(id), -- NULL = guest
order_date TEXT NOT NULL
);
CREATE TABLE order_items (
order_id INTEGER NOT NULL REFERENCES orders(id),
drink_id INTEGER NOT NULL REFERENCES drinks(id),
qty INTEGER NOT NULL CHECK (qty > 0),
PRIMARY KEY (order_id, drink_id)
);
CREATE TABLE loyalty_cards (
customer_id INTEGER PRIMARY KEY REFERENCES customers(id),
points INTEGER NOT NULL DEFAULT 0 CHECK (points >= 0)
);
البيانات: أربعة زبائن (نورة ولينا من الرياض، وسلطان من جدة، وفيصل من الدمام)، خمسة مشروبات، خمسة طلبات (أحدها 104 لضيف بلا حساب)، وبطاقتا ولاء لنورة ولينا فقط.
الخطوة 2: الربط الداخلي
SELECT o.id AS order_id, c.name, o.order_date FROM orders o INNER JOIN customers c ON c.id = o.customer_id ORDER BY o.id;
| order_id | name | order_date |
|---|---|---|
| 101 | Noura | 2026-10-01 |
| 102 | Noura | 2026-10-03 |
| 103 | Sultan | 2026-10-03 |
| 105 | Lina | 2026-10-05 |
أربعة طلبات فقط من خمسة: طلب الضيف 104 اختفى لأنه لا مقابل له في جدول الزبائن.
الخطوة 3: الربط الخارجي والصفوف غير المطابقة
SELECT c.name, o.id AS order_id FROM customers c LEFT JOIN orders o ON o.customer_id = c.id ORDER BY c.id, o.id;
| name | order_id |
|---|---|
| Noura | 101 |
| Noura | 102 |
| Sultan | 103 |
| Lina | 105 |
| Faisal | NULL |
ظهر فيصل مع NULL لأنه لم يطلب شيئًا. ولعزل هؤلاء فقط نضيف WHERE o.id IS NULL (يُسمّى هذا النمط «الربط المضاد» Anti-join):
SELECT c.name FROM customers c LEFT JOIN orders o ON o.customer_id = c.id WHERE o.id IS NULL;
| name |
|---|
| Faisal |
الخطوة 4: الفرق بين ON و WHERE
نريد كل الزبائن مع طلباتهم في 3 أكتوبر إن وُجدت. الشرط في ON:
SELECT c.name, o.id AS order_id
FROM customers c
LEFT JOIN orders o
ON o.customer_id = c.id
AND o.order_date = '2026-10-03'
ORDER BY c.id;
| name | order_id |
|---|---|
| Noura | 102 |
| Sultan | 103 |
| Lina | NULL |
| Faisal | NULL |
ولو نقلنا الشرط نفسه إلى WHERE:
SELECT c.name, o.id AS order_id FROM customers c LEFT JOIN orders o ON o.customer_id = c.id WHERE o.order_date = '2026-10-03' ORDER BY c.id;
| name | order_id |
|---|---|
| Noura | 102 |
| Sultan | 103 |
اختفى لينا وفيصل لأن WHERE حذف صفوف NULL: انقلب الربط الخارجي إلى داخلي عمليًا.
الخطوة 5: ربط ثلاثة جداول أو أكثر عبر الجدول الوسيط
SELECT o.id AS order_id,
COALESCE(c.name, '(guest)') AS customer,
d.name AS drink,
oi.qty,
oi.qty * d.price_halalas AS line_total
FROM orders o
LEFT JOIN customers c ON c.id = o.customer_id
JOIN order_items oi ON oi.order_id = o.id
JOIN drinks d ON d.id = oi.drink_id
ORDER BY o.id, d.id;
| order_id | customer | drink | qty | line_total |
|---|---|---|---|---|
| 101 | Noura | Flat White | 2 | 3200 |
| 101 | Noura | Cortado | 1 | 1400 |
| 102 | Noura | Cold Brew | 1 | 1800 |
| 103 | Sultan | Spanish Latte | 2 | 3900 |
| 104 | (guest) | Flat White | 1 | 1600 |
| 105 | Lina | Cold Brew | 3 | 5400 |
| 105 | Lina | Spanish Latte | 1 | 1950 |
لاحظ أن LEFT JOIN على الزبائن أبقى طلب الضيف (104) بينما ربطُ الأصناف والمشروبات بـ JOIN العادي سليم لأن لكل صنف طلبًا ومشروبًا دائمًا. تقرأ الربط من اليسار إلى اليمين: كل JOIN يضيف جدولًا إلى الناتج السابق.
الخطوة 6: الربط مع التجميع
SELECT c.name,
COUNT(DISTINCT o.id) AS orders,
COALESCE(SUM(oi.qty * d.price_halalas), 0) AS spent
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
LEFT JOIN order_items oi ON oi.order_id = o.id
LEFT JOIN drinks d ON d.id = oi.drink_id
GROUP BY c.id
ORDER BY spent DESC, c.id;
| name | orders | spent |
|---|---|---|
| Lina | 1 | 7350 |
| Noura | 2 | 6400 |
| Sultan | 1 | 3900 |
| Faisal | 0 | 0 |
COUNT(DISTINCT o.id) يمنع عدّ الطلب مرتين حين يحوي صنفين (طلب نورة 101 له صفّان بعد الربط). وبلا COALESCE كان مجموع فيصل NULL لا 0.
SELECT c.name,
COUNT(*) AS count_star,
COUNT(o.id) AS count_orders
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
GROUP BY c.id
ORDER BY c.id;
| name | count_star | count_orders |
|---|---|---|
| Noura | 2 | 2 |
| Sultan | 1 | 1 |
| Lina | 1 | 1 |
| Faisal | 1 | 0 |
عند فيصل COUNT(*) يعطي 1 (الصف غير المطابق) و COUNT(o.id) يعطي 0، وهذا هو الصحيح لعدد الطلبات.
الخطوة 7: واحد إلى واحد، وربط الجدول بنفسه
SELECT c.name, COALESCE(l.points, 0) AS points FROM customers c LEFT JOIN loyalty_cards l ON l.customer_id = c.id ORDER BY c.id;
| name | points |
|---|---|
| Noura | 120 |
| Sultan | 0 |
| Lina | 40 |
| Faisal | 0 |
SELECT a.name AS first, b.name AS second, a.city FROM customers a JOIN customers b ON a.city = b.city AND a.id < b.id;
| first | second | city |
|---|---|---|
| Noura | Lina | Riyadh |
الخطوة 8: خطأ الضرب الديكارتي
SELECT COUNT(*) AS rows_returned FROM customers CROSS JOIN drinks;
| rows_returned |
|---|
| 20 |
4 زبائن × 5 مشروبات = 20 صفًّا من تركيبات لا تعني شيئًا. هذا هو ما تحصل عليه حين تنسى ON.
الخطوة 9: الاستعلامات الفرعية
IN: الزبائن الذين طلبوا مشروبًا باردًا يومًا ما.
SELECT name
FROM customers
WHERE id IN (SELECT o.customer_id
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
JOIN drinks d ON d.id = oi.drink_id
WHERE d.category = 'cold')
ORDER BY id;
| name |
|---|
| Noura |
| Sultan |
| Lina |
استعلام مترابط: المشروبات الأغلى من متوسط فئتها (متوسط الساخن 1500 والبارد 1950).
SELECT d.name, d.category, d.price_halalas
FROM drinks d
WHERE d.price_halalas > (SELECT AVG(price_halalas)
FROM drinks
WHERE category = d.category)
ORDER BY d.id;
| name | category | price_halalas |
|---|---|---|
| Flat White | hot | 1600 |
| Matcha | cold | 2100 |
وهنا فخ NOT IN. المقصود: الزبائن الذين لم يطلبوا شيئًا.
SELECT name FROM customers WHERE id NOT IN (SELECT customer_id FROM orders);
| name |
|---|
نتيجة فارغة! لأن orders.customer_id يحوي NULL (الضيف). بينما:
SELECT name FROM customers c WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id);
| name |
|---|
| Faisal |
أما حين يكون العمود NOT NULL كما في order_items.drink_id فـ NOT IN آمن:
SELECT name FROM drinks WHERE id NOT IN (SELECT drink_id FROM order_items);
| name |
|---|
| Matcha |
الخطوة 10: التعويض عن RIGHT و FULL
«كل الطلبات مع اسم الزبون إن وُجد» ربط أيمن على الزبائن؛ نكتبه بقلب ترتيب الجدولين:
SELECT o.id AS order_id, c.name FROM orders o LEFT JOIN customers c ON c.id = o.customer_id ORDER BY o.id;
| order_id | name |
|---|---|
| 101 | Noura |
| 102 | Noura |
| 103 | Sultan |
| 104 | NULL |
| 105 | Lina |
و«كل الزبائن وكل الطلبات» ربط كامل، نبنيه من LEFT JOIN ثم نضيف الطلبات اليتيمة:
SELECT c.name, o.id AS order_id FROM customers c LEFT JOIN orders o ON o.customer_id = c.id UNION ALL SELECT NULL, o.id FROM orders o WHERE o.customer_id IS NULL;
| name | order_id |
|---|---|
| Noura | 101 |
| Noura | 102 |
| Sultan | 103 |
| Lina | 105 |
| Faisal | NULL |
| NULL | 104 |
الخطوة 11: حماية المفاتيح الأجنبية
| المحاولة (بعد تفعيل PRAGMA) | النتيجة |
|---|---|
INSERT INTO orders VALUES (106, 99, '2026-10-06') | FOREIGN KEY constraint failed |
DELETE FROM customers WHERE id = 1 | FOREIGN KEY constraint failed |
الأول لأن الزبون 99 غير موجود، والثاني لأن لنورة طلبات تابعة، والسلوك الافتراضي NO ACTION يرفض.
الخطوة 12: من بايثون
import sqlite3
conn = sqlite3.connect("cafe.db")
conn.execute("PRAGMA foreign_keys = ON") # every new connection
sql = """
SELECT c.name, COUNT(o.id) AS orders
FROM customers c
LEFT JOIN orders o ON o.customer_id = c.id
WHERE c.city = ?
GROUP BY c.id
"""
for name, n in conn.execute(sql, ("Riyadh",)):
print(name, n)
conn.close()
PRAGMA يُنفَّذ على كل اتصال جديد، والقيمة تُمرَّر بالعلامة ? لا بدمج النص.
خرافة مقابل حقيقة
| الخرافة | الحقيقة |
|---|---|
JOIN وحدها تعني ربطًا داخليًا وخارجيًا معًا | JOIN بلا صفة هي INNER JOIN. الربط الخارجي يجب أن تكتبه صراحة: LEFT JOIN. |
تعريف FOREIGN KEY يحمي بياناتي تلقائيًا في SQLite | الفحص معطّل افتراضيًا؛ يلزم PRAGMA foreign_keys = ON في كل اتصال. |
الشرط في WHERE والشرط في ON سواء دائمًا | متكافئان في الربط الداخلي فقط. في LEFT JOIN يختلفان لأن WHERE يحذف صفوف NULL بعد الربط. |
NOT IN هو دائمًا الطريقة الأبسط لإيجاد «من لا يملك» | يفشل بصمت (نتيجة فارغة) إن وُجدت NULL في القائمة. NOT EXISTS أو الربط المضاد أسلم. |
الاستعلام الفرعي أبطأ دائمًا من JOIN | ليس قاعدة؛ المحسّن يعيد كتابة كثير منها، والأداء يتوقف على الفهارس والبيانات. اختر الأوضح ثم قِس إن لزم. |
أين تُستخدم هذه الأدوات فعليًا؟
الفواتير والتقارير
كل فاتورة تجمع الطلب والزبون والأصناف والأسعار من أربعة جداول بسلسلة JOIN.
تحليل العملاء
«من لم يشترِ منذ شهر؟» ربط مضاد، و«أكثر الزبائن إنفاقًا» ربط مع SUM و GROUP BY.
سلامة البيانات
المفاتيح الأجنبية وإجراءات ON DELETE تمنع الطلبات اليتيمة وتحدد ماذا يحدث عند حذف زبون.
أدوات الـ ORM
مكتبات ربط الكائنات بالجداول (الخطوة التالية في المسار) تترجم العلاقات بين الأصناف إلى مفاتيح أجنبية وربط، ففهم ما تحتها يجعلها شفافة.
جرّب بنفسك
تريد كل العملاء حتى من لم يطلب شيئًا. أي ربط؟
LEFT JOIN يُبقي كل صفوف الجدول الأيسر، وهنا العملاء، ويضع NULL حيث لا طلب.
بطاقات مراجعة سريعة
LEFT JOIN … WHERE right.col = x صفوفًا أقل مما توقعت؟WHERE يحذف صفوف NULL التي أنتجها الربط، فيتحول إلى ربط داخلي. انقل الشرط إلى ON.m × n (الضرب الديكارتي).PRAGMA foreign_keys = ON; على كل اتصال جديد.NOT EXISTS أو LEFT JOIN … WHERE o.id IS NULL.اختبر فهمك
1. (استرجاع) ما الفرق بين INNER JOIN و LEFT JOIN في نتيجة طلب الضيف (104)؟
الداخلي يستبعد الطلب لأنه بلا زبون مقابل. أما LEFT JOIN من الطلبات إلى الزبائن فيُبقيه مع NULL في أعمدة الزبون.
2. (لماذا) لماذا نعطي كل جدول اسمًا مستعارًا حتى لو لم نكن نربطه بنفسه؟
لأن الأعمدة المتشابهة (id مثلًا) تحتاج توضيح مصدرها وإلا ظهر ambiguous column name، وللاختصار وسهولة القراءة.
3. (اكتشف الخطأ) SELECT c.name, COUNT(*) FROM customers c LEFT JOIN orders o ON o.customer_id = c.id GROUP BY c.id; يقول إن فيصل لديه طلب واحد. ما الخطأ؟
COUNT(*) يعدّ الصف غير المطابق الذي أنتجه الربط الخارجي. الصحيح COUNT(o.id) فيعطي 0.
4. (اكتشف الخطأ) لماذا تُرجع SELECT name FROM customers WHERE id NOT IN (SELECT customer_id FROM orders) صفوفًا صفرًا رغم أن فيصل لم يطلب؟ وكيف تصلحها؟
لأن قائمة الـ IN تحوي NULL (طلب الضيف) فلا يتحقق NOT IN لأي صف. أصلحها بـ NOT EXISTS أو بـ WHERE customer_id IS NOT NULL داخل الاستعلام الفرعي.
5. (توقّع) كم صفًّا يرجع SELECT * FROM orders CROSS JOIN drinks؟
25 = 5 طلبات × 5 مشروبات.
مصادر هذا الدرس
مرتّبة بالنوع. ابدأ بالتوثيق الرسمي إذا تبي المرجع الدقيق، وبالمقال أو الفيديو إذا تبي الشرح أولًا.
خلّصت الدرس؟
سجّل الدخول ليُحفظ تقدّمك.