{"id":"l-sql-4-3","courseId":"sql-for-analysts","lang":"ar","kind":"lesson","summary":"نسّق الـ queries بحيث يستطيع المراجع فحصها بسرعة، وانقل عادات SQLite التي تعلّمتها في هذه الدورة إلى PostgreSQL بمعرفة مواضع الاختلاف في التواريخ والقيم المنطقية وLIKE وقواعد التجميع.","html":"<p>يارا، المحلّلة الأقدم في فريقك، تراجع كل query قبل أن يصل إلى صاحب الطلب. نادرًا ما يكون تعليقها الأول على عمل المحلّلين الجدد عن المنطق. إنه «لا أستطيع فحص هذا بسرعة». أما تعليقها الثاني هذا الربع فعملي: Cartwheel تنقل تقاريرها إلى مستودع بيانات على PostgreSQL، وكل query كتبته يجب أن ينجو من هذا الانتقال. يغطي هذا الدرس الأمرين: SQL يستطيع المراجعون قراءته، وSQL ينتقل بين المحرّكات.</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">query على شخص آخر أن يفحصه</h2>\n<p>هذا 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 style=\"color:#0550AE;--shiki-dark:#79C0FF\"> t</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 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\">avg</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">(satisfaction) </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">from</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> support_tickets t,customers c </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">where</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> t</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">customer_id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">=</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">c</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">customer_id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> and</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> closed_at </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">is not null</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> and</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> opened_at</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">>=</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\">'2025-01-01'</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> group by</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 1</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> order by</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 3</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> desc</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>على المراجع أن يجتهد ليجيب عن أسئلة أساسية. لماذا <code dir=\"ltr\">customers</code> هنا؟ (إنه غير مستخدم؛ بقي من نسخة سابقة، وهو الآن JOIN على أحدهم أن يفكّر فيه.) هل هذا شرط ربط أم شرط تصفية؟ إلى أي جدول ينتمي <code dir=\"ltr\">satisfaction</code>؟ ما العمود 3؟ هل ينقص شرط 2025 حدٌّ أعلى أم أن ذلك مقصود؟ وهذا التحليل نفسه مكتوبًا ليُفحص:</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:#6E7781;--shiki-dark:#8B949E\">-- Support satisfaction by contact channel, tickets opened in 2025.</span></span>\n<span class=\"line\"><span style=\"color:#6E7781;--shiki-dark:#8B949E\">-- grain: one row per channel. Only closed tickets: open ones can't be rated yet.</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">SELECT</span></span>\n<span class=\"line\"><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">  t</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\"> closed_tickets,</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:#0550AE;--shiki-dark:#79C0FF\">t</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">satisfaction</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\"> rated,</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\">(</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">t</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">satisfaction</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\"> avg_satisfaction</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 style=\"color:#CF222E;--shiki-dark:#FF7B72\">AS</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> t</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">WHERE</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> t</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">closed_at</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> IS NOT NULL</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">  AND</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> t</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">opened_at</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\"> t</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">opened_at</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\"> t</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">.</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\">channel</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">ORDER BY</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> avg_satisfaction </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">DESC</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>التغييرات، مرتّبة حسب أهميتها:</p>\n<ol>\n<li><strong>احذف ما لا يُستخدم.</strong> اختفى الـ JOIN غير المستخدم. كل جدول في FROM احتمال fan-out أو شرط تصفية متنكّر، فعليه أن يستحق مكانه.</li>\n<li><strong>ربط صريح.</strong> حين تربط فعلًا، اكتب <code dir=\"ltr\">JOIN ... ON</code>، ولا تكتب أبدًا فاصلة مع شرط في WHERE. عندها يقع شرط الربط بجانب الجدول الذي ينتمي إليه.</li>\n<li><strong>الأسماء بدل المواضع.</strong> لكل عمود في النتيجة اسم، ويستخدمه ORDER BY. <code dir=\"ltr\">GROUP BY 1</code> تسهيل مختلَف عليه؛ أما الأسماء فتصمد أمام تعديلات قائمة SELECT.</li>\n<li><strong>عمود واحد في كل سطر، منسوب إلى اسم مستعار حسب الدور.</strong> فتصبح الفروق في مراجعة الكود سطرًا واحدًا لكل تغيير.</li>\n<li><strong>تعليقات تقول لماذا.</strong> يذكر التعليق في رأس الـ query السؤال ومستوى التفصيل؛ و«التذاكر المغلقة فقط» تشرح قرارًا. لا تعلّق على <em>ما</em> يقوله الـ SQL بوضوح.</li>\n<li><strong>اعرض المقام.</strong> يقع <code dir=\"ltr\">rated</code> بجانب المتوسط، وهو درس معدل الاستجابة من القسم 1.</li>\n</ol>\n<p>الكلمات المفتاحية بأحرف كبيرة، والفواصل في بداية السطر أو نهايته، وعرض المسافات البادئة، كلها أعراف فريق. اختر واحدًا، وطبّقه في كل مكان، ودع أداة تنسيق تفرضه. الاتساق أهم بكثير من الأسلوب الذي يفوز.</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>ابنِ الـ queries الطويلة كخطوات CTE</span></p>\n<p>لأي شيء أطول من شاشة واحدة، اكتب CTE لكل خطوة منطقية، وضع تعليق <code dir=\"ltr\">-- grain:</code> على كلٍّ منها، وأبقِ الـ SELECT الأخير قصيرًا. عندها يستطيع المراجع تشغيل كل خطوة وحدها، تمامًا كما فعلت أنت أثناء كتابتها.</p>\n</aside>\n<h2 id=\"sec-2\" tabindex=\"-1\">من SQLite إلى PostgreSQL</h2>\n<p>SQL معيار، لكن لكل محرّك دواله الخاصة ودرجة تساهله الخاصة. كل ما في هذه الدورة في الغالب يعمل دون تغيير في PostgreSQL: الـ JOIN، والـ CTEs، والـ CTEs التكرارية، ودوال النافذة، و<code dir=\"ltr\">FILTER</code>، و<code dir=\"ltr\">NULLIF</code>، و<code dir=\"ltr\">COALESCE</code>، و<code dir=\"ltr\">IS DISTINCT FROM</code>. وتتجمّع الفروق في مواضع قليلة:</p>\n<div class=\"table-wrap\" role=\"region\" aria-label=\"جدول قابل للتمرير\" tabindex=\"0\"><table><thead>\n<tr>\n<th>المهمة</th>\n<th>SQLite</th>\n<th>PostgreSQL</th>\n</tr>\n</thead>\n<tbody>\n<tr>\n<td>تسمية الشهر</td>\n<td><code dir=\"ltr\">strftime('%Y-%m', d)</code></td>\n<td><code dir=\"ltr\">to_char(d, 'YYYY-MM')</code></td>\n</tr>\n<tr>\n<td>بداية الشهر</td>\n<td><code dir=\"ltr\">date(d, 'start of month')</code></td>\n<td><code dir=\"ltr\">date_trunc('month', d)</code></td>\n</tr>\n<tr>\n<td>إضافة فترة زمنية</td>\n<td><code dir=\"ltr\">date(d, '+7 days')</code></td>\n<td><code dir=\"ltr\">d + INTERVAL '7 days'</code></td>\n</tr>\n<tr>\n<td>الساعات بين لحظتين</td>\n<td><code dir=\"ltr\">(julianday(b) - julianday(a)) * 24</code></td>\n<td><code dir=\"ltr\">EXTRACT(EPOCH FROM b - a) / 3600</code></td>\n</tr>\n<tr>\n<td>مطابقة غير حسّاسة لحالة الأحرف</td>\n<td><code dir=\"ltr\">LIKE</code> (ASCII)</td>\n<td><code dir=\"ltr\">ILIKE</code></td>\n</tr>\n<tr>\n<td>الاختيار بين احتمالين</td>\n<td><code dir=\"ltr\">IIF(c, a, b)</code> أو <code dir=\"ltr\">CASE</code></td>\n<td><code dir=\"ltr\">CASE</code></td>\n</tr>\n<tr>\n<td>عدّ شرط</td>\n<td><code dir=\"ltr\">SUM(cond)</code> أو <code dir=\"ltr\">FILTER</code></td>\n<td><code dir=\"ltr\">COUNT(*) FILTER (WHERE cond)</code></td>\n</tr>\n</tbody>\n</table></div><p>التغيير الأكبر يقع تحت الجدول. لدى PostgreSQL نوعا <code dir=\"ltr\">date</code> و<code dir=\"ltr\">timestamp</code> حقيقيان، فتُقارَن أعمدة التاريخ كتواريخ، وتعيد العمليات الحسابية على التواريخ فترات زمنية (intervals) لا أرقامًا. والقيم المنطقية نوع حقيقي أيضًا: <code dir=\"ltr\">SUM(status = 'cancelled')</code> يفشل هناك لأنك لا تستطيع جمع قيمة منطقية، ولهذا يكون FILTER هو الخيار القابل للنقل.</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>يقبل SQLite الأعمدة المجرّدة في queries التجميع، ويطابق <code dir=\"ltr\">LIKE</code> دون اعتبار لحالة الأحرف، ويسمح لك بمقارنة النص بالأرقام. أما PostgreSQL فيرفض كل حالة من هذه أو يتصرّف فيها بشكل مختلف. هذا خبر جيد في معظمه، لأن المحرّك الصارم يحوّل الأخطاء الصامتة إلى أخطاء ظاهرة. لكنه يعني أن «نُفّذ في SQLite» لا يعني «إنه صحيح». إذا اعتمد query على سلوك متساهل، فأعِد كتابته صراحةً: أضف العمود إلى GROUP BY، واستخدم <code dir=\"ltr\">LOWER()</code> في الطرفين، وحوّل الأنواع عن قصد.</p>\n</aside>\n<p>فرقان أصغر يوقعان الناس باستمرار. القسمة الصحيحة تقتطع الكسور في المحرّكين (<code dir=\"ltr\">7 / 2</code> تساوي 3)، فاستمر في كتابة <code dir=\"ltr\">100.0 *</code> للنسب المئوية. ويضع PostgreSQL قيم NULL أخيرًا في الترتيب التصاعدي بينما يضعها SQLite أولًا، فأضف <code dir=\"ltr\">NULLS FIRST</code> أو <code dir=\"ltr\">NULLS LAST</code> كلما كان موضع NULL مهمًا.</p>\n<p>التمرين مهمة نقل حقيقية: query من مستودع البيانات يفشل في SQLite. بعد ذلك، يجمع المشروع الختامي الدورة كلها.</p>\n","toc":[{"id":"sec-1","text":"query على شخص آخر أن يفحصه","level":2},{"id":"sec-2","text":"من SQLite إلى PostgreSQL","level":2}],"takeaways":["الـ SQL المقروء يستخدم JOIN … ON صريحًا، وعمودًا واحدًا في كل سطر، وأسماء مستعارة حسب الأدوار، وخطوات CTE مسمّاة، وتعليقات تشرح لماذا لا ماذا.","احذف ما لا يستخدمه الـ query، مثل عمليات الربط غير المستخدمة وSELECT *، لأن كل جدول إضافي احتمال fan-out وسؤال على المراجع.","التعامل مع التواريخ هو أكبر فرق بين SQLite وPostgreSQL؛ فـ strftime وjulianday تصبحان date_trunc وto_char وفترات زمنية (intervals) وأنواع تاريخ حقيقية.","SQLite أكثر تساهلًا من PostgreSQL (الأعمدة المجرّدة، وLIKE غير الحسّاس لحالة الأحرف، والقيم المنطقية كـ 0/1)، فقد يفشل query يعمل في SQLite أو يتصرّف بشكل مختلف في PostgreSQL."],"further":[{"title":"SQLite quirks, caveats and gotchas","url":"https://www.sqlite.org/quirks.html"},{"title":"PostgreSQL date/time functions and operators","url":"https://www.postgresql.org/docs/current/functions-datetime.html"},{"title":"PostgreSQL pattern matching (LIKE and ILIKE)","url":"https://www.postgresql.org/docs/current/functions-matching.html"}],"quiz":[{"q":"<p>تظهر العبارة <code dir=\"ltr\">FROM support_tickets t, customers c WHERE t.customer_id = c.customer_id</code> في query لا يستخدم أي عمود من <code dir=\"ltr\">c.</code> إطلاقًا. ماذا يجب أن يطلب المراجع؟</p>","options":[{"text":"أعِد كتابتها كـ LEFT JOIN حتى لا تضيع أي تذكرة.","why":"تغيير نوع الـ JOIN يُبقي الجدول غير المستخدم؛ المشكلة في وجوده أصلًا."},{"text":"احذف جدول العملاء؛ فهو لا يضيف شيئًا سوى مصدر محتمل لـ fan-out أو لفقدان صفوف.","why":"صحيح. كل جدول في FROM شيء يجب على القارئ التفكير فيه. إذا لم يُستخدم أي عمود منه ولم يكن يصفّي عن قصد، فاحذفه."},{"text":"أضف DISTINCT إلى الـ SELECT احتياطًا.","why":"DISTINCT يغطّي التكرار بدل إزالة سببه، وقد يدمج هنا أيضًا صفوفًا حقيقية."},{"text":"لا شيء؛ الربط بالفاصلة SQL قياسي ولا بأس بإبقائه.","why":"الربط بالفاصلة مقبول، لكنه يخفي شرط الربط في WHERE، حيث يسهل نسيانه. الصيغة المقروءة هي JOIN … ON الصريح."}],"answer":1},{"q":"<p>أي تعبير في SQLite له مقابل مباشر في PostgreSQL هو <code dir=\"ltr\">date_trunc('month', order_date)</code>؟</p>","options":[{"text":"<code dir=\"ltr\">julianday(order_date)</code>","why":"تحوّل julianday التاريخ إلى رقم يوم؛ ولا تقتطعه إلى الشهر."},{"text":"<code dir=\"ltr\">strftime('%w', order_date)</code>","why":"هذا رقم يوم الأسبوع، لا بداية الشهر."},{"text":"<code dir=\"ltr\">date(order_date, '+1 month')</code>","why":"هذا يضيف شهرًا؛ ومقابله في PostgreSQL هو <code dir=\"ltr\">order_date + INTERVAL '1 month'</code>."},{"text":"<code dir=\"ltr\">date(order_date, 'start of month')</code>","why":"صحيح. كلاهما يعيد اليوم الأول من شهر الطلب. يعيد PostgreSQL قيمة timestamp، ويعيد SQLite نصًا بالصيغة ‘YYYY-MM-01’."}],"answer":3},{"q":"<p>هذا يعمل في SQLite: <code dir=\"ltr\">SELECT channel, status, COUNT(*) FROM orders GROUP BY channel</code>. ماذا يحدث في PostgreSQL؟</p>","options":[{"text":"يفشل، لأن <code dir=\"ltr\">status</code> ليس ضمن التجميع ولا داخل دالة تجميع.","why":"صحيح. يفرض PostgreSQL قاعدة GROUP BY التي يتساهل فيها SQLite، فيصبح خطأ العمود المجرّد خطأً لا يمكن أن يفوتك."},{"text":"يعيد النتيجة نفسها التي يعيدها SQLite.","why":"يرفض PostgreSQL اختيار قيمة اعتباطية لعمود مجرّد."},{"text":"يعيد صفًا لكل قناة وحالة.","why":"هذا يحتاج إلى <code dir=\"ltr\">GROUP BY channel, status</code>؛ ولا يضيف PostgreSQL أعمدة التجميع نيابةً عنك."}],"answer":0},{"q":"<p>يجد <code dir=\"ltr\">WHERE email LIKE '%@EXAMPLE.COM'</code> كل العملاء الـ 600 في SQLite. كم عميلًا يجد في PostgreSQL، حيث تُخزَّن عناوين البريد بأحرف صغيرة؟</p>","options":[{"text":"600، فـ LIKE يتصرّف بالطريقة نفسها في كل مكان","why":"حساسية LIKE لحالة الأحرف من أكثر فروق اللهجات شيوعًا."},{"text":"خطأ، لأن LIKE يحتاج إلى ILIKE في PostgreSQL","why":"لدى PostgreSQL المعامل LIKE أيضًا؛ لكنه حسّاس لحالة الأحرف ببساطة."},{"text":"0، لأن LIKE في PostgreSQL حسّاس لحالة الأحرف؛ استخدم ILIKE أو قارن LOWER(email)","why":"صحيح. LIKE في SQLite يتجاهل حالة الأحرف الإنجليزية افتراضيًا، أما في PostgreSQL فلا. وILIKE هو النسخة غير الحسّاسة لحالة الأحرف في PostgreSQL."}],"answer":2}],"exercise":{"kind":"sql","starter":"-- Pasted from the PostgreSQL warehouse. It fails in SQLite: port it.\n-- grain: one row per month, Dubai customers, 2025, non-cancelled orders\nSELECT\n  to_char(date_trunc('month', o.order_date::timestamp), 'YYYY-MM') AS month,\n  COUNT(*) AS orders,\n  COUNT(*) FILTER (WHERE o.channel ILIKE 'MOBILE%') AS app_orders\nFROM orders AS o\nJOIN customers AS c ON c.customer_id = o.customer_id\nWHERE c.city ILIKE 'dubai'\n  AND o.status <> 'cancelled'\n  AND o.order_date >= DATE '2025-01-01'\n  AND o.order_date < DATE '2026-01-01'\nGROUP BY month\nORDER BY month;","solution":"SELECT\n  strftime('%Y-%m', o.order_date) AS month,\n  COUNT(*) AS orders,\n  COUNT(*) FILTER (WHERE o.channel LIKE 'MOBILE%') AS app_orders\nFROM orders AS o\nJOIN customers AS c ON c.customer_id = o.customer_id\nWHERE c.city LIKE 'dubai'\n  AND o.status <> 'cancelled'\n  AND o.order_date >= '2025-01-01'\n  AND o.order_date < '2026-01-01'\nGROUP BY month\nORDER BY month;","orderMatters":true,"title":"انقل query من مستودع البيانات إلى SQLite","prompt":"<p>وجدت هدى هذا الـ query في مستودع البيانات على PostgreSQL وتريد الأرقام نفسها من قاعدة بيانات SQLite الخاصة بـ Cartwheel: للعملاء في <strong>دبي</strong>، صف واحد لكل شهر في <strong>2025</strong> فيه <code dir=\"ltr\">month</code> (‘YYYY-MM’) و<code dir=\"ltr\">orders</code> (الطلبات غير الملغاة) و<code dir=\"ltr\">app_orders</code> (الطلبات التي قُدّمت عبر تطبيق الجوال). رتّب حسب <code dir=\"ltr\">month</code>. الترتيب مهم.</p>\n<p>الكود المبدئي يفشل في SQLite. ترجم كل جزء خاص بـ PostgreSQL إلى SQLite وأبقِ المنطق كما هو.</p>\n","hints":["شغّله واقرأ أول خطأ. ثم ابحث عن كل ميزة خاصة بـ PostgreSQL: تحويلات <code dir=\"ltr\">::</code>، و<code dir=\"ltr\">to_char</code>، و<code dir=\"ltr\">date_trunc</code>، و<code dir=\"ltr\">ILIKE</code>، والقيم الحرفية <code dir=\"ltr\">DATE '...'</code>.","تسمية الشهر بالصيغة ‘YYYY-MM’ في SQLite هي <code dir=\"ltr\">strftime('%Y-%m', o.order_date)</code>. التواريخ نصوص ISO، فتصبح القيم الحرفية للتاريخ نصوصًا عادية.","LIKE في SQLite لا يميّز حالة الأحرف الإنجليزية أصلًا، فيصبح <code dir=\"ltr\">ILIKE</code> هو <code dir=\"ltr\">LIKE</code>. أما <code dir=\"ltr\">COUNT(*) FILTER (WHERE ...)</code> فيعمل في المحرّكين ويمكن أن يبقى."],"explanation":"<p>أربعة أشياء خاصة بـ PostgreSQL: التحويل <code dir=\"ltr\">::timestamp</code>، و<code dir=\"ltr\">to_char</code> مع <code dir=\"ltr\">date_trunc</code>، و<code dir=\"ltr\">ILIKE</code>، والقيم الحرفية ذات النوع <code dir=\"ltr\">DATE '...'</code>. في SQLite تسمية الشهر هي <code dir=\"ltr\">strftime('%Y-%m', ...)</code>، ونص ISO لا يحتاج إلى تحويل، وLIKE يتجاهل حالة الأحرف الإنجليزية أصلًا. و<code dir=\"ltr\">FILTER</code> جزء من SQL القياسي ويعمل دون تغيير. لاحظ الفخ المعاكس إن عاد هذا الـ query يومًا: <code dir=\"ltr\">LIKE 'MOBILE%'</code> في SQLite يطابق ‘mobile_app’، لكن LIKE نفسه في PostgreSQL لن يطابق شيئًا، ولهذا استخدم الأصل ILIKE. كان ديسمبر أكثر أشهر دبي ازدحامًا، بـ 23 طلبًا.</p>\n","checkLabels":{}},"words":662,"readMinutes":4}