تخطَّ إلى المحتوى

الاستعلامات الفرعية في MySQL: استعلام داخل استعلام

الدرس 9 من 12 · MySQL

«ما المنتجات الأغلى من متوسط الأسعار؟» و«من العملاء الذين لم يطلبوا شيئاً؟» أسئلة يطرحها صاحب أي متجر كل يوم، وكل منها يحتاج نتيجة استعلام لتُستخدم داخل استعلام آخر. هذا هو الاستعلام الفرعي (Subquery). في هذا الدرس ستتعلم أنواعه على بيانات متجرنا، مع القيود والرسائل الخاصة بـ MySQL.

نعمل على قاعدة shop بجداولها الثلاثة: products (ستة منتجات)، و customers (سارة وعمر وكريم)، و orders (خمسة طلبات).

متوسط أسعار منتجاتنا حالياً 188.79. المنتجات الأغلى منه:

SELECT name, price FROM products
WHERE price > (SELECT AVG(price) FROM products);

الناتج:

name price
سماعة لاسلكية 450.50
حقيبة مدرسية 280.00
آلة حاسبة علمية 199.99

الاستعلام بين القوسين يُنفَّذ أولاً ويُرجع قيمة واحدة، ثم يستخدمها الاستعلام الخارجي في المقارنة. ولأن المتوسط يُحسب في كل مرة، تبقى النتيجة صحيحة مهما أضفت من منتجات أو غيّرت من أسعار.

تخيّل الأمر مثل سؤال جانبي تسأله للمحاسب قبل اتخاذ قرارك: «كم المتوسط؟»، ثم تكمل عملك بإجابته.

لكن احذر: مع = و > يجب أن يُرجع الاستعلام الفرعي قيمة واحدة فقط:

SELECT name FROM products
WHERE price = (SELECT price FROM products WHERE category = 'كتب');

الناتج: ERROR 1242 (21000): Subquery returns more than 1 row

عندنا كتابان، فأرجع الاستعلام الداخلي سعرين، و MySQL لا تعرف بأيهما تقارن.

عندما يُرجع الاستعلام الفرعي عدة قيم، استخدم IN. العملاء الذين لهم طلب واحد على الأقل:

SELECT name FROM customers
WHERE id IN (SELECT customer_id FROM orders);

الناتج:

name
سارة
عمر

والعكس بـ NOT IN: استبدلها في الاستعلام نفسه وستحصل على كريم، العميل الذي سجّل ولم يطلب بعد. قائمة كهذه كنز لفريق التسويق.

EXISTS والاستعلام المترابط

Section titled “EXISTS والاستعلام المترابط”

EXISTS تسأل: هل يُرجع الاستعلام الفرعي صفاً واحداً على الأقل؟ المنتجات التي لم يطلبها أحد:

SELECT p.name FROM products p
WHERE 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_count
FROM customers c;

الناتج:

name orders_count
سارة 3
عمر 2
كريم 0
SELECT p1.category, p1.name, p1.price
FROM products p1
WHERE 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 لحساب أعلى سعر في تصنيفه.

نتيجة أي استعلام جدول، فيمكن وضعها في FROM وربطها بجداول أخرى. هذا اسمه الجدول المشتق (Derived Table). كم أنفق كل عميل؟

SELECT c.name, t.total
FROM customers c
JOIN (
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.id
ORDER 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

منذ 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.total
FROM customers c
JOIN spending s ON s.customer_id = c.id
WHERE s.total > 500;

الناتج:

name total
عمر 1009.00

الاستعلام يُقرأ الآن من الأعلى للأسفل كخطوتين: احسب الإنفاق، ثم اعرض كبار العملاء. هذه الصيغة اسمها تعبير الجدول المشترك (CTE).

قيد خاص بـ MySQL: تعديل الجدول نفسه

Section titled “قيد خاص بـ MySQL: تعديل الجدول نفسه”

نريد خصم 10% على المنتجات الأغلى من المتوسط:

UPDATE products SET price = price * 0.9
WHERE 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.9
WHERE price > (
SELECT average FROM (SELECT AVG(price) AS average FROM products) AS t
);

جرّبناه داخل معاملة فأصبح سعر السماعة 405.45 والحقيبة 252.00، ثم تراجعنا بـ ROLLBACK لتبقى بيانات الكورس كما هي.

استخدم عندما
JOIN تريد عرض أعمدة من الجدولين معاً
IN أو EXISTS تريد تصفية جدول بناءً على جدول آخر
استعلام فرعي بقيمة واحدة تقارن برقم محسوب مثل المتوسط
جدول مشتق أو WITH تحتاج تجميعاً ثم ربطاً أو تصفية على ناتجه
  1. اعرض المنتجات التي مخزونها أقل من متوسط المخزون.
  2. اعرض أسماء المنتجات التي طلبها العميل «عمر»، باستخدام IN مع استعلام فرعي.
  3. اعرض اسم كل منتج وبجانبه عدد مرات طلبه، باستعلام مترابط في قائمة الأعمدة.
  4. تحدٍّ: باستخدام WITH، احسب إجمالي مبيعات كل تصنيف، ثم اعرض التصنيفات التي تجاوزت مبيعاتها 300.
  • أكثر من صف مع =: الخطأ 1242. استخدم IN، أو أضف دالة تجميع أو LIMIT 1 لضمان قيمة واحدة.
  • جدول مشتق بلا اسم: الخطأ 1248. أضف AS name بعد القوس.
  • تعديل جدول والقراءة منه: الخطأ 1093. لف الاستعلام الفرعي في جدول مشتق.
  • استعلام مترابط على جدول ضخم: يُعاد تنفيذه لكل صف وقد يكون بطيئاً. جرّب كتابته بـ JOIN، وتأكد من وجود فهرس على عمود الربط كما سترى في الدرس القادم.

سؤال

ماذا تشترط MySQL عند وضع استعلام فرعي في FROM؟

سؤال

أي أداة تختار لمعرفة العملاء الذين لم يطلبوا شيئاً، إذا كان عمود الربط قد يحتوي NULL؟

  • الاستعلام الفرعي استعلام بين قوسين داخل استعلام آخر.
  • مع عوامل المقارنة يُرجع قيمة واحدة، ومع IN قائمة من القيم.
  • EXISTS تفحص وجود صفوف، والاستعلام المترابط يُعاد تقييمه لكل صف.
  • الجدول المشتق في FROM يحتاج اسماً مستعاراً، و WITH تجعله أوضح.
  • MySQL تمنع تعديل جدول والقراءة منه معاً إلا عبر جدول مشتق.