{"id":"l-sql-2-1","courseId":"sql-for-analysts","lang":"ar","kind":"lesson","summary":"اضغط الصفوف في صف واحد لكل مجموعة بـ GROUP BY، وصفِّ المجموعات بـ HAVING، وعُدّ بشروط باستخدام FILTER، وحدّد مستوى تفصيل كل نتيجة قبل أن تثق بها.","html":"<p>يعود مارك من العمليات بالسؤال الذي لم تستطع الإجابة عنه في الدرس الماضي: «ما متوسط وقت الحل لدينا، حسب الأولوية؟». تعرف كيف تحسب ساعات تذكرة واحدة. أما الآن فتحتاج إلى <em>صف واحد لكل أولوية</em>، يلخّص كلٌّ منها عشرات التذاكر. هذا ما يفعله <code dir=\"ltr\">GROUP BY</code>، والفكرة التي تحته، أي مستوى تفصيل النتيجة، هي أنفع مفهوم منفرد في SQL التحليلي.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">صف واحد لكل ماذا؟</h2>\n<p>لكل جدول ولكل نتيجة <strong>مستوى تفصيل</strong> (grain): الشيء الذي يمثّله الصف الواحد. في <code dir=\"ltr\">support_tickets</code> صف واحد لكل تذكرة. ومارك يريد صفًا واحدًا لكل أولوية. اكتب ذلك أولًا، ثم اجعل الـ query يطابقه:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"sql\" data-run=\"sql\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">SQL</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn code-run\" data-action=\"run\" data-lang=\"sql\" data-index=\"0\" aria-label=\"شغّل هذا المثال\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\"/></svg><span>تشغيل</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">SELECT</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  priority,</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  COUNT</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">*</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)                </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> tickets,</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  COUNT</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(closed_at)        </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> closed,</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  ROUND</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">AVG</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((julianday(closed_at) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">-</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> julianday(opened_at)) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">*</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 24</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">), </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> avg_hours,</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  ROUND</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">MAX</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">((julianday(closed_at) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">-</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> julianday(opened_at)) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">*</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 24</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">), </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> max_hours</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">FROM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> support_tickets</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">GROUP BY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> priority</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ORDER BY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> avg_hours;</span></span></code></pre><div class=\"run-output\" data-run-output=\"0\" hidden></div></div>\n<p>أربعة صفوف، صف لكل أولوية، وهذا يطابق مستوى التفصيل الذي حدّدته. التذاكر العاجلة تُغلق في 6.5 ساعات في المتوسط، والمرتفعة في يوم واحد أو نحوه، والمنخفضة والعادية في أكثر من ثلاثة أيام. لاحظ كيف يتخطّى <code dir=\"ltr\">COUNT(closed_at)</code> و<code dir=\"ltr\">AVG</code> التذاكر المفتوحة بصمت، كما وعدت الدروس السابقة تمامًا. والعمود <code dir=\"ltr\">max_hours</code> مهم أيضًا: قد يبدو المتوسط جيدًا بينما انتظر عميل واحد أسبوعًا كاملًا.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 720 150\" role=\"img\" aria-labelledby=\"l-sql-2-1-t1\"><defs><marker id=\"arrow-l-sql-2-1-1\" viewBox=\"0 0 10 10\" refX=\"9\" refY=\"5\" markerWidth=\"7\" markerHeight=\"7\" orient=\"auto-start-reverse\"><path d=\"M0 0 L10 5 L0 10 z\" class=\"d-arrowhead\"/></marker></defs>\n  <title id=\"l-sql-2-1-t1\">ترتيب التقييم المنطقي: FROM، ثم يصفّي WHERE الصفوف، ويكوّن GROUP BY المجموعات، ويصفّي HAVING المجموعات، ويحسب SELECT أعمدة النتيجة، ويرتّب ORDER BY، ويقصّ LIMIT.</title>\n  <rect class=\"d-box\" x=\"10\" y=\"40\" width=\"86\" height=\"44\" rx=\"8\"/>\n  <text class=\"d-code\" x=\"53\" y=\"67\" text-anchor=\"middle\">FROM</text>\n  <rect class=\"d-box-accent\" x=\"111\" y=\"40\" width=\"86\" height=\"44\" rx=\"8\"/>\n  <text class=\"d-code\" x=\"154\" y=\"67\" text-anchor=\"middle\">WHERE</text>\n  <rect class=\"d-box-primary\" x=\"212\" y=\"40\" width=\"100\" height=\"44\" rx=\"8\"/>\n  <text class=\"d-code\" x=\"262\" y=\"67\" text-anchor=\"middle\">GROUP BY</text>\n  <rect class=\"d-box-accent\" x=\"327\" y=\"40\" width=\"86\" height=\"44\" rx=\"8\"/>\n  <text class=\"d-code\" x=\"370\" y=\"67\" text-anchor=\"middle\">HAVING</text>\n  <rect class=\"d-box\" x=\"428\" y=\"40\" width=\"86\" height=\"44\" rx=\"8\"/>\n  <text class=\"d-code\" x=\"471\" y=\"67\" text-anchor=\"middle\">SELECT</text>\n  <rect class=\"d-box\" x=\"529\" y=\"40\" width=\"100\" height=\"44\" rx=\"8\"/>\n  <text class=\"d-code\" x=\"579\" y=\"67\" text-anchor=\"middle\">ORDER BY</text>\n  <rect class=\"d-box\" x=\"644\" y=\"40\" width=\"66\" height=\"44\" rx=\"8\"/>\n  <text class=\"d-code\" x=\"677\" y=\"67\" text-anchor=\"middle\">LIMIT</text>\n  <text class=\"d-label-muted\" x=\"154\" y=\"110\" text-anchor=\"middle\">يصفّي الصفوف</text>\n  <text class=\"d-label-muted\" x=\"262\" y=\"110\" text-anchor=\"middle\">يكوّن المجموعات</text>\n  <text class=\"d-label-muted\" x=\"370\" y=\"110\" text-anchor=\"middle\">يصفّي المجموعات</text>\n  <text class=\"d-label-muted\" x=\"471\" y=\"110\" text-anchor=\"middle\">يحسب</text>\n  <path class=\"d-arrow\" d=\"M30 130 L690 130\" marker-end=\"url(#arrow-l-sql-2-1-1)\"/>\n</svg><figcaption>الترتيب المنطقي الذي يقيّم به SQL الـ query</figcaption></figure>\n<p>يشرح هذا المخطط معظم أخطاء GROUP BY. يعمل <code dir=\"ltr\">WHERE</code> قبل أن توجد المجموعات، فلا يستطيع أن يختبر <code dir=\"ltr\">COUNT(*)</code>؛ هذه مهمة <code dir=\"ltr\">HAVING</code>. ويعمل <code dir=\"ltr\">ORDER BY</code> بعد SELECT، فيستطيع دائمًا استخدام الاسم المستعار لعمود. يقبل SQLite وPostgreSQL أيضًا الاسم المستعار في <code dir=\"ltr\">GROUP BY</code> من باب التسهيل، ولهذا يعمل <code dir=\"ltr\">GROUP BY month</code> أدناه، لكن الترتيب المنطقي يبقى كما في المخطط.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">WHERE مقابل HAVING</h2>\n<p>يريد كريم من المالية أن يعرف أي المنتجات باعت 100 وحدة على الأقل بسعرها الكامل، دون أي خصم. عدد الوحدات لكل منتج قيمة تجميعية، فيذهب الحدّ إلى <code dir=\"ltr\">HAVING</code>. أما «دون خصم» فصفة لكل سطر طلب على حدة، أي أنه شرط على الصفوف ومكانه <code dir=\"ltr\">WHERE</code>:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"sql\" data-run=\"sql\" data-index=\"1\"><div class=\"code-head\"><span class=\"code-title\">SQL</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn code-run\" data-action=\"run\" data-lang=\"sql\" data-index=\"1\" aria-label=\"شغّل هذا المثال\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\"/></svg><span>تشغيل</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">SELECT</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  product_id,</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  SUM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(quantity) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> units,</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  COUNT</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">DISTINCT</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> order_id) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> orders</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">FROM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> order_items</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">WHERE</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> discount </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 0</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">GROUP BY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> product_id</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">HAVING</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> SUM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(quantity) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">>=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 100</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ORDER BY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> units </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">DESC</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">LIMIT</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 5</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span></code></pre><div class=\"run-output\" data-run-output=\"1\" hidden></div></div>\n<p>اقرأه بترتيب المخطط: أسقِط السطور المخفّضة، وجمّع الباقي حسب المنتج، وأبقِ المنتجات التي لها 100 وحدة أو أكثر، ثم احسب ورتّب واقصّ. يعدّ <code dir=\"ltr\">COUNT(DISTINCT order_id)</code> الطلبات لا السطور، لأن كل منتج في بيانات Cartwheel يظهر مرة واحدة على الأكثر في كل طلب، فلا يختلف عدد السطور عن عدد الطلبات إلا حين تعدّ عبر منتجات مختلفة.</p>\n<aside class=\"callout callout-mistake\" role=\"note\"><p class=\"callout-title\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3\"/><path d=\"M12 9v4\"/><path d=\"M12 17h.01\"/></svg><span><span class=\"callout-kind\">خطأ شائع</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>الأعمدة المجرّدة في SQLite</span></p>\n<p>كل عمود في SELECT يجب أن يكون في <code dir=\"ltr\">GROUP BY</code> أو داخل دالة تجميع. يفرض PostgreSQL هذه القاعدة، أما SQLite فلا. يعمل <code dir=\"ltr\">SELECT channel, status, COUNT(*) FROM orders GROUP BY channel</code> ويعرض “delivered” لكل قناة، لأن SQLite ملأ <code dir=\"ltr\">status</code> من صف اعتباطي في كل مجموعة. والأسوأ أن نسيان <code dir=\"ltr\">GROUP BY</code> كليًا يضغط الجدول في <strong>صف واحد</strong> فيه رقم منتج اعتباطي بجانب المجموع الكلي. إذا جاءت النتيجة بصفوف أقل مما توقّعت، فافحص الـ GROUP BY أولًا.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">عدّ جزء من الصفوف داخل كل مجموعة</h2>\n<p>تسأل هدى من التسويق عن نسبة طلبات كل شهر التي جاءت عبر تطبيق الجوال. تحتاج إلى عددين لكل شهر: كل الطلبات، وطلبات التطبيق. عبارة <code dir=\"ltr\">FILTER</code> تقصر دالة تجميع واحدة على بعض الصفوف:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"sql\" data-run=\"sql\" data-index=\"2\"><div class=\"code-head\"><span class=\"code-title\">SQL</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn code-run\" data-action=\"run\" data-lang=\"sql\" data-index=\"2\" aria-label=\"شغّل هذا المثال\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><path d=\"M5 5a2 2 0 0 1 3.008-1.728l11.997 6.998a2 2 0 0 1 .003 3.458l-12 7A2 2 0 0 1 5 19z\"/></svg><span>تشغيل</span></button><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">SELECT</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">  strftime(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'%Y-%m'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, order_date) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> month</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  COUNT</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">*</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> orders,</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  COUNT</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">*</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">FILTER</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">WHERE</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> channel </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'mobile_app'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> app_orders,</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  ROUND</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">100</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">0</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> *</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> COUNT</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">*</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">FILTER</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">WHERE</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> channel </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'mobile_app'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">/</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> COUNT</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">*</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">), </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">1</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> app_pct</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">FROM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> orders</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">WHERE</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> order_date </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">>=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '2025-07-01'</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  AND</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> order_date </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">&#x3C;</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">  '2026-01-01'</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">GROUP BY</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> month</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ORDER BY</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> month</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span></code></pre><div class=\"run-output\" data-run-output=\"2\" hidden></div></div>\n<p>تتراوح حصة التطبيق بين 28% و40% على وجه التقريب، وحجم نوفمبر يزيد على ثلاثة أضعاف حجم يوليو بفضل Black Friday. هناك تفصيلان مهمّان. استخدام <code dir=\"ltr\">100.0</code> بدل <code dir=\"ltr\">100</code> يفرض القسمة العشرية؛ فـ SQLite يقسم الأعداد الصحيحة قسمة صحيحة، و<code dir=\"ltr\">47 / 129</code> تساوي 0. و<code dir=\"ltr\">FILTER</code> جزء من SQL القياسي، ويدعمه SQLite وPostgreSQL. في المحرّكات التي لا تدعمه، اكتب <code dir=\"ltr\">SUM(CASE WHEN channel = 'mobile_app' THEN 1 ELSE 0 END)</code>، وهو العدد نفسه بصيغة أطول.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">التحقق من مستوى التفصيل</h2>\n<p>حين يعمل query مجمَّع، تحقّق منه قبل أن تُبلغ به:</p>\n<ol>\n<li><strong>عدد الصفوف</strong>: هل يطابق عدد المجموعات الذي تتوقعه (4 أولويات، 6 أشهر)؟</li>\n<li><strong>التفرّد</strong>: هل مفتاح GROUP BY فريد في النتيجة؟ هو كذلك بحكم البناء، إلا إذا كان عمود مجرّد يخفي شيئًا.</li>\n<li><strong>المجاميع</strong>: هل مجموع عمود عبر المجموعات يساوي المجموع غير المجمَّع؟ في التذاكر، الأعداد الأربعة مجموعها 873.</li>\n</ol>\n<p>لا يستغرق أيٌّ من هذه الفحوص أكثر من دقيقة، ومعًا تلتقط أغلب أخطاء التجميع قبل أن يلتقطها أصحاب الطلب. اجعلها جزءًا من الحلقة التي بدأتها في الدرس الأول، بين «اكتب الـ query» و«أجب». ويصبح الفحص الثالث حيويًا في الدروس القادمة، حين تبدأ بربط الجداول. فالـ JOIN قد يغيّر مستوى التفصيل دون أن يخبرك، والجمع على مستوى تفصيل خاطئ هو بداية الحساب المزدوج.</p>\n","toc":[{"id":"sec-1","text":"صف واحد لكل ماذا؟","level":2},{"id":"sec-2","text":"WHERE مقابل HAVING","level":2},{"id":"sec-3","text":"عدّ جزء من الصفوف داخل كل مجموعة","level":2},{"id":"sec-4","text":"التحقق من مستوى التفصيل","level":2}],"takeaways":["مستوى التفصيل (grain) في النتيجة هو ما يمثّله الصف الواحد؛ قُله بصوت عالٍ قبل كتابة GROUP BY، وتحقّق منه بعدها.","كل عمود في قائمة SELECT يجب أن يكون إمّا في GROUP BY أو داخل دالة تجميع؛ يسمح SQLite بالأعمدة المجرّدة لكنه يملؤها من صف اعتباطي.","يصفّي WHERE الصفوف قبل التجميع، ويصفّي HAVING المجموعات بعده، لذا تنتمي الشروط على SUM أو COUNT إلى HAVING.","COUNT(*) FILTER (WHERE …) أو SUM(CASE WHEN … THEN 1 ELSE 0 END) يعدّ جزءًا من الصفوف داخل كل مجموعة دون query ثانٍ."],"further":[{"title":"SQLite aggregate functions","url":"https://www.sqlite.org/lang_aggfunc.html"},{"title":"SQLite SELECT processing, including bare columns","url":"https://www.sqlite.org/lang_select.html#resultset"},{"title":"PostgreSQL aggregate expressions and FILTER","url":"https://www.postgresql.org/docs/current/sql-expressions.html#SYNTAX-AGGREGATES"}],"quiz":[{"q":"<p>تحتاج إلى الدول التي يزيد عدد طلباتها في 2025 على 50. أين يذهب كل شرط؟</p>","options":[{"text":"شرط السنة و<code dir=\"ltr\">COUNT(*) &gt; 50</code> كلاهما في WHERE.","why":"يعمل WHERE قبل التجميع، حين لا يكون هناك عدد بعد، فوضع دالة تجميع فيه خطأ."},{"text":"كلاهما في HAVING.","why":"يرفض PostgreSQL وجود <code dir=\"ltr\">order_date</code> غير المجمَّع في HAVING، أما SQLite فيختبر تاريخ صف اعتباطي واحد من كل مجموعة، فتخرج الأعداد خاطئة بصمت. شرط السنة شرط على الصفوف ومكانه WHERE."},{"text":"شرط السنة في HAVING، و<code dir=\"ltr\">COUNT(*) &gt; 50</code> في WHERE.","why":"هذا معكوس. WHERE لا يرى دوال التجميع أصلًا."},{"text":"شرط السنة في WHERE، و<code dir=\"ltr\">COUNT(*) &gt; 50</code> في HAVING.","why":"صحيح. صفِّ الصفوف أولًا (طلبات 2025 فقط)، ثم صفِّ المجموعات حسب قيمة التجميع."}],"answer":3},{"q":"<p>ما مستوى التفصيل في هذه النتيجة؟</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"sql\" data-index=\"0\"><div class=\"code-head\"><span class=\"code-title\">SQL</span><span class=\"code-actions\"><button type=\"button\" class=\"code-btn\" data-action=\"copy\" aria-label=\"نسخ الكود\"><svg class=\"icon \" viewBox=\"0 0 24 24\" fill=\"none\" stroke=\"currentColor\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" aria-hidden=\"true\" focusable=\"false\"><rect width=\"14\" height=\"14\" x=\"8\" y=\"8\" rx=\"2\" ry=\"2\"/><path d=\"M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2\"/></svg><span>نسخ</span></button></span></div><pre class=\"shiki\" tabindex=\"0\"><code><span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">SELECT</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> customer_id, strftime(</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'%Y-%m'</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, order_date) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> month</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">COUNT</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">*</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">FROM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> orders</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">GROUP BY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> customer_id, </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">month</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span></code></pre></div>\n","options":[{"text":"صف واحد لكل عميل.","why":"الشهر أيضًا مفتاح تجميع، فالعميل الذي طلب في ثلاثة أشهر يحصل على ثلاثة صفوف."},{"text":"صف واحد لكل طلب.","why":"التجميع يضغط الطلبات؛ عدة طلبات للعميل نفسه في شهر واحد تصبح صفًا واحدًا."},{"text":"صف واحد لكل شهر.","why":"لكل شهر صف لكل عميل طلب فيه، لا صف واحد."},{"text":"صف واحد لكل عميل في كل شهر طلب فيه.","why":"صحيح. مستوى التفصيل هو تركيبة مفاتيح GROUP BY، والأشهر التي لا طلبات فيها لا تظهر إطلاقًا."}],"answer":3},{"q":"<p>في SQLite يعمل <code dir=\"ltr\">SELECT channel, status, COUNT(*) FROM orders GROUP BY channel</code> دون خطأ. ماذا يعرض العمود <code dir=\"ltr\">status</code>؟</p>","options":[{"text":"حالة صفٍّ ما من كل مجموعة قناة، وهي لا تخبرك بشيء عن المجموعة.","why":"صحيح. يسمح SQLite بهذا «العمود المجرّد» ويملؤه من صف اعتباطي. أما PostgreSQL فيرفض الـ query."},{"text":"الحالة الأكثر تكرارًا في كل قناة.","why":"لا يوجد حساب للمنوال هنا؛ يأخذ SQLite قيمة من صف واحد في المجموعة."},{"text":"قائمة مفصولة بفواصل لكل الحالات.","why":"هذا ما يفعله <code dir=\"ltr\">GROUP_CONCAT(status)</code> (أو <code dir=\"ltr\">string_agg</code>)؛ العمود المجرّد قيمة واحدة."}],"answer":0},{"q":"<p>أي تعبير يعطي نسبة الطلبات الملغاة من طلبات كل شهر، كنسبة مئوية؟</p>","options":[{"text":"<code dir=\"ltr\">COUNT(status = 'cancelled') * 100.0 / COUNT(*)</code>","why":"COUNT يعدّ القيم غير الـ NULL، و<code dir=\"ltr\">status = 'cancelled'</code> قيمته 0 أو 1 وليست NULL أبدًا، فيعدّ كل الصفوف ويعيد 100."},{"text":"<code dir=\"ltr\">100.0 * COUNT(*) FILTER (WHERE status = 'cancelled') / COUNT(*)</code>","why":"صحيح. يقصر FILTER دالة التجميع تلك على الصفوف الملغاة؛ و100.0 يتجنّب القسمة الصحيحة."},{"text":"<code dir=\"ltr\">COUNT(*) / COUNT(*) FILTER (WHERE status = 'cancelled')</code>","why":"هذه هي النسبة المعكوسة، ومع القسمة الصحيحة في SQLite ستُقتطع أيضًا."}],"answer":1}],"exercise":{"kind":"sql","starter":"-- Best-selling products: at least 130 units\nSELECT\n  product_id,\n  SUM(quantity) AS units,\n  ROUND(SUM(quantity * unit_price * (1 - discount)), 2) AS revenue\nFROM order_items\nORDER BY units DESC;","solution":"SELECT\n  product_id,\n  SUM(quantity) AS units,\n  ROUND(SUM(quantity * unit_price * (1 - discount)), 2) AS revenue\nFROM order_items\nGROUP BY product_id\nHAVING SUM(quantity) >= 130\nORDER BY units DESC, product_id;","orderMatters":false,"title":"المنتجات الأكثر مبيعًا لدى كريم","prompt":"<p>يريد كريم كل منتج باع <strong>130 وحدة على الأقل</strong> عبر كل سطور الطلبات (تجاهل حالة الطلب الآن).</p>\n<p>أعِد صفًا واحدًا لكل منتج فيه <code dir=\"ltr\">product_id</code> و<code dir=\"ltr\">units</code> (إجمالي الكمية) و<code dir=\"ltr\">revenue</code> (مجموع <code dir=\"ltr\">quantity * unit_price * (1 - discount)</code> مقرّبًا إلى منزلتين عشريتين). ترتيب الصفوف لا يهم.</p>\n<p>الكود المبدئي يعيد صفًا واحدًا. اجعله يعيد مستوى التفصيل الصحيح وطبّق الحدّ.</p>\n","hints":["حدّد مستوى التفصيل: صف واحد لكل منتج. أي عبارة تحقق ذلك؟","أضف <code dir=\"ltr\">GROUP BY product_id</code>. الآن يظهر كل منتج؛ وتحتاج إلى إبقاء بعض المجموعات فقط.","الشرط على <code dir=\"ltr\">SUM(quantity)</code> لا يمكن أن يكون في WHERE. استخدم <code dir=\"ltr\">HAVING SUM(quantity) &gt;= 130</code>."],"explanation":"<p>بدون GROUP BY يجمّع SQLite الجدول كله في صف واحد ويملأ العمود المجرّد <code dir=\"ltr\">product_id</code> من سطر اعتباطي، فيعرض الكود المبدئي منتجًا واحدًا بجانب إجمالي الشركة كلها. يضبط <code dir=\"ltr\">GROUP BY product_id</code> مستوى التفصيل على صف واحد لكل منتج، ويصفّي <code dir=\"ltr\">HAVING</code> هذه المجموعات بعد أن توجد المجاميع. أحد عشر منتجًا تتجاوز الحدّ. لم يبع Standing Desk 140cm (المنتج 21) إلا 132 وحدة، لكن إيراده يتجاوز 60,000، وهذا تذكير بأن عدد الوحدات والإيراد يرتّبان المنتجات بطريقتين مختلفتين جدًا.</p>\n","checkLabels":{}},"words":644,"readMinutes":4}