{"id":"l-sql-2-3","courseId":"sql-for-analysts","lang":"ar","kind":"lesson","summary":"تعرّف على اللحظة التي يضاعف فيها الـ JOIN الصفوف، واشرح لماذا تتضخّم المجاميع على الجانب المتكرّر، وأصلح ذلك بتجميع كل جدول إلى مستوى التفصيل الصحيح قبل الربط.","html":"<p>يرسل كريم من المالية سؤالًا سريعًا: «في 2025، حسب القناة، كم جمعنا من رسوم الشحن، وكم من إيراد العناصر؟». أنت تعرف الـ JOIN المطلوب. وهذا هو الـ 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:#0550AE;--shiki-dark:#79C0FF\">  o</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">channel</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">,</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\">SUM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">o</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">shipping_fee</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">), </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</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\"> shipping,</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\">SUM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">oi</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">quantity</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> *</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> oi</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">unit_price</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:#CF222E;--shiki-dark:#FF7B72\"> -</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> oi</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">discount</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">)), </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</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\"> item_revenue</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 style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> o</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">JOIN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> order_items </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> oi </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ON</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> oi</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">order_id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> o</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">order_id</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">WHERE</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> o</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">status</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> &#x3C;></span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'cancelled'</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  AND</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> o</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">order_date</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> >=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '2025-01-01'</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  AND</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> o</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">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:#0550AE;--shiki-dark:#79C0FF\"> o</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">channel</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span></code></pre><div class=\"run-output\" data-run-output=\"0\" hidden></div></div>\n<p>إيراد العناصر صحيح. أما الشحن فيقارب ضعف ما يجب: يعرض الويب 6,732 بينما الرقم الحقيقي 3,413. لم يظهر أي خطأ، ولا شيء يبدو سخيفًا من النظرة الأولى. هذا هو <strong>الـ fan-out</strong> (تضاعف الصفوف)، أغلى خطأ في التحليل ثمنًا، لأنه ينتج أرقامًا قابلة للتصديق.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">من أين يأتي المال الإضافي</h2>\n<p>بين <code dir=\"ltr\">orders</code> و<code dir=\"ltr\">order_items</code> علاقة واحد-إلى-متعدد: طلب واحد، وسطر واحد أو أكثر. حين تربطهما، يكون في النتيجة صف لكل <em>سطر</em>، ويحمل كل سطر نسخة من أعمدة طلبه، بما فيها <code dir=\"ltr\">shipping_fee</code>.</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 720 230\" role=\"img\" aria-labelledby=\"l-sql-2-3-t1\"><defs><marker id=\"arrow-l-sql-2-3-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-3-t1\">الطلب 1001 برسوم شحن 7.99 يرتبط بأربعة سطور عناصر. في النتيجة بعد الربط أربعة صفوف، يعرض كلٌّ منها 7.99، فيعطي جمع الرسوم 31.96 بدل 7.99.</title>\n  <text class=\"d-label-strong\" x=\"110\" y=\"28\" text-anchor=\"middle\">orders (واحد)</text>\n  <rect class=\"d-box-primary\" x=\"30\" y=\"90\" width=\"160\" height=\"44\" rx=\"8\"/>\n  <text class=\"d-code\" x=\"110\" y=\"117\" text-anchor=\"middle\">1001  fee 7.99</text>\n  <text class=\"d-label-strong\" x=\"350\" y=\"28\" text-anchor=\"middle\">order_items (متعدد)</text>\n  <rect class=\"d-box\" x=\"280\" y=\"40\" width=\"140\" height=\"32\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"350\" y=\"61\" text-anchor=\"middle\">line 2</text>\n  <rect class=\"d-box\" x=\"280\" y=\"80\" width=\"140\" height=\"32\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"350\" y=\"101\" text-anchor=\"middle\">line 3</text>\n  <rect class=\"d-box\" x=\"280\" y=\"120\" width=\"140\" height=\"32\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"350\" y=\"141\" text-anchor=\"middle\">line 4</text>\n  <rect class=\"d-box\" x=\"280\" y=\"160\" width=\"140\" height=\"32\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"350\" y=\"181\" text-anchor=\"middle\">line 5</text>\n  <path class=\"d-line\" d=\"M190 112 L280 56\"/>\n  <path class=\"d-line\" d=\"M190 112 L280 96\"/>\n  <path class=\"d-line\" d=\"M190 112 L280 136\"/>\n  <path class=\"d-line\" d=\"M190 112 L280 176\"/>\n  <text class=\"d-label-strong\" x=\"590\" y=\"28\" text-anchor=\"middle\">الصفوف بعد الربط</text>\n  <rect class=\"d-box-warn\" x=\"500\" y=\"40\" width=\"180\" height=\"32\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"590\" y=\"61\" text-anchor=\"middle\">1001 7.99 line 2</text>\n  <rect class=\"d-box-warn\" x=\"500\" y=\"80\" width=\"180\" height=\"32\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"590\" y=\"101\" text-anchor=\"middle\">1001 7.99 line 3</text>\n  <rect class=\"d-box-warn\" x=\"500\" y=\"120\" width=\"180\" height=\"32\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"590\" y=\"141\" text-anchor=\"middle\">1001 7.99 line 4</text>\n  <rect class=\"d-box-warn\" x=\"500\" y=\"160\" width=\"180\" height=\"32\" rx=\"6\"/>\n  <text class=\"d-code\" x=\"590\" y=\"181\" text-anchor=\"middle\">1001 7.99 line 5</text>\n  <text class=\"d-label\" x=\"590\" y=\"220\" text-anchor=\"middle\">SUM(fee) = 31.96</text>\n  <path class=\"d-arrow\" d=\"M425 116 L495 116\" marker-end=\"url(#arrow-l-sql-2-3-1)\"/>\n</svg><figcaption>ربط طلب واحد بأربعة عناصر يكرّر رسومه أربع مرات</figcaption></figure>\n<p>نجا إيراد العناصر لأنه يقع في جانب «المتعدد»: إيراد كل سطر يظهر مرة واحدة بالضبط. أما الشحن فيقع في جانب «الواحد»، فيُحسب مرة لكل سطر. متوسط عدد السطور في طلبات Cartwheel هو 1.93، ولهذا تضاعف الشحن تقريبًا.</p>\n<p>القاعدة العامة: <strong>الـ JOIN يغيّر مستوى التفصيل إلى مستوى تفصيل أكثر جداوله تفصيلًا، وأي رقم مخزّن بمستوى تفصيل أخشن يتكرّر.</strong></p>\n<h2 id=\"sec-2\" tabindex=\"-1\">الإصلاح: جمّع أولًا، ثم اربط</h2>\n<p>أوصل كل جدول إلى مستوى تفصيل الإجابة <em>قبل</em> الربط. هنا، اضغط <code dir=\"ltr\">order_items</code> في صف واحد لكل طلب داخل subquery، ثم اربط النتيجة بـ <code dir=\"ltr\">orders</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:#0550AE;--shiki-dark:#79C0FF\">  o</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">channel</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\">  ROUND</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">SUM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">o</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">shipping_fee</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">), </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</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\"> shipping,</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\">SUM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">i</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">revenue</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">), </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</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\"> item_revenue</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 style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> o</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">JOIN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  SELECT</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> order_id, </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:#1F2328;--shiki-dark:#E6EDF3\"> unit_price </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:#CF222E;--shiki-dark:#FF7B72\"> -</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> discount)) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> revenue</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\">  GROUP BY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> order_id</span></span>\n<span class=\"line\"><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> i </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ON</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> i</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">order_id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> o</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">order_id</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">WHERE</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> o</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">status</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> &#x3C;></span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'cancelled'</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  AND</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> o</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">order_date</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> >=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> '2025-01-01'</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  AND</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> o</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">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:#0550AE;--shiki-dark:#79C0FF\"> o</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">channel</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>الـ subquery داخل <code dir=\"ltr\">FROM</code> (ويُسمّى <em>جدولًا مشتقًّا</em>) يتصرّف كجدول مؤقت بمستوى التفصيل الذي اخترته، وهو هنا صف واحد لكل <code dir=\"ltr\">order_id</code>. الشحن الآن يطابق query على <code dir=\"ltr\">orders</code> وحده، وإيراد العناصر لم يتغيّر، و<code dir=\"ltr\">COUNT(*)</code> يعدّ الطلبات، لأن الصفوف عادت طلبات. سيعرض القسم التالي طريقة أرتب لكتابة الشيء نفسه باستخدام <code dir=\"ltr\">WITH</code>.</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>SUM(DISTINCT) كإصلاح للـ fan-out</span></p>\n<p><code dir=\"ltr\">COUNT(DISTINCT o.order_id)</code> طريقة مشروعة لعدّ الطلبات بعد fan-out، لأن معرّفات الطلبات فريدة. أما <code dir=\"ltr\">SUM(DISTINCT o.shipping_fee)</code> فليس كذلك: إنه يجمع كل <em>مبلغ</em> مختلف مرة واحدة. مئات الطلبات تتشارك الرسوم نفسها 4.99، فيعيد على بيانات Cartwheel القيمة 22.97 لكل قناة، وهي مجموع بضعة مستويات للرسوم. DISTINCT يزيل تكرار القيم، لا تكرار الصفوف أبدًا.</p>\n</aside>\n<h2 id=\"sec-3\" tabindex=\"-1\">تضاعفان في آن واحد</h2>\n<p>الـ fan-out يتراكم. تسأل جوليا كم إيرادًا جاء من طلبات احتاجت إلى تذكرة دعم. اربط العناصر والتذاكر بالطلبات نفسها، فيتكرّر كل سطر مرة لكل تذكرة على طلبه؛ و110 طلبات لديها أكثر من تذكرة واحدة. المجموع الساذج يقول 166,562. أما الإجابة الصحيحة، التي تسأل هل <em>توجد</em> تذكرة بدل ربط كل تذكرة، فهي 140,675:</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 style=\"color:#0550AE;--shiki-dark:#79C0FF\"> ROUND</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(</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:#1F2328;--shiki-dark:#E6EDF3\"> unit_price </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:#CF222E;--shiki-dark:#FF7B72\"> -</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> discount)), </span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">2</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\"> revenue_with_tickets</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\"> order_id </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">IN</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> (</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">SELECT</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> order_id </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">FROM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> support_tickets);</span></span></code></pre><div class=\"run-output\" data-run-output=\"2\" hidden></div></div>\n<p><code dir=\"ltr\">IN (subquery)</code> يصفّي دون ربط، فلا يمكن أن يضاعف الصفوف أبدًا. هذا النمط، «هل يوجد تطابق؟»، ويُسمّى semi-join، يحظى بشرح كامل في الدرس التالي مع نقيضه.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">اكتشاف الـ fan-out قبل أن يصيبك</h2>\n<p>لا داعي لأن تنتظر مجموعًا خاطئًا. قبل الربط، اسأل عن كل مفتاح ربط: هل هو فريد في هذا الجانب؟ <code dir=\"ltr\">order_id</code> فريد في <code dir=\"ltr\">orders</code> (فهو المفتاح الأساسي) لكنه ليس فريدًا في <code dir=\"ltr\">order_items</code> ولا في <code dir=\"ltr\">support_tickets</code>، فربط أيٍّ منهما بـ <code dir=\"ltr\">orders</code> يضاعف صفوف الطلبات. فحص سريع يخبرك بذلك:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"sql\" data-run=\"sql\" data-index=\"3\"><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=\"3\" 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:#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\"> rows_after_join,</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\"> distinct_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></code></pre><div class=\"run-output\" data-run-output=\"3\" hidden></div></div>\n<p>3,991 صفًا مقابل 2,066 طلبًا: معامل تضاعف يقارب الاثنين. حين يختلف هذان الرقمان، فأي عمود تجمعه من جانب «الواحد» يحتاج إلى تجميع مسبق أو إلى إبعاده عن الـ JOIN. وحين يتساويان، يكون الـ JOIN واحد-إلى-واحد وآمنًا للجمع من أي جانب.</p>\n<h2 id=\"sec-5\" tabindex=\"-1\">عادة المطابقة</h2>\n<p>في كل مرة تربط فيها جداول، طابِق مجموعًا واحدًا على الأقل مع نسخة من جدول واحد. الشحن حسب القناة من الـ query المربوط يجب أن يساوي الشحن حسب القناة من <code dir=\"ltr\">orders</code> وحده؛ وإيراد العناصر يجب أن يساوي المجموع على <code dir=\"ltr\">order_items</code> للطلبات نفسها. إذا تطابقا، فالـ JOIN لم يضاعف شيئًا. وإذا لم يتطابقا، فقد أمسكت بالخطأ قبل أن يصل إلى عرض كريم أمام مجلس الإدارة.</p>\n<aside class=\"callout callout-tip\" 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=\"M15 14c.2-1 .7-1.7 1.5-2.5 1-.9 1.5-2.2 1.5-3.5A6 6 0 0 0 6 8c0 1 .2 2.2 1.5 3.5.7.7 1.3 1.5 1.5 2.5\"/><path d=\"M9 18h6\"/><path d=\"M10 22h4\"/></svg><span><span class=\"callout-kind\">نصيحة</span><span class=\"callout-sep\" aria-hidden=\"true\"> · </span>سمِّ مستوى التفصيل في تعليق</span></p>\n<p>ابدأ الـ queries المجمَّعة بتعليق مثل <code dir=\"ltr\">-- grain: one row per channel</code>، وضع <code dir=\"ltr\">-- grain: one row per order</code> فوق كل جدول مشتق. يستغرق ذلك خمس ثوانٍ، ويجعل الـ fan-out مرئيًا لك ولكل من يراجع الـ query.</p>\n</aside>\n<p>في الدرس التالي ستربط الجدول بنفسه، وتبحث عن صفوف <em>لا</em> تطابق لها على الإطلاق.</p>\n","toc":[{"id":"sec-1","text":"من أين يأتي المال الإضافي","level":2},{"id":"sec-2","text":"الإصلاح: جمّع أولًا، ثم اربط","level":2},{"id":"sec-3","text":"تضاعفان في آن واحد","level":2},{"id":"sec-4","text":"اكتشاف الـ fan-out قبل أن يصيبك","level":2},{"id":"sec-5","text":"عادة المطابقة","level":2}],"takeaways":["ربط علاقة واحد-إلى-متعدد يعطي النتيجة مستوى تفصيل جانب «المتعدد»، ويكرّر كل عمود من جانب «الواحد».","جمع عمود من الجانب المتكرّر، مثل رسوم شحن الطلب بعد ربطه بالعناصر، يحسبه مرة لكل صف مطابق.","جمّع جانب «المتعدد» أولًا إلى مستوى تفصيل جانب «الواحد» داخل subquery، ثم اربط، حتى يُحسب كل رقم مرة واحدة بالضبط.","قد ينقذ COUNT(DISTINCT key) عدًّا ما، لكن SUM(DISTINCT value) ليس إصلاحًا أبدًا لأنه يدمج أيضًا صفوفًا مختلفة تتشارك القيمة نفسها.","طابِق كل مجموع بعد الـ JOIN مع المجموع نفسه محسوبًا من جدوله وحده."],"further":[{"title":"SQLite SELECT, subqueries in the FROM clause","url":"https://www.sqlite.org/lang_select.html#fromclause"},{"title":"PostgreSQL table expressions and subqueries in FROM","url":"https://www.postgresql.org/docs/current/queries-table-expressions.html"}],"quiz":[{"q":"<p>رسوم شحن الطلب 1001 هي 7.99 وفيه أربعة سطور عناصر. بعد ربط <code dir=\"ltr\">orders</code> بـ <code dir=\"ltr\">order_items</code>، كم يساهم <code dir=\"ltr\">SUM(o.shipping_fee)</code> من هذا الطلب؟</p>","options":[{"text":"7.99","why":"هذه هي الرسوم الحقيقية، لكن الـ JOIN أنتج أربعة صفوف لهذا الطلب، يحمل كلٌّ منها 7.99."},{"text":"31.96","why":"صحيح. أعمدة الطلب تتكرّر في كل صف من صفوف عناصره الأربعة، فتُجمع الرسوم أربع مرات."},{"text":"1.99، أي الرسوم مقسومة على العناصر","why":"الـ JOIN لا يقسم القيم أبدًا؛ إنه ينسخ أعمدة جانب «الواحد» إلى كل صف مطابق."}],"answer":1},{"q":"<p>يصلح زميلك مجموع شحن متضخّمًا بكتابة <code dir=\"ltr\">SUM(DISTINCT o.shipping_fee)</code>. ما الخطأ في ذلك؟</p>","options":[{"text":"لا شيء؛ DISTINCT يزيل التكرارات التي صنعها الـ JOIN.","why":"DISTINCT يزيل القيم المكرّرة لا الطلبات المكرّرة. 372 طلبًا مختلفًا تتشارك رسومًا قدرها 4.99، فتنضغط كلها في قيمة واحدة."},{"text":"SUM(DISTINCT) ليست صياغة SQL صحيحة.","why":"إنها صحيحة في SQLite وPostgreSQL، وهذا ما يجعلها خطأً صامتًا ومغريًا."},{"text":"يجمع كل مبلغ رسوم مختلف مرة واحدة، فيقترب المجموع من مجموع بضعة مستويات للرسوم.","why":"صحيح. على بيانات Cartwheel يعيد 22.97 لكل قناة، وهو مجموع مستويات الرسوم المختلفة، بدل الأرقام الحقيقية التي تتراوح بين 678 و3,413."},{"text":"إنه صحيح لكنه أبطأ من طريقة الـ subquery.","why":"ليس صحيحًا. السرعة لا تهم حين يكون الرقم خاطئًا."}],"answer":2},{"q":"<p>تحتاج إلى إيراد العناصر وعدد التذاكر لكل طلب. في <code dir=\"ltr\">order_items</code> و<code dir=\"ltr\">support_tickets</code> كليهما صفوف كثيرة لكل طلب. ما الطريقة الأكثر أمانًا؟</p>","options":[{"text":"اربط الطلبات بالجدولين في عبارة FROM واحدة واستخدم SUM وCOUNT.","why":"عمليتا ربط واحد-إلى-متعدد مستقلتان تتضاعفان معًا. فيُحسب الإيراد مرة لكل تذكرة، وتُحسب التذاكر مرة لكل عنصر."},{"text":"استخدم COUNT(DISTINCT t.ticket_id) وSUM(DISTINCT revenue).","why":"العدد سينجو، لكن SUM(DISTINCT) يدمج سطورًا مختلفة صادف أن تساوى إيرادها."},{"text":"استخدم LEFT JOIN بدل INNER JOIN للجدولين.","why":"نوع الـ JOIN يتحكّم في الصفوف التي تبقى، لا في عدد مرات تكرارها؛ الـ fan-out يحدث مع النوعين."},{"text":"جمّع العناصر لكل طلب والتذاكر لكل طلب في subqueryين، ثم اربط الاثنين بالطلبات.","why":"صحيح. كل subquery يعيد صفًا واحدًا لكل طلب، فتكون عمليات الربط النهائية واحد-إلى-واحد ولا يتكرّر شيء."}],"answer":3}],"exercise":{"kind":"sql","starter":"-- grain: one row per country (2025, non-cancelled orders)\nSELECT\n  c.country,\n  COUNT(*) AS orders,\n  ROUND(SUM(o.shipping_fee), 2) AS shipping_total,\n  ROUND(SUM(oi.quantity * oi.unit_price * (1 - oi.discount)), 2) AS item_revenue\nFROM orders AS o\nJOIN customers AS c ON c.customer_id = o.customer_id\nJOIN order_items AS oi ON oi.order_id = o.order_id\nWHERE o.status <> 'cancelled'\n  AND o.order_date >= '2025-01-01'\n  AND o.order_date < '2026-01-01'\nGROUP BY c.country;","solution":"SELECT\n  c.country,\n  COUNT(*) AS orders,\n  ROUND(SUM(o.shipping_fee), 2) AS shipping_total,\n  ROUND(SUM(i.revenue), 2) AS item_revenue\nFROM orders AS o\nJOIN customers AS c ON c.customer_id = o.customer_id\nJOIN (\n  -- grain: one row per order\n  SELECT order_id, SUM(quantity * unit_price * (1 - discount)) AS revenue\n  FROM order_items\n  GROUP BY order_id\n) AS i ON i.order_id = o.order_id\nWHERE o.status <> 'cancelled'\n  AND o.order_date >= '2025-01-01'\n  AND o.order_date < '2026-01-01'\nGROUP BY c.country;","orderMatters":false,"title":"الشحن حسب الدولة، محسوبًا مرة واحدة","prompt":"<p>يريد كريم التقسيم نفسه <strong>حسب دولة العميل</strong> لعام 2025 (الطلبات غير الملغاة): <code dir=\"ltr\">country</code>، و<code dir=\"ltr\">orders</code> (عدد الطلبات)، و<code dir=\"ltr\">shipping_total</code> (مجموع <code dir=\"ltr\">shipping_fee</code> مقرّبًا إلى منزلتين عشريتين)، و<code dir=\"ltr\">item_revenue</code> (مقرّبًا إلى منزلتين عشريتين). ترتيب الصفوف لا يهم.</p>\n<p>إيراد العناصر في الكود المبدئي صحيح، لكن أعداد الطلبات ومجاميع الشحن متضخّمة. أصلح مستوى التفصيل.</p>\n","hints":["أي جدول هو جانب «المتعدد»؟ بعد ربطه، ماذا يمثّل الصف الواحد، وكم مرة يظهر <code dir=\"ltr\">shipping_fee</code> لكل طلب؟","استبدل الربط بـ <code dir=\"ltr\">order_items</code> بربط بـ subquery يعيد صفًا واحدًا لكل <code dir=\"ltr\">order_id</code> مع إيراده.","داخل الـ subquery: <code dir=\"ltr\">SELECT order_id, SUM(quantity * unit_price * (1 - discount)) AS revenue FROM order_items GROUP BY order_id</code>. ثم اجمع <code dir=\"ltr\">i.revenue</code> خارجه."],"explanation":"<p>ربط <code dir=\"ltr\">order_items</code> يحوّل كل طلب إلى صف لكل سطر، فعدّ <code dir=\"ltr\">COUNT(*)</code> السطور، وحسب <code dir=\"ltr\">SUM(o.shipping_fee)</code> كل رسوم مرة لكل سطر. ضغط العناصر أولًا في صف واحد لكل طلب يجعل كل ربط واحد-إلى-واحد بمستوى تفصيل الطلب، فتُحسب الطلبات والرسوم مرة واحدة ويبقى إيراد العناصر كما هو. تتصدّر المملكة المتحدة بـ 241 طلبًا ونحو 58,000 من إيراد العناصر. سيصلح <code dir=\"ltr\">COUNT(DISTINCT o.order_id)</code> العدد أيضًا، لكن لا توجد حيلة DISTINCT تصلح مجموع الشحن.</p>\n","checkLabels":{}},"words":629,"readMinutes":4}