الاستعلام الفرعي: استعلام SELECT داخل استعلام SELECT آخر
الاستعلام الفرعي (subquery) هو ببساطة جملة SELECT موضوعة داخل جملة أخرى ومحاطة بأقواس. يقوم SQLite بتنفيذ الاستعلام الداخلي أولاً، ثم يُمرّر نتيجته إلى الاستعلام الخارجي ليكمل عمله.
لنُجهّز مثالاً بسيطاً سنعتمد عليه في الشرح:
خمسة طلبات، أربعة عملاء، اثنان منهم لم يطلبا أي شيء. هذا هو المثال الذي سنبني عليه طوال الشرح.
الاستعلامات الفرعية داخل WHERE: الفلترة بقائمة قيم
الصيغة الأكثر شيوعًا للـ subquery في sqlite: نجلب قائمة من المعرّفات عبر استعلام داخلي، ثم نفلتر الاستعلام الخارجي بناءً عليها.
الاستعلام الداخلي يُرجِع كل customer_id موجود في جدول orders، والاستعلام الخارجي يحتفظ فقط بالعملاء الذين يقع id الخاص بهم ضمن هذه القائمة. هكذا يظهر كليو وبوريس وآدا، بينما يغيب ديمتري لأنه لا يملك أي طلبات.
النمط IN (SELECT ...) هو حصان العمل في الاستعلامات الفرعية داخل WHERE عندما تريد "الصفوف الموجودة في A ولها مقابل في B". اقرأها ذهنيًا هكذا: "حيث تكون قيمة هذا العمود واحدة من القيم التي يُرجِعها الاستعلام الداخلي".
NOT IN: انتبه للقيم NULL
السؤال المعاكس — "أيّ العملاء لم يطلبوا شيئًا؟" — يبعد عنك سطرًا واحدًا فقط:
هذا يفي بالغرض هنا. لكن NOT IN فيها فخ خبيث: إذا أعاد الاستعلام الفرعي قيمة NULL ولو لمرة واحدة، فإن NOT IN بأكمله يتحوّل إلى NULL (وهو ليس TRUE)، والنتيجة أنك تحصل على صفر صفوف. مفاجأة صامتة بلا أي تحذير.
العادة الآمنة عند استخدام NOT IN مع عمود قد يحتوي على NULL:
أو استخدم NOT EXISTS، وهي لا تعاني من هذه المشكلة أصلاً. سنتطرق إليها لاحقاً.
الاستعلامات الفرعية القياسية (Scalar Subqueries): صف واحد وعمود واحد
الاستعلام الفرعي القياسي (scalar subquery) يُرجع قيمة واحدة فقط — صف واحد وعمود واحد — ويمكنك استخدامه في أي موضع تُتوقع فيه قيمة.
الاستعلام الداخلي SELECT MAX(total) FROM orders يُرجع القيمة 200، ثم يقوم الاستعلام الخارجي بتصفية الطلبات التي تطابق هذه القيمة. هذه الطريقة مفيدة في أي حالة تحتاج فيها للمقارنة مع نتيجة دالة تجميعية.
كذلك يمكنك استخدام scalar subquery داخل قائمة SELECT لإلحاق قيمة محسوبة بكل صف:
كل صف في جدول customers يُشغّل الاستعلام الداخلي مرة واحدة، مع تمرير قيمة customers.id إليه. هذا ما يُسمى بـ correlated subquery — وسنتحدث عنه بتفصيل أكبر بعد قليل. في الحالات التي تريد فيها "رقمًا واحدًا لكل صف" مثل هذه، عادةً ما يكون LEFT JOIN مع GROUP BY أفضل من ناحية الأداء، لكن صيغة scalar subquery تبقى أنظف وأوضح في القراءة.
EXISTS: مجرد التحقق من وجود تطابق
EXISTS هو القريب الهادئ لـ IN. لا يهتم بالقيم نفسها — كل ما يفعله هو التحقق ممّا إذا كان الاستعلام الفرعي يُرجع أي صف أم لا. ولهذا السبب تجد الناس يكتبون SELECT 1 بداخله عادةً، لأن العمود المُختار لا يهم أصلًا.
يجد هذا الاستعلام العملاء الذين لديهم طلبٌ واحد على الأقل تجاوزت قيمته 100. لاحظ أن الاستعلام الداخلي يستخدم c.id القادم من الاستعلام الخارجي، وهذا بالضبط ما يجعله correlated subquery. يتوقف SQLite عن فحص الجدول الداخلي بمجرد عثوره على أول تطابق، ولهذا السبب غالبًا ما يتفوّق EXISTS على IN حين يكون السؤال: "هل لهذا الصف صفٌّ مرتبط به؟".
أما النفي عبر NOT EXISTS، فهو الطريقة الآمنة مع قيم NULL للسؤال "لا يوجد صف مرتبط":
الاستعلام الفرعي داخل FROM: الجدول المُشتق
يمكن استخدام الاستعلام الفرعي في أي مكان يقبل اسم جدول، بما في ذلك جملة FROM. هنا يتحوّل الاستعلام الداخلي إلى ما يُعرف بـ"الجدول المُشتق" (Derived Table)، وهو جدول مؤقت بإمكانك إعطاؤه اسمًا ثم الانضمام إليه أو تصفيته أو تطبيق دوال التجميع عليه كأي جدول عادي.
الاستعلام الداخلي يحسب الإجمالي لكل عميل، بينما الاستعلام الخارجي يأخذ متوسط هذه الإجماليات لكل دولة. هذا النوع من التجميع على مرحلتين هو بالضبط ما صُممت من أجله الجداول المشتقة (derived tables)، خاصة حين يستحيل إنجاز كل شيء داخل GROUP BY واحد.
لاحظ أن الاسم المستعار AS per_customer ليس اختياريًا؛ كل جدول مشتق يجب أن يكون له اسم.
الاستعلامات الفرعية المرتبطة (Correlated Subqueries): تنفيذ لكل صف خارجي
نقول عن الاستعلام الفرعي إنه مرتبط (correlated) عندما يشير إلى عمود من الاستعلام الخارجي. في هذه الحالة، يضطر SQLite إلى إعادة تنفيذ الاستعلام الداخلي مع كل صف من الاستعلام الخارجي، وهذا يمنحك مرونة كبيرة لكن قد يكون مكلفًا من ناحية الأداء.
لكل عميل، نجيب أكبر طلب قام به. الاستعلام الداخلي يعتمد على customers.id، فيتم تنفيذه مرة لكل عميل. أما العملاء الذين لا يملكون أي طلبات، فستكون النتيجة NULL — وهذا بالضبط ما نريده.
الـ correlated subquery هي الخيار الطبيعي عندما تريد قول: "لكل صف في A، احسب شيئاً من B". إذا كان الجدول صغيراً أو كان البحث يعتمد على فهرس (index)، فلا مشكلة. لكن مع الجداول الضخمة بدون فهارس داعمة، اعمل قياس أداء قبل النشر — لأن JOIN مع GROUP BY غالباً ما يكون أسرع.
subquery vs join: أيهما تختار؟
الاستعلامان التاليان يجيبان على نفس السؤال:
كلا الصيغتين تُرجِعان نفس النتائج، ومُحسِّن SQLite كثيرًا ما يُعيد كتابة إحداهما إلى الأخرى داخليًا. لذا الاختيار يعتمد على وضوح القراءة:
- استخدم الاستعلام الفرعي (subquery) عندما تحتاج فقط للتصفية، ولا ترغب بأن تتسرّب أعمدة الجدول الداخلي إلى النتيجة.
- استخدم JOIN عندما تحتاج النتيجة أعمدةً من كلا الجدولين.
- استخدم EXISTS عندما يكون سؤالك: "هل يوجد صفّ مرتبط واحد على الأقل؟" — فهو أوضح، ويُجنّبك مطبّات
NULLالتي يقع فيهاINوNOT IN.
وإن ترددت، فاكتب الصيغة التي تشرح نفسها بنفسها حين تقرؤها بصوت مرتفع.
مأزق شائع: الاستعلامات الفرعية التي تُرجع أكثر من صفّ
أيّ subquery يُستخدم مع = يجب أن يُرجِع صفًّا واحدًا كحدٍّ أقصى. وإذا أرجع أكثر من صفّ، يختار SQLite واحدًا منها (عشوائيًا فعليًا)، فتحصل على نتائج خاطئة بصمت — دون أيّ رسالة خطأ.
استخدم IN عندما يُحتمل أن يُرجع الاستعلام الداخلي أكثر من صف:
إذا كنت تتوقع صفًا واحدًا فقط وتريد فرض ذلك، أضف LIMIT 1 مع ORDER BY حتى يكون الاختيار حتميًا على الأقل. والأفضل من ذلك: اكتب الاستعلام بطريقة تضمن البيانات نفسها صفًا واحدًا (بالتصفية على عمود فريد).
الخطوة التالية: تعابير الجداول العامة (CTE)
الاستعلامات الفرعية داخل FROM تصبح فوضوية بسرعة، خاصة حين تحتاج إلى نفس الجدول المشتق مرتين، أو حين يصل التداخل إلى ثلاثة مستويات. تتيح لك تعابير الجداول العامة (WITH ... AS (...)) تسمية الاستعلام الفرعي في البداية ثم الإشارة إليه بالاسم في بقية الجملة. هذا ما سنتناوله في الصفحة التالية.
الأسئلة الشائعة
ما هو الاستعلام الفرعي (Subquery) في SQLite؟
الاستعلام الفرعي هو جملة SELECT مكتوبة داخل جملة أخرى ومحاطة بأقواس. يقوم SQLite بتنفيذ الاستعلام الداخلي أولاً ثم يمرّر نتيجته إلى الاستعلام الخارجي. يمكن استخدام الاستعلامات الفرعية في WHERE وFROM وSELECT وعدة مواضع أخرى.
ما الفرق بين IN و EXISTS في SQLite؟
IN (SELECT ...) يتحقق إن كانت القيمة تطابق أي صف يُرجعه الاستعلام الفرعي. أما EXISTS (SELECT ...) فيتحقق فقط من وجود أي صف في النتيجة دون الاهتمام بالقيم نفسها. عادةً ما يكون EXISTS الخيار الأفضل عندما يشير الاستعلام الداخلي إلى الصف الخارجي (أي عند وجود استعلام فرعي مرتبط/correlated).
هل أستخدم الاستعلام الفرعي أم JOIN في SQLite؟
استخدم JOIN عندما تحتاج إلى أعمدة من كلا الجدولين في النتيجة النهائية. أما الاستعلام الفرعي فيناسبك عندما يكون هدفك مجرد التصفية أو حساب قيمة واحدة. في الغالب يقوم محسّن SQLite بإعادة كتابة أحد الشكلين إلى الآخر داخلياً، لذا اختر الصيغة الأوضح قراءةً لك.
ما المقصود بالاستعلام الفرعي المرتبط (Correlated Subquery)؟
هو استعلام فرعي يستخدم عموداً من الاستعلام الخارجي، وبالتالي يُعاد تنفيذه لكل صف خارجي على حدة. هذا النوع مرن لكنه قد يكون بطيئاً مع الجداول الكبيرة. إذا لاحظت أن استعلاماً مرتبطاً يستهلك أداء استعلامك، فإن إعادة كتابته على شكل JOIN أو باستخدام CTE غالباً ما تُحسّن الأمور.