الغوص العميق في الاستعلامات الفرعية
الاستعلامات الفرعية (المعروفة أيضاً بالاستعلامات المتداخلة أو الاستعلامات الداخلية) هي تراكيب SQL قوية تسمح لك باستخدام نتيجة استعلام واحد داخل استعلام آخر. في هذا الدرس، سنستكشف الأنواع المختلفة من الاستعلامات الفرعية ومتى نستخدمها بفعالية.
ما هي الاستعلامات الفرعية؟
الاستعلام الفرعي هو عبارة SELECT متداخلة داخل عبارة SQL أخرى. يمكن استخدام الاستعلامات الفرعية في جمل SELECT و INSERT و UPDATE و DELETE و FROM.
مفهوم أساسي: يتم تنفيذ الاستعلامات الفرعية أولاً، ويتم تمرير نتائجها إلى الاستعلام الخارجي. فكر فيها كجداول مؤقتة موجودة فقط طوال مدة الاستعلام.
أنواع الاستعلامات الفرعية
يدعم MySQL عدة أنواع من الاستعلامات الفرعية، كل منها يخدم أغراضاً مختلفة:
1. الاستعلامات القياسية (Scalar): تُرجع قيمة واحدة (صف واحد، عمود واحد)
2. استعلامات الأعمدة: تُرجع عموداً واحداً بصفوف متعددة
3. استعلامات الصفوف: تُرجع صفاً واحداً بأعمدة متعددة
4. استعلامات الجداول: تُرجع مجموعة نتائج كاملة (صفوف وأعمدة متعددة)
5. الاستعلامات المترابطة: تشير إلى أعمدة من الاستعلام الخارجي
1. الاستعلامات القياسية
تُرجع الاستعلامات القياسية قيمة واحدة بالضبط ويمكن استخدامها حيثما يُتوقع قيمة واحدة:
-- البحث عن المنتجات بسعر أعلى من المتوسط
SELECT product_name, price
FROM products
WHERE price > (
SELECT AVG(price)
FROM products
);
-- عرض المنتج مع فرق السعر من المتوسط
SELECT
product_name,
price,
price - (SELECT AVG(price) FROM products) AS price_diff
FROM products;
2. استعلامات الأعمدة (IN, ANY, ALL)
تُرجع استعلامات الأعمدة قيماً متعددة في عمود واحد وتُستخدم عادةً مع عوامل IN أو ANY أو ALL:
-- البحث عن العملاء الذين قدموا طلبات في 2023
SELECT customer_name
FROM customers
WHERE customer_id IN (
SELECT DISTINCT customer_id
FROM orders
WHERE YEAR(order_date) = 2023
);
-- البحث عن المنتجات أغلى من جميع المنتجات في فئة "Books"
SELECT product_name, price
FROM products
WHERE price > ALL (
SELECT price
FROM products
WHERE category = 'Books'
);
نصيحة: استخدم IN عند التحقق من العضوية في مجموعة. استخدم ANY/SOME عند المقارنة بقيمة واحدة على الأقل. استخدم ALL عند المقارنة بكل قيمة في المجموعة.
3. استعلامات الصفوف
تُرجع استعلامات الصفوف صفاً واحداً بأعمدة متعددة:
-- البحث عن منتج يطابق مزيج معين من السعر والمخزون
SELECT product_name
FROM products
WHERE (price, stock_quantity) = (
SELECT MIN(price), MAX(stock_quantity)
FROM products
WHERE category = 'Electronics'
);
4. استعلامات الجداول (جملة FROM)
تُرجع استعلامات الجداول صفوفاً وأعمدة متعددة، وغالباً ما تُستخدم في جملة FROM كجداول مشتقة:
-- حساب متوسط قيمة الطلب حسب العميل
SELECT
customer_name,
avg_order_value
FROM customers c
JOIN (
SELECT
customer_id,
AVG(total_amount) AS avg_order_value
FROM orders
GROUP BY customer_id
) AS order_stats ON c.customer_id = order_stats.customer_id
WHERE avg_order_value > 100;
-- البحث عن أفضل 3 منتجات في كل فئة
SELECT *
FROM (
SELECT
product_name,
category,
price,
ROW_NUMBER() OVER (PARTITION BY category ORDER BY price DESC) AS rank_in_category
FROM products
) AS ranked_products
WHERE rank_in_category <= 3;
5. الاستعلامات المترابطة
تشير الاستعلامات المترابطة إلى أعمدة من الاستعلام الخارجي ويتم تنفيذها مرة واحدة لكل صف في الاستعلام الخارجي:
-- البحث عن المنتجات الأغلى في فئتها
SELECT p1.product_name, p1.category, p1.price
FROM products p1
WHERE p1.price = (
SELECT MAX(p2.price)
FROM products p2
WHERE p2.category = p1.category
);
-- البحث عن العملاء الذين قدموا طلبات أكثر من متوسط قيمة طلباتهم
SELECT
c.customer_name,
o.order_id,
o.total_amount
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
WHERE o.total_amount > (
SELECT AVG(total_amount)
FROM orders o2
WHERE o2.customer_id = c.customer_id
);
تحذير الأداء: يمكن أن تكون الاستعلامات المترابطة بطيئة لأنها تُنفذ مرة واحدة لكل صف في الاستعلام الخارجي. فكر في استخدام JOINs أو دوال النافذة كبدائل عندما يكون ذلك ممكناً.
EXISTS و NOT EXISTS
يختبر EXISTS ما إذا كان الاستعلام الفرعي يُرجع أي صفوف. غالباً ما يكون أكثر كفاءة من IN لمجموعات البيانات الكبيرة:
-- البحث عن العملاء الذين قدموا طلباً واحداً على الأقل
SELECT customer_name
FROM customers c
WHERE EXISTS (
SELECT 1
FROM orders o
WHERE o.customer_id = c.customer_id
);
-- البحث عن المنتجات التي لم يتم طلبها أبداً
SELECT product_name
FROM products p
WHERE NOT EXISTS (
SELECT 1
FROM order_items oi
WHERE oi.product_id = p.product_id
);
نصيحة التحسين: SELECT 1 في جمل EXISTS هو اصطلاح - يتحقق MySQL فقط من وجود صفوف، وليس ما يتم تحديده. هذا يجعل EXISTS فعالاً جداً.
الاستعلامات الفرعية في جمل مختلفة
-- استعلام فرعي في SELECT (قياسي)
SELECT
product_name,
(SELECT COUNT(*) FROM order_items WHERE product_id = p.product_id) AS times_ordered
FROM products p;
-- استعلام فرعي في WHERE
SELECT product_name
FROM products
WHERE category_id IN (SELECT category_id FROM categories WHERE is_active = 1);
-- استعلام فرعي في HAVING
SELECT category, COUNT(*) AS product_count
FROM products
GROUP BY category
HAVING COUNT(*) > (SELECT AVG(cnt) FROM (SELECT COUNT(*) AS cnt FROM products GROUP BY category) AS counts);
اعتبارات الأداء
أفضل الممارسات:
- استخدم EXISTS بدلاً من IN للفحوصات المترابطة مع مجموعات البيانات الكبيرة
- فكر في JOINs كبدائل - غالباً ما تكون أسرع
- تجنب الاستعلامات الفرعية في جملة SELECT عند معالجة العديد من الصفوف
- استخدم EXPLAIN لتحليل أداء الاستعلام الفرعي
- أنشئ فهارس مناسبة على الأعمدة المستخدمة في الاستعلامات الفرعية
مثال من العالم الحقيقي: تحليلات التجارة الإلكترونية
-- البحث عن المنتجات التي تؤدي أفضل من متوسط الفئة
SELECT
p.product_name,
p.category,
p.sales_count,
(SELECT AVG(sales_count)
FROM products p2
WHERE p2.category = p.category) AS category_avg,
p.sales_count - (SELECT AVG(sales_count)
FROM products p2
WHERE p2.category = p.category) AS diff_from_avg
FROM products p
WHERE p.sales_count > (
SELECT AVG(sales_count)
FROM products p2
WHERE p2.category = p.category
)
ORDER BY category, diff_from_avg DESC;
تمرين عملي:
التحدي: اكتب استعلامات للسيناريوهات التالية:
- البحث عن جميع العملاء الذين أنفقوا أكثر من متوسط الإنفاق الإجمالي
- سرد المنتجات التي لم يتم طلبها أبداً باستخدام EXISTS
- البحث عن ثاني أعلى راتب في كل قسم
تلميحات الحل:
-- 1. إنفاق العملاء أعلى من المتوسط
SELECT customer_name, total_spent
FROM (
SELECT c.customer_name, SUM(o.total_amount) AS total_spent
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id
GROUP BY c.customer_id, c.customer_name
) AS customer_spending
WHERE total_spent > (SELECT AVG(total_spent) FROM (...));
-- 2. المنتجات التي لم يتم طلبها أبداً
SELECT product_name FROM products p
WHERE NOT EXISTS (SELECT 1 FROM order_items WHERE product_id = p.product_id);
-- 3. ثاني أعلى راتب
SELECT department, MAX(salary) AS second_highest
FROM employees e1
WHERE salary < (SELECT MAX(salary) FROM employees e2 WHERE e2.department = e1.department)
GROUP BY department;
الملخص
في هذا الدرس، تعلمت:
- خمسة أنواع من الاستعلامات الفرعية: قياسية، أعمدة، صفوف، جداول، ومترابطة
- كيفية استخدام IN و ANY و ALL و EXISTS و NOT EXISTS مع الاستعلامات الفرعية
- الاستعلامات المترابطة تشير إلى أعمدة الاستعلام الخارجي
- اعتبارات الأداء وتقنيات التحسين
- متى تستخدم الاستعلامات الفرعية مقابل JOINs
التالي: في الدرس التالي، سنتقن تقنيات JOIN المتقدمة بما في ذلك الانضمامات الذاتية، والانضمامات لجداول متعددة، واستراتيجيات تحسين الانضمام!