{"id":"l-sql-4-2","courseId":"sql-for-analysts","lang":"ar","kind":"lesson","summary":"اقرأ مخرجات EXPLAIN QUERY PLAN لتميّز المسح الكامل من البحث عبر الـ index، واكتب شروطًا تستطيع قاعدة البيانات خدمتها من index، وقدّر متى يستحق index جديد أن تطلبه.","html":"<p>لدى مارك لوحة معلومات تعرض طلبات يوم مختار، وقد أصبحت بطيئة على قاعدة البيانات الإنتاجية. «الـ query نفسه كالعادة. لماذا صار بطيئًا فجأة؟». على طلبات Cartwheel التي تبلغ ألفين، يعيد كل query في هذه الدورة نتيجته فورًا. أما بيئة الإنتاج ففيها سنوات من الطلبات، وهناك يكون الفرق بين query يقرأ كل صف وآخر يقفز إلى الصفوف الصحيحة فرقًا بين ثوانٍ ودقائق. لا يمكنك أن تشعر بهذا الفرق على قاعدة بيانات صغيرة، لكن يمكنك أن <strong>تراه</strong> في خطة التنفيذ (query plan).</p>\n<h2 id=\"sec-1\" tabindex=\"-1\">ما هو الـ index</h2>\n<p>الـ index بنية منفصلة تحفظ قيم عمود أو أكثر مرتّبة، مع مؤشّر من كل قيمة إلى صفها. إيجاد ‘2025-11-28’ في قائمة مرتّبة يشبه إيجاد كلمة في قاموس: تقفز إلى الصفحة الصحيحة على وجه التقريب بدل القراءة من البداية. بدون index، تضطر قاعدة البيانات إلى قراءة الجدول كله واختبار كل صف، وهذا هو <strong>المسح</strong> (scan).</p>\n<figure class=\"diagram\"><svg class=\"diagram-svg\" focusable=\"false\" viewBox=\"0 0 720 230\" role=\"img\" aria-labelledby=\"l-sql-4-2-t1\"><defs><marker id=\"arrow-l-sql-4-2-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-4-2-t1\">على اليسار: مسح كامل يفحص كل صفوف جدول orders واحدًا تلو الآخر. على اليمين: الـ index المسمّى idx_orders_date يحفظ التواريخ مرتّبة؛ والبحث يقفز إلى 2025-11-28، ويقرأ المدخلات المطابقة ويتبع مؤشّراتها إلى الصفوف.</title>\n  <text class=\"d-label-strong\" x=\"160\" y=\"28\" text-anchor=\"middle\">SCAN orders</text>\n  <rect class=\"d-box\" x=\"60\" y=\"40\" width=\"200\" height=\"26\" rx=\"4\"/>\n  <rect class=\"d-box\" x=\"60\" y=\"70\" width=\"200\" height=\"26\" rx=\"4\"/>\n  <rect class=\"d-box\" x=\"60\" y=\"100\" width=\"200\" height=\"26\" rx=\"4\"/>\n  <rect class=\"d-box\" x=\"60\" y=\"130\" width=\"200\" height=\"26\" rx=\"4\"/>\n  <rect class=\"d-box\" x=\"60\" y=\"160\" width=\"200\" height=\"26\" rx=\"4\"/>\n  <text class=\"d-label-muted\" x=\"160\" y=\"215\" text-anchor=\"middle\">اختبار كل صف</text>\n  <path class=\"d-arrow\" d=\"M40 45 L40 185\" marker-end=\"url(#arrow-l-sql-4-2-1)\"/>\n  <text class=\"d-label-strong\" x=\"450\" y=\"28\" text-anchor=\"middle\">SEARCH idx_orders_date</text>\n  <rect class=\"d-box\" x=\"370\" y=\"40\" width=\"160\" height=\"26\" rx=\"4\"/>\n  <text class=\"d-code\" x=\"450\" y=\"58\" text-anchor=\"middle\">2024-01-02</text>\n  <rect class=\"d-box\" x=\"370\" y=\"70\" width=\"160\" height=\"26\" rx=\"4\"/>\n  <text class=\"d-code\" x=\"450\" y=\"88\" text-anchor=\"middle\">…</text>\n  <rect class=\"d-box-success\" x=\"370\" y=\"100\" width=\"160\" height=\"26\" rx=\"4\"/>\n  <text class=\"d-code\" x=\"450\" y=\"118\" text-anchor=\"middle\">2025-11-28 08:…</text>\n  <rect class=\"d-box-success\" x=\"370\" y=\"130\" width=\"160\" height=\"26\" rx=\"4\"/>\n  <text class=\"d-code\" x=\"450\" y=\"148\" text-anchor=\"middle\">2025-11-28 23:…</text>\n  <rect class=\"d-box\" x=\"370\" y=\"160\" width=\"160\" height=\"26\" rx=\"4\"/>\n  <text class=\"d-code\" x=\"450\" y=\"178\" text-anchor=\"middle\">2025-12-30</text>\n  <path class=\"d-arrow\" d=\"M330 50 L365 108\" marker-end=\"url(#arrow-l-sql-4-2-1)\"/>\n  <rect class=\"d-box-accent\" x=\"580\" y=\"100\" width=\"120\" height=\"26\" rx=\"4\"/>\n  <text class=\"d-label\" x=\"640\" y=\"118\" text-anchor=\"middle\">الصف 2290</text>\n  <rect class=\"d-box-accent\" x=\"580\" y=\"130\" width=\"120\" height=\"26\" rx=\"4\"/>\n  <text class=\"d-label\" x=\"640\" y=\"148\" text-anchor=\"middle\">الصف 1769</text>\n  <path class=\"d-arrow\" d=\"M530 113 L576 113\" marker-end=\"url(#arrow-l-sql-4-2-1)\"/>\n  <path class=\"d-arrow\" d=\"M530 143 L576 143\" marker-end=\"url(#arrow-l-sql-4-2-1)\"/>\n  <text class=\"d-label-muted\" x=\"530\" y=\"215\" text-anchor=\"middle\">قفزة، ثم قراءة النطاق، ثم تتبّع المؤشّرات</text>\n</svg><figcaption>المسح يقرأ كل صف؛ والبحث عبر الـ index يقفز إلى النطاق المطابق</figcaption></figure>\n<p>في قاعدة بيانات Cartwheel بعض الـ indexes بالفعل. يمكنك عرضها من جدول المخطط:</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:#CF222E;--shiki-dark:#FF7B72\"> name</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">, tbl_name, </span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">sql</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">FROM</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> sqlite_schema</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">WHERE</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> type</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0A3069;--shiki-dark:#A5D6FF\"> 'index'</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\">orders</code> يوجد index على <code dir=\"ltr\">customer_id</code> وآخر على <code dir=\"ltr\">order_date</code>، وعلى <code dir=\"ltr\">order_items</code> index على <code dir=\"ltr\">order_id</code> وآخر على <code dir=\"ltr\">product_id</code>. أما المدخل <code dir=\"ltr\">sqlite_autoindex_customers_1</code> فموجود لأن <code dir=\"ltr\">email</code> معرّف كـ UNIQUE؛ ويفرض SQLite التفرّد باستخدام index.</p>\n<h2 id=\"sec-2\" tabindex=\"-1\">قراءة خطة التنفيذ</h2>\n<p>ضع <code dir=\"ltr\">EXPLAIN QUERY PLAN</code> قبل أي SELECT، فيصف SQLite كيف سينفّذه بدل أن ينفّذه. الجزء المهم هو العمود <code dir=\"ltr\">detail</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:#1F2328;--shiki-dark:#E6EDF3\">EXPLAIN QUERY PLAN</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:#CF222E;--shiki-dark:#FF7B72\">status</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-11-28'</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\">  '2025-11-29'</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><code dir=\"ltr\">SEARCH orders USING INDEX idx_orders_date (order_date&gt;? AND order_date&lt;?)</code>. تعني <strong>SEARCH</strong> أن الـ index ضيّق العمل إلى نطاق. والآن الـ query الخاص بلوحة مارك، الذي «بسّطه» أحدهم الشهر الماضي:</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:#1F2328;--shiki-dark:#E6EDF3\">EXPLAIN QUERY PLAN</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:#CF222E;--shiki-dark:#FF7B72\">status</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:#CF222E;--shiki-dark:#FF7B72\"> date</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-11-28'</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><code dir=\"ltr\">SCAN orders</code>. النتيجة نفسها، لكن الآن يُقرأ كل صف وتُحسب <code dir=\"ltr\">date()</code> لكل واحد منها. يخزّن الـ index قيم <code dir=\"ltr\">order_date</code> الخام؛ ولا يعرف شيئًا عن <code dir=\"ltr\">date(order_date)</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>إحاطة العمود الذي عليه index</span></p>\n<p>لا يستطيع الشرط استخدام index إلا حين يقف العمود المفهرس وحده في أحد طرفي المقارنة. <code dir=\"ltr\">date(order_date) = ...</code> و<code dir=\"ltr\">strftime('%Y', order_date) = '2025'</code> و<code dir=\"ltr\">customer_id + 0 = 42</code> و<code dir=\"ltr\">LOWER(email) = ...</code> كلها تخفي العمود داخل تعبير. أعِد كتابتها كنطاقات على العمود المجرّد: <code dir=\"ltr\">order_date &gt;= '2025-01-01' AND order_date &lt; '2026-01-01'</code>. الشروط التي تستطيع استخدام index تُسمّى غالبًا <strong>sargable</strong>. والنطاقات نصف المفتوحة التي تعلّمتها في درس التواريخ sargable بطبيعة تصميمها.</p>\n</aside>\n<p>هناك عبارتان أخريان في الخطط تستحقان أن تتعرّف عليهما. <code dir=\"ltr\">USING COVERING INDEX</code> تعني أن الـ index وحده احتوى على كل عمود احتاجه الـ query، فلم يُلمس الجدول إطلاقًا، وهذا أسرع أنواع البحث. و<code dir=\"ltr\">USE TEMP B-TREE FOR GROUP BY</code> أو <code dir=\"ltr\">FOR ORDER BY</code> تعني أن SQLite اضطر إلى ترتيب الصفوف بنفسه، وهذا مقبول مع النتائج الصغيرة ويستحق نظرة مع الكبيرة.</p>\n<p>خطط الـ JOIN تعرض سطرًا لكل جدول، بالترتيب الذي يزورها به SQLite:</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:#1F2328;--shiki-dark:#E6EDF3\">EXPLAIN QUERY PLAN</span></span>\n<span class=\"line\"><span style=\"color:#CF222E;--shiki-dark:#FF7B72\">SELECT</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 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:#1F2328;--shiki-dark:#E6EDF3\">) </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:#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\">customer_id</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> =</span><span style=\"color:#0550AE;--shiki-dark:#79C0FF\"> 42</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\">order_id</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\">;</span></span></code></pre><div class=\"run-output\" data-run-output=\"3\" hidden></div></div>\n<p>السطران كلاهما SEARCH: اعثر على طلبات العميل 42 عبر <code dir=\"ltr\">idx_orders_customer</code>، ثم على عناصر كل طلب عبر <code dir=\"ltr\">idx_items_order</code>. وجود index على كل مفتاح أجنبي تربط به هو أثمن عادة منفردة في الفهرسة.</p>\n<h2 id=\"sec-3\" tabindex=\"-1\">الخطط تنبؤات</h2>\n<p>خطة التنفيذ هي اختيار المُحسِّن (optimizer)، لا ضمان للسرعة، وهي تعتمد على البيانات. في جدول صغير جدًا قد يقرّر المُحسِّن أن المسح أرخص من الـ index، وسيكون محقًا. يستطيع المحرّكان كلاهما الاحتفاظ بإحصاءات عن كل جدول يجمعها الأمر <code dir=\"ltr\">ANALYZE</code> (في PostgreSQL تشغّله عملية autovacuum تلقائيًا، أما في SQLite فلا بد أن يشغّله أحد)، ويستخدمانها لتقدير عدد الصفوف التي تعيدها كل خطوة. حين تصبح هذه الإحصاءات قديمة، تخطئ الخطط بطرق مفاجئة، وهذا سؤال آخر تطرحه على صاحب قاعدة البيانات بشأن query تباطأ «بلا سبب».</p>\n<p>بعض الشروط لا تستطيع استخدام index عادي مهما كتبتها. <code dir=\"ltr\">name LIKE '%mat%'</code> يضطر إلى النظر داخل كل قيمة، لأن القائمة المرتّبة لا تساعدك في إيجاد نص في منتصف الكلمات. إذا كان بحث كهذا متكرّرًا، فالحل أداة مختلفة، مثل البحث النصي الكامل (full-text search)، لا index إضافي.</p>\n<h2 id=\"sec-4\" tabindex=\"-1\">متى تطلب index جديدًا</h2>\n<p>كثيرًا ما لا يملك المحلّلون المخطط، لكنك ستكون من يلاحظ الـ query البطيء. يصفّي فريق الدعم التذاكر حسب <code dir=\"ltr\">opened_at</code> باستمرار، ولا يوجد index على هذا العمود، فتكون خطة هذه الـ queries مسحًا (SCAN). والطلب الذي سترفعه إلى صاحب قاعدة البيانات سيكون:</p>\n<div class=\"code-block\" dir=\"ltr\" data-lang=\"sql\" data-index=\"4\"><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\">CREATE</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> INDEX</span><span style=\"color:#8250DF;--shiki-dark:#D2A8FF\"> idx_tickets_opened_at</span><span style=\"color:#CF222E;--shiki-dark:#FF7B72\"> ON</span><span style=\"color:#1F2328;--shiki-dark:#E6EDF3\"> support_tickets (opened_at);</span></span></code></pre></div>\n<p>الـ indexes ليست مجانية. كلٌّ منها يأخذ مساحة تخزين ويجب تحديثه مع كل إدراج وتحديث وحذف، فالجدول الذي عليه عشرة indexes يكتب أبطأ بشكل ملحوظ. اطلب indexes على أعمدة تصفّي عليها queries متكرّرة أو تربط بها أو ترتّب حسبها، وتضيّق النتيجة كثيرًا. الـ index على <code dir=\"ltr\">status</code>، الذي له خمس قيم فقط، نادرًا ما يساعد؛ أما الـ index على تاريخ أو معرّف فيساعد عادةً. وفي الـ index متعدّد الأعمدة، ضع أولًا العمود الذي تختبره بـ <code dir=\"ltr\">=</code> ثم عمود النطاق: <code dir=\"ltr\">(customer_id, order_date)</code> يخدم «طلبات العميل 42 في 2025» ببحث واحد.</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>الفكرة نفسها في PostgreSQL</span></p>\n<p>يعرض <code dir=\"ltr\">EXPLAIN</code> في PostgreSQL شجرة فيها تكاليف وأعداد صفوف تقديرية (“Seq Scan” هو المسح الكامل لديه، و&quot;Index Scan&quot; هو البحث)، أما <code dir=\"ltr\">EXPLAIN ANALYZE</code> فيشغّل الـ query فعلًا ويضيف الأزمنة الحقيقية. وقواعد الشروط الـ sargable والـ indexes على مفاتيح الربط تنتقل كما هي دون تغيير.</p>\n</aside>\n<p>الـ queries السريعة لا تنفع إلا إذا استطاع الناس قراءتها. في الدرس التالي: عادات الأسلوب وفروق اللهجات التي تجعل SQL الذي تكتبه قابلًا للمراجعة والنقل.</p>\n","toc":[{"id":"sec-1","text":"ما هو الـ index","level":2},{"id":"sec-2","text":"قراءة خطة التنفيذ","level":2},{"id":"sec-3","text":"الخطط تنبؤات","level":2},{"id":"sec-4","text":"متى تطلب index جديدًا","level":2}],"takeaways":["الـ index نسخة مرتّبة من عمود أو أكثر مع مؤشّرات تعود إلى الصفوف، فتستطيع قاعدة البيانات القفز إلى القيم المطابقة بدل قراءة كل صف.","في EXPLAIN QUERY PLAN تعني SEARCH … USING INDEX أن الـ index ضيّق العمل؛ وتعني SCAN أن كل صف (أو كل مدخل في الـ index) قد قُرئ.","وضع عمود عليه index داخل دالة أو عملية حسابية، مثل date(order_date)، يمنع عادةً استخدام الـ index؛ قارن العمود المجرّد بنطاق بدل ذلك.","الـ indexes تسرّع القراءة لكنها تكلّف مساحة تخزين وتبطئ كل إدراج وتحديث، لذا أضفها للشروط المتكرّرة والانتقائية ولمفاتيح الربط."],"further":[{"title":"SQLite EXPLAIN QUERY PLAN","url":"https://www.sqlite.org/eqp.html"},{"title":"SQLite query optimizer overview","url":"https://www.sqlite.org/optoverview.html"},{"title":"PostgreSQL using EXPLAIN","url":"https://www.postgresql.org/docs/current/using-explain.html"}],"quiz":[{"q":"<p>على <code dir=\"ltr\">orders</code> يوجد index على <code dir=\"ltr\">order_date</code>. أي شرط لطلبات 28 نوفمبر 2025 يستطيع استخدامه كـ SEARCH؟</p>","options":[{"text":"<code dir=\"ltr\">WHERE date(order_date) = '2025-11-28'</code>","why":"يخزّن الـ index قيم <code dir=\"ltr\">order_date</code> الخام لا <code dir=\"ltr\">date(order_date)</code>، فتضطر قاعدة البيانات إلى حساب الدالة لكل صف."},{"text":"<code dir=\"ltr\">WHERE strftime('%Y-%m-%d', order_date) = '2025-11-28'</code>","why":"المشكلة نفسها بدالة مختلفة؛ أي تعبير يحيط بالعمود يخفيه عن الـ index."},{"text":"<code dir=\"ltr\">WHERE order_date &gt;= '2025-11-28' AND order_date &lt; '2025-11-29'</code>","why":"صحيح. العمود المجرّد يُقارَن بثوابت، فتستطيع قاعدة البيانات الانتقال إلى بداية النطاق في الـ index المرتّب والتوقف عند نهايته."}],"answer":2},{"q":"<p>تعرض خطة التنفيذ <code dir=\"ltr\">SCAN orders USING COVERING INDEX idx_orders_date</code>. ماذا يعني ذلك؟</p>","options":[{"text":"قُرئ كل مدخل في الـ index، لكن الجدول نفسه لم يُلمس لأن الـ index احتوى على كل عمود احتاجه الـ query.","why":"صحيح. هذا أرخص من مسح الجدول، لأن الـ index أصغر، لكنه يبقى مرورًا كاملًا لا SEARCH موجّهًا."},{"text":"استُخدم الـ index للقفز مباشرةً إلى الصفوف المطابقة.","why":"هذا سيكون SEARCH. أما SCAN فيعني دائمًا قراءة كل شيء، وهنا الـ index كله."},{"text":"الـ query معطوب وسيعيد نتائج خاطئة.","why":"خطط التنفيذ تصف السرعة لا الصحة أبدًا؛ النتيجة واحدة في الحالتين."},{"text":"أنشأ SQLite index مؤقتًا لهذا الـ query.","why":"الـ index المؤقت يظهر في الخطة على شكل AUTOMATIC INDEX؛ أما هذا فيسمّي index دائمًا."}],"answer":0},{"q":"<p>قاعدة بيانات تحليلية تستقبل مليون جلسة ويب جديدة يوميًا، ويُستعلم منها بضع مرات يوميًا حسب <code dir=\"ltr\">source</code>. يقترح أحدهم indexes على الأعمدة التسعة كلها في <code dir=\"ltr\">web_sessions</code>. ما أفضل رد؟</p>","options":[{"text":"وافق؛ المزيد من الـ indexes يسرّع الـ queries دائمًا.","why":"الـ indexes لا تساعد إلا الـ queries التي تصفّي أو تربط أو ترتّب على أعمدتها، وكل واحد منها يضيف عملًا إلى كل إدراج."},{"text":"ارفض كل الـ indexes لأن الكتابة أهم من القراءة.","why":"هذا مطلق أكثر من اللازم. الـ index الذي يخدم شرطًا متكرّرًا وانتقائيًا يستحق عادةً تكلفة الكتابة."},{"text":"ضع index على <code dir=\"ltr\">customer_id</code> فقط، لأن المعرّفات هي دائمًا أفضل index.","why":"أفضل index هو الذي يطابق طريقة الاستعلام من الجدول. لا شيء في هذا السيناريو يصفّي حسب العميل."},{"text":"ضع indexes على الأعمدة التي تصفّي عليها queries حقيقية متكرّرة أو تربط بها، وافحص الخطط، وتجاهل الباقي.","why":"صحيح. لكل index تكلفة كتابة وتخزين، فأضف ما يخدم queries فعلية وتأكّد بـ EXPLAIN QUERY PLAN أنها مستخدمة."}],"answer":3},{"q":"<p>في PostgreSQL، ماذا يفعل <code dir=\"ltr\">EXPLAIN ANALYZE</code> ولا يفعله <code dir=\"ltr\">EXPLAIN</code>؟</p>","options":[{"text":"يعيد كتابة الـ query ليستخدم indexes أفضل.","why":"لا يغيّر أيٌّ من الأمرين الـ query؛ إنهما يقدّمان تقريرًا عنه فقط."},{"text":"يشغّل الـ query فعلًا ويعرض أعداد الصفوف والأزمنة الحقيقية بجانب التقديرات.","why":"صحيح. هذا يجعله الأداة المناسبة لإيجاد المواضع التي تخطئ فيها التقديرات، وسببًا للحذر عند تشغيله على أوامر تعدّل البيانات."},{"text":"يحلّل الـ query بحثًا عن أخطاء الصياغة فقط.","why":"الصياغة يفحصها كل أمر؛ وANALYZE يضيف التنفيذ لا التحقّق."}],"answer":1}],"exercise":{"kind":"sql","starter":"-- Orders per day over the Black Friday weekend (Fri 28 Nov to Mon 1 Dec 2025)\nSELECT\n  date(order_date) AS day,\n  COUNT(*) AS orders\nFROM orders\nWHERE order_date BETWEEN '2025-11-28' AND '2025-12-01'\nGROUP BY day\nORDER BY day;","solution":"SELECT\n  date(order_date) AS day,\n  COUNT(*) AS orders\nFROM orders\nWHERE order_date >= '2025-11-28'\n  AND order_date < '2025-12-02'\nGROUP BY day\nORDER BY day;","orderMatters":true,"title":"شرط سريع وصحيح لعطلة نهاية الأسبوع","prompt":"<p>تعرض لوحة مارك الطلبات لكل يوم في عطلة نهاية أسبوع Black Friday، <strong>من الجمعة 28 نوفمبر إلى الاثنين 1 ديسمبر 2025 شاملًا</strong> (كل الحالات).</p>\n<p>أعِد <code dir=\"ltr\">day</code> (‘YYYY-MM-DD’) و<code dir=\"ltr\">orders</code>، مرتّبة حسب <code dir=\"ltr\">day</code>. الترتيب مهم. شرط الكود المبدئي متوافق مع الـ index لكنه يُسقط يومًا كاملًا.</p>\n<p>أصلحه حتى تكون النتيجة كاملة <strong>ويبقى</strong> الشرط مستخدمًا لـ <code dir=\"ltr\">idx_orders_date</code>: شغّل الـ query الخاص بك مع <code dir=\"ltr\">EXPLAIN QUERY PLAN</code> قبله وتحقّق من أن الخطة تقول SEARCH لا SCAN.</p>\n","hints":["أي يوم مفقود من نتيجة الكود المبدئي، ولماذا يخسره BETWEEN؟","إحاطة العمود، كما في <code dir=\"ltr\">date(order_date) BETWEEN ...</code>، ستصلح العدد لكنها ستحوّل الخطة إلى SCAN.","استخدم نطاقًا نصف مفتوح على العمود المجرّد: <code dir=\"ltr\">order_date &gt;= '2025-11-28' AND order_date &lt; '2025-12-02'</code>."],"explanation":"<p>يتوقف <code dir=\"ltr\">BETWEEN '2025-11-28' AND '2025-12-01'</code> عند منتصف الليل في بداية 1 ديسمبر، فيختفي Cyber Monday. أما <code dir=\"ltr\">date(order_date) BETWEEN ...</code> فيعيد الصفوف الصحيحة لكنه يجعل SQLite يحسب <code dir=\"ltr\">date()</code> لكل طلب، أي مسحًا كاملًا. النطاق نصف المفتوح يُبقي العمود المفهرس مجرّدًا في أحد طرفي كل مقارنة، فتقول الخطة <code dir=\"ltr\">SEARCH orders USING COVERING INDEX idx_orders_date</code>، ويشمل يوم الاثنين كله: 15 و12 و11 و4 طلبات.</p>\n","checkLabels":{}},"words":799,"readMinutes":5}