الاستعلامات الفرعية في MySQL: استعلام داخل استعلام
«ما المنتجات الأغلى من متوسط الأسعار؟» و«من العملاء الذين لم يطلبوا شيئاً؟» أسئلة يطرحها صاحب أي متجر كل يوم، وكل منها يحتاج نتيجة استعلام لتُستخدم داخل استعلام آخر. هذا هو الاستعلام الفرعي (Subquery). في هذا الدرس ستتعلم أنواعه على بيانات متجرنا، مع القيود والرسائل الخاصة بـ MySQL.
نعمل على قاعدة shop بجداولها الثلاثة: products (ستة منتجات)، و customers (سارة وعمر وكريم)، و orders (خمسة طلبات).
المقارنة بقيمة محسوبة
Section titled “المقارنة بقيمة محسوبة”متوسط أسعار منتجاتنا حالياً 188.79. المنتجات الأغلى منه:
SELECT name, price FROM productsWHERE price > (SELECT AVG(price) FROM products);الناتج:
| name | price |
|---|---|
| سماعة لاسلكية | 450.50 |
| حقيبة مدرسية | 280.00 |
| آلة حاسبة علمية | 199.99 |
الاستعلام بين القوسين يُنفَّذ أولاً ويُرجع قيمة واحدة، ثم يستخدمها الاستعلام الخارجي في المقارنة. ولأن المتوسط يُحسب في كل مرة، تبقى النتيجة صحيحة مهما أضفت من منتجات أو غيّرت من أسعار.
تخيّل الأمر مثل سؤال جانبي تسأله للمحاسب قبل اتخاذ قرارك: «كم المتوسط؟»، ثم تكمل عملك بإجابته.
لكن احذر: مع = و > يجب أن يُرجع الاستعلام الفرعي قيمة واحدة فقط:
SELECT name FROM productsWHERE price = (SELECT price FROM products WHERE category = 'كتب');الناتج: ERROR 1242 (21000): Subquery returns more than 1 row
عندنا كتابان، فأرجع الاستعلام الداخلي سعرين، و MySQL لا تعرف بأيهما تقارن.
IN: ضمن قائمة ناتجة
Section titled “IN: ضمن قائمة ناتجة”عندما يُرجع الاستعلام الفرعي عدة قيم، استخدم IN. العملاء الذين لهم طلب واحد على الأقل:
SELECT name FROM customersWHERE id IN (SELECT customer_id FROM orders);الناتج:
| name |
|---|
| سارة |
| عمر |
والعكس بـ NOT IN: استبدلها في الاستعلام نفسه وستحصل على كريم، العميل الذي سجّل ولم يطلب بعد. قائمة كهذه كنز لفريق التسويق.
EXISTS والاستعلام المترابط
Section titled “EXISTS والاستعلام المترابط”EXISTS تسأل: هل يُرجع الاستعلام الفرعي صفاً واحداً على الأقل؟ المنتجات التي لم يطلبها أحد:
SELECT p.name FROM products pWHERE NOT EXISTS ( SELECT 1 FROM orders o WHERE o.product_id = p.id);الناتج:
| name |
|---|
| حقيبة مدرسية |
| قصص الأطفال |
الاستعلام الداخلي يستخدم p.id القادم من الخارج. هذا استعلام فرعي مترابط (Correlated Subquery): يُعاد تقييمه لكل منتج. و SELECT 1 عُرف شائع، لأن المهم وجود صف لا محتواه.
والاستعلام المترابط يصلح في قائمة الأعمدة أيضاً. عدد طلبات كل عميل:
SELECT c.name, (SELECT COUNT(*) FROM orders o WHERE o.customer_id = c.id) AS orders_countFROM customers c;الناتج:
| name | orders_count |
|---|---|
| سارة | 3 |
| عمر | 2 |
| كريم | 0 |
الأغلى في كل تصنيف
Section titled “الأغلى في كل تصنيف”SELECT p1.category, p1.name, p1.priceFROM products p1WHERE p1.price = ( SELECT MAX(p2.price) FROM products p2 WHERE p2.category = p1.category);الناتج:
| category | name | price |
|---|---|---|
| كتب | كتاب البرمجة للمبتدئين | 108.00 |
| إلكترونيات | سماعة لاسلكية | 450.50 |
| أدوات مدرسية | حقيبة مدرسية | 280.00 |
استخدمنا الجدول نفسه باسمين: p1 للمنتج الحالي، و p2 لحساب أعلى سعر في تصنيفه.
الجدول المشتق
Section titled “الجدول المشتق”نتيجة أي استعلام جدول، فيمكن وضعها في FROM وربطها بجداول أخرى. هذا اسمه الجدول المشتق (Derived Table). كم أنفق كل عميل؟
SELECT c.name, t.totalFROM customers cJOIN ( SELECT o.customer_id, SUM(o.quantity * p.price) AS total FROM orders o JOIN products p ON p.id = o.product_id GROUP BY o.customer_id) AS t ON t.customer_id = c.idORDER BY t.total DESC;الناتج:
| name | total |
|---|---|
| عمر | 1009.00 |
| سارة | 379.49 |
الاستعلام الداخلي حسب إجمالي كل عميل، والخارجي ربطه بالأسماء. و MySQL تشترط اسماً مستعاراً للجدول المشتق. بدون AS t:
SELECT * FROM (SELECT DISTINCT category FROM products);الناتج: ERROR 1248 (42000): Every derived table must have its own alias
WITH: أوضح وأنظف
Section titled “WITH: أوضح وأنظف”منذ MySQL 8.0 يمكنك تعريف الجدول المؤقت أولاً باسم واضح، ثم استخدامه:
WITH spending AS ( SELECT o.customer_id, SUM(o.quantity * p.price) AS total FROM orders o JOIN products p ON p.id = o.product_id GROUP BY o.customer_id)SELECT c.name, s.totalFROM customers cJOIN spending s ON s.customer_id = c.idWHERE s.total > 500;الناتج:
| name | total |
|---|---|
| عمر | 1009.00 |
الاستعلام يُقرأ الآن من الأعلى للأسفل كخطوتين: احسب الإنفاق، ثم اعرض كبار العملاء. هذه الصيغة اسمها تعبير الجدول المشترك (CTE).
قيد خاص بـ MySQL: تعديل الجدول نفسه
Section titled “قيد خاص بـ MySQL: تعديل الجدول نفسه”نريد خصم 10% على المنتجات الأغلى من المتوسط:
UPDATE products SET price = price * 0.9WHERE price > (SELECT AVG(price) FROM products);الناتج: ERROR 1093 (HY000): You can't specify target table 'products' for update in FROM clause
MySQL ترفض أن تعدّل جدولاً وتقرأ منه في استعلام فرعي في الأمر نفسه. الحل المعروف أن تلف الاستعلام الفرعي في جدول مشتق، فتُحسب قيمته أولاً وتُحفظ مؤقتاً:
UPDATE products SET price = price * 0.9WHERE price > ( SELECT average FROM (SELECT AVG(price) AS average FROM products) AS t);جرّبناه داخل معاملة فأصبح سعر السماعة 405.45 والحقيبة 252.00، ثم تراجعنا بـ ROLLBACK لتبقى بيانات الكورس كما هي.
استعلام فرعي أم JOIN؟
Section titled “استعلام فرعي أم JOIN؟”| استخدم | عندما |
|---|---|
JOIN |
تريد عرض أعمدة من الجدولين معاً |
IN أو EXISTS |
تريد تصفية جدول بناءً على جدول آخر |
| استعلام فرعي بقيمة واحدة | تقارن برقم محسوب مثل المتوسط |
جدول مشتق أو WITH |
تحتاج تجميعاً ثم ربطاً أو تصفية على ناتجه |
جرّب بنفسك
Section titled “جرّب بنفسك”- اعرض المنتجات التي مخزونها أقل من متوسط المخزون.
- اعرض أسماء المنتجات التي طلبها العميل «عمر»، باستخدام
INمع استعلام فرعي. - اعرض اسم كل منتج وبجانبه عدد مرات طلبه، باستعلام مترابط في قائمة الأعمدة.
- تحدٍّ: باستخدام
WITH، احسب إجمالي مبيعات كل تصنيف، ثم اعرض التصنيفات التي تجاوزت مبيعاتها 300.
أخطاء شائعة
Section titled “أخطاء شائعة”- أكثر من صف مع
=: الخطأ 1242. استخدمIN، أو أضف دالة تجميع أوLIMIT 1لضمان قيمة واحدة. - جدول مشتق بلا اسم: الخطأ 1248. أضف
AS nameبعد القوس. - تعديل جدول والقراءة منه: الخطأ 1093. لف الاستعلام الفرعي في جدول مشتق.
- استعلام مترابط على جدول ضخم: يُعاد تنفيذه لكل صف وقد يكون بطيئاً. جرّب كتابته بـ
JOIN، وتأكد من وجود فهرس على عمود الربط كما سترى في الدرس القادم.
اختبر نفسك
Section titled “اختبر نفسك”سؤال
ماذا تشترط MySQL عند وضع استعلام فرعي في FROM؟
كل جدول مشتق يحتاج اسماً مستعاراً، وإلا يظهر الخطأ 1248.
سؤال
أي أداة تختار لمعرفة العملاء الذين لم يطلبوا شيئاً، إذا كان عمود الربط قد يحتوي NULL؟
NOT EXISTS لا تتأثر بقيم NULL، بعكس NOT IN التي قد تُرجع نتيجة فارغة.
الخلاصة
Section titled “الخلاصة”- الاستعلام الفرعي استعلام بين قوسين داخل استعلام آخر.
- مع عوامل المقارنة يُرجع قيمة واحدة، ومع
INقائمة من القيم. EXISTSتفحص وجود صفوف، والاستعلام المترابط يُعاد تقييمه لكل صف.- الجدول المشتق في
FROMيحتاج اسماً مستعاراً، وWITHتجعله أوضح. - MySQL تمنع تعديل جدول والقراءة منه معاً إلا عبر جدول مشتق.