الاستعلامات الفرعية والمشروع الختامي
أسئلة مركّبة باستعلام داخل استعلام، ثم مشروع يجمع المقرر كله. ⏱ نحو 35 دقيقة.
- تكتب استعلامًا فرعيًّا قياسيًّا يعيد قيمة واحدة.
- تستخدم
INوNOT INوEXISTSمع الاستعلامات الفرعية، وتتجنب فخNULL. - تكتب استعلامًا مرتبطًا وجدولًا مشتقًّا وتعبير
WITH. - تختار بين الاستعلام الفرعي و
JOIN، وتخطط للمشروع الختامي.
١استعلام داخل استعلام
الاستعلام الفرعي Subquery: استعلام بين قوسين داخل استعلام آخر، يُحسب أولًا وتُستخدم نتيجته. «المنتجات التي سعرها أعلى من متوسط الأسعار»: لا نعرف المتوسط، فنتركه لاستعلام فرعي:
SELECT name, priceFROM productsWHERE price > (SELECT AVG(price) FROM products)ORDER BY price DESC;
| name | price |
|---|---|
| عسل سدر 500غ | 150 |
| مبخرة | 120 |
| تمر عجوة 1كغ | 90 |
| دلة قهوة | 75 |
الاستعلام الفرعي هنا يعيد قيمة واحدة (المتوسط)، لذلك نقارنها بـ >. ويسمى استعلامًا فرعيًّا قياسيًّا Scalar.
٢IN و NOT IN: قائمة من استعلام
حين يعيد الاستعلام الفرعي عمودًا من قيم كثيرة، نستخدم IN. «المنتجات التي لم يطلبها أحد قط»:
SELECT name, stockFROM productsWHERE id NOT IN (SELECT product_id FROM order_items);
| name | stock |
|---|---|
| تمر سكري 1كغ | 0 |
٣EXISTS والاستعلام المرتبط
الاستعلام الفرعي المرتبط Correlated يستخدم عمودًا من الاستعلام الخارجي، فيُنفَّذ مرة لكل صف. «العملاء الذين طلبوا في مايو 2025»:
SELECT nameFROM customers cWHERE EXISTS ( SELECT 1 FROM orders o WHERE o.customer_id = c.id -- مرتبط بالعميل الحالي AND o.order_date >= '2025-05-01');
| name |
|---|
| سعود العتيبي |
| خالد الحربي |
| ماجد الغامدي |
ومثال مرتبط آخر: «المنتجات الأغلى من متوسط تصنيفها»؛ المتوسط يتغير بتغير تصنيف المنتج:
SELECT name, category, priceFROM products pWHERE price > (SELECT AVG(price) FROM products WHERE category = p.category)ORDER BY category;
| name | category | price |
|---|---|---|
| مبخرة | أدوات | 120 |
| زعفران 5غ | بهارات | 55 |
| تمر عجوة 1كغ | تمور | 90 |
| عسل سدر 500غ | عسل | 150 |
| قهوة عربية 250غ | مشروبات | 35 |
٤الجدول المشتق و WITH
يمكن أن يكون الاستعلام الفرعي في FROM، فتعامل نتيجته كأنها جدول. «متوسط قيمة الطلب وأكبر طلب»: نحسب إجمالي كل طلب أولًا، ثم نلخّص الإجماليات. والصياغة الأوضح بـ WITH (تعبير الجدول المؤقت CTE):
WITH totals AS ( SELECT order_id, SUM(qty * price) AS total FROM order_items GROUP BY order_id)SELECT ROUND(AVG(total), 2) AS avg_order, MAX(total) AS max_orderFROM totals;
| avg_order | max_order |
|---|---|
| 144.67 | 240 |
WITH مدعوم في MySQL 8 و SQLite و PostgreSQL. وهو يقسّم الاستعلام الطويل إلى خطوات مسمّاة يسهل قراءتها وتصحيحها.
٥الاستعلام الفرعي أم JOIN؟
كثير من الاستعلامات الفرعية يمكن كتابتها بـ JOIN، والعكس. «عدد طلبات كل عميل» بالطريقتين:
-- استعلام فرعي لكل عميلSELECT name, (SELECT COUNT(*) FROM orders o WHERE o.customer_id = c.id) AS orders_countFROM customers c;
| name | orders_count |
|---|---|
| سعود العتيبي | 3 |
| نورة القحطاني | 2 |
| خالد الحربي | 2 |
| ريم الشهري | 1 |
| فهد الدوسري | 1 |
| سارة المطيري | 1 |
| عبدالله الزهراني | 0 |
| هند السبيعي | 1 |
| ماجد الغامدي | 1 |
| لمى العنزي | 0 |
-- ربط وتجميع مرة واحدةSELECT c.name, COUNT(o.id) AS orders_countFROM customers cLEFT JOIN orders o ON o.customer_id = c.idGROUP BY c.id;
| name | orders_count |
|---|---|
| سعود العتيبي | 3 |
| نورة القحطاني | 2 |
| خالد الحربي | 2 |
| ريم الشهري | 1 |
| فهد الدوسري | 1 |
| سارة المطيري | 1 |
| عبدالله الزهراني | 0 |
| هند السبيعي | 1 |
| ماجد الغامدي | 1 |
| لمى العنزي | 0 |
كلاهما صحيح ونتيجتهما واحدة. الأول يُنفَّذ مرة لكل عميل، والثاني يجمع مرة واحدة. في الجداول الكبيرة قارن خطتيهما وزمنيهما واختر الأسرع. والمحسّنات الحديثة كثيرًا ما تحوّل أحدهما إلى الآخر تلقائيًّا.
| استخدم | حين |
|---|---|
| الاستعلام الفرعي القياسي | تحتاج قيمة واحدة للمقارنة: متوسط، أكبر قيمة. |
EXISTS / NOT EXISTS | السؤال «هل يوجد؟» بلا حاجة لأعمدة من الجدول الآخر. |
JOIN | تحتاج أعمدة من الجدولين معًا في النتيجة. |
WITH | الاستعلام طويل وله خطوات. |
٦المشروع الختامي
الآن تجمع كل ما تعلمته في مشروع واحد تنفّذه في المعمل على MySQL، في مجموعات من طالبين أو ثلاثة. اختر نظامًا من هذه، أو اقترح نظامًا آخر يوافق عليه المعلم:
- مكتبة الكلية: الكتب والمؤلفون والطلاب والإعارات.
- عيادة: المرضى والأطباء والمواعيد والوصفات.
- نادٍ رياضي: الأعضاء والاشتراكات والمدربون والحصص.
- مطعم: الأصناف والطاولات والطلبات والفواتير.
| المرحلة | المطلوب | الوحدات |
|---|---|---|
| 1. التحليل والتصميم | متطلبات النظام، ومخطط ERD (4 كيانات أو أكثر، وعلاقة متعدد لمتعدد واحدة على الأقل). | 4، 6 |
| 2. الإنشاء | CREATE TABLE بأنواع مناسبة، ومفاتيح أساسية وأجنبية، وقيود NOT NULL و UNIQUE و CHECK و DEFAULT. | 3، 5 |
| 3. البيانات | 15 صفًّا أو أكثر في كل جدول رئيس بـ INSERT، وأمثلة UPDATE و DELETE داخل معاملات. | 8، 9 |
| 4. الاستعلامات | 10 استعلامات تجيب عن أسئلة حقيقية: تصفية، وترتيب، وتجميع مع HAVING، وربط 3 جداول، واستعلام فرعي. | 7، 12 |
| 5. الأداء | استعلامان على جدول كبير (ولّد 50 ألف صف أو أكثر): الزمن والخطة قبل الفهرس وبعده، ونسبة التحسن. | 10، 11 |
| 6. التسليم | ملف .sql يعمل من أوله لآخره، وصورة ERD، وتقرير قصير بنتائج القياس. | — |
توقّع وتحقّق
استعلام فرعي يعيد قيمة واحدة نقارنه بـ:
«المنتجات التي لم تُطلب قط»:
الاستعلام المرتبط:
WITH تُستخدم لـ:
CTE.الخلاصة
- الاستعلام الفرعي بين قوسين يُحسب ويُستخدم في الاستعلام الخارجي.
- قيمة واحدة: > = <. قائمة:
IN. وجود:EXISTS. - المرتبط يستخدم عمودًا من الخارجي ويُنفَّذ لكل صف.
WITHاسمAS(…) يقسّم الاستعلام إلى خطوات.- المشروع الختامي: تصميم، فإنشاء، فبيانات، فاستعلامات، فقياس أداء.
تحدٍّ برمجي: أفضل العملاء
متقدمالإدارة تريد مكافأة العملاء الذين يزيد إنفاقهم على متوسط إنفاق العملاء. احسب الإنفاق من أصناف الطلبات غير الملغاة.
- الإنفاق = مجموع
qty * priceفيorder_itemsلطلبات العميل التي حالتها ليست «ملغى». - متوسط الإنفاق: متوسط إنفاق العملاء الذين لهم طلبات غير ملغاة.
- اعرض
nameوspentللعملاء الأعلى من المتوسط، مرتبين تنازليًّا بالإنفاق. - استخدم WITH أو استعلامًا فرعيًّا.
👀 اعرض النتيجة المطلوبة
| name | spent |
|---|---|
| سعود العتيبي | 383 |
| نورة القحطاني | 295 |
| خالد الحربي | 240 |
| سارة المطيري | 240 |
- الكود يعمل بلا أخطاء
- النتيجة: العملاء الأعلى من المتوسط وإنفاقهم
- يستخدم WITH أو استعلامًا فرعيًّا
💡 تلميح 1
💡 تلميح 2
2: WHERE s.spent > (SELECT AVG(spent) FROM spending)حاول بنفسك أولًا؛ للمسألة أكثر من حلّ صحيح.
WITH spending AS ( SELECT o.customer_id, SUM(oi.qty * oi.price) AS spent FROM orders o JOIN order_items oi ON oi.order_id = o.id WHERE o.status <> 'ملغى' GROUP BY o.customer_id)SELECT c.name, s.spentFROM spending sJOIN customers c ON c.id = s.customer_idWHERE s.spent > (SELECT AVG(spent) FROM spending)ORDER BY s.spent DESC;