LIMIT في SQLite: تحديد عدد النتائج المُرجَعة
تُعدّ LIMIT أبسط أداة تحكّم في SQL، فهي ببساطة تُخبر SQLite: "أعطني هذا العدد من الصفوف كحدٍّ أقصى". ضعها في نهاية جملة SELECT وستحصل على عدد النتائج المطلوب أو أقل — لن يتجاوزها أبدًا، وقد يكون أقل إن لم يحتوِ الجدول على صفوف كافية لتلبية الطلب.
ستحصل على أول ثلاثة صفوف. لكن أي ثلاثة بالضبط؟ هنا تكمن المشكلة — فبدون ORDER BY، يختار SQLite الترتيب الذي يناسبه في تلك اللحظة. قد يكون اليوم ترتيب الإدراج، وغداً — بعد عملية تحديث أو تغيير في الفهرس — قد يتغير. استخدام LIMIT وحده لا بأس به إذا أردت "عينة سريعة"، لكن متى ما كان الترتيب مهماً، فلا بد من تحديده صراحةً.
تخطي الصفوف من البداية باستخدام OFFSET
عند دمج LIMIT مع OFFSET، يمكنك طلب شريحة من منتصف النتائج. الجملة OFFSET k تتجاهل أول k صف، ثم تُرجع LIMIT n ما يصل إلى n صف مما تبقى.
هذا يعني: «تخطَّ صفّين، ثم أعِد الصفّين التاليين» — أي الصفّين الثالث والرابع من النتيجة المرتّبة. تخيّل الأمر هكذا: WHERE يُرشِّح، وORDER BY يُرتِّب، وOFFSET يتخطّى، وLIMIT يُحدِّد السقف. تُنفَّذ بهذا الترتيب، وكلٌّ منها له دور مهم.
ترقيم الصفحات في SQLite يحتاج دائمًا إلى ORDER BY
أكثر استخدام شائع لـ LIMIT وOFFSET هو ترقيم الصفحات، أي تقسيم قائمة طويلة إلى صفحات تحتوي مثلًا على 20 صفًّا لكلٍّ منها. الصفحة الأولى تكون LIMIT 20 OFFSET 0، والصفحة الثانية LIMIT 20 OFFSET 20، وهكذا.
هناك أمران تجب ملاحظتهما. أولاً، وجود ORDER BY ليس اختيارياً — بدونه لا معنى لـ"الصفحة الثانية"، إذ قد يتغير ترتيب الصفوف بين كل تحميل وآخر. ثانياً، مفتاح الترتيب يتضمن id كفاصل عند التساوي. فإذا كان لتدوينتين نفس قيمة created_at، فأنت بحاجة إلى عمود فريد يضمن ترتيباً ثابتاً بينهما، وإلا فقد تتبادلان مواقعهما ويتسرب صف من صفحة إلى أخرى.
القاعدة الذهبية: رتّب حسب عمود فريد، أو حسب عمود الترتيب مضافاً إليه فاصل فريد عند التساوي.
اختصار: LIMIT n, m
تدعم SQLite صيغة قديمة بالفاصلة للحفاظ على التوافق مع MySQL، وهي: LIMIT offset, count. معناها مطابق تماماً لـ LIMIT count OFFSET offset، لكن ترتيب القيمتين معكوس، ومن السهل الخلط بينهما عند القراءة.
-- هاتان الجملتان متكافئتان:
SELECT * FROM books LIMIT 10 OFFSET 20;
SELECT * FROM books LIMIT 20, 10; -- الإزاحة أولاً، ثم العدد
الصيغة الثانية مختصرة، لكنها توقع كثيرين ممن يتوقعون أن يكون الرقم الأول هو عدد الصفوف. الأفضل أن تلتزم بـ LIMIT n OFFSET k — فهي صريحة وتُقرأ بالترتيب الطبيعي.
استخدام OFFSET بدون LIMIT: حيلة LIMIT -1
لا يمكن استخدام OFFSET وحده — فقواعد SQLite تشترط أن يأتي بعد LIMIT. إذًا كيف تعبّر عن "تخطَّ أول 10 صفوف وأعطني كل ما يليها"؟ الحيلة المتعارف عليها هي LIMIT -1، حيث يفسّرها SQLite بمعنى "بلا حد أعلى".
أي قيمة سالبة لـ LIMIT تؤدي نفس الغرض، لكن -1 هو الاصطلاح المتعارف عليه. ستراها غالبًا في السكربتات التي تتنقل عبر النتائج صفحةً صفحة، وتحتاج في الدفعة الأخيرة استعلامًا يقول "أعطني الباقي".
فخ أداء OFFSET في SQLite
إليك ما لا يذكره أحد إلا بعد أن تصطدم به: OFFSET لا يجعل SQLite يتخطى العمل فعليًا، بل يجعله يتخطى الإخراج فقط. فلكي يُرجع لك الصفوف من 10,001 إلى 10,020، يظل المحرك يمر داخليًا على العشرة آلاف صف الأولى قبل أن يبدأ بإصدار النتائج. القيم الصغيرة من OFFSET لا تكلّف شيئًا، لكن حين تصل إلى عشرات أو مئات الآلاف يصبح البطء ملحوظًا.
للتنقل العميق بين الصفحات، الحل المعتاد هو الترقيم بالمفتاح (keyset pagination): بدلًا من "تخطَّ N صفًا"، تحتفظ بمفتاح الفرز للصف الأخير، ثم تطلب "الصفوف التي تأتي بعد هذا الصف".
كل صفحة تعتمد على بحث مفهرس بدل ما تمر على كل الصفوف التي قبلها. المقابل لذلك: ما تقدر تقفز مباشرة إلى "الصفحة 47" — تقدر تتنقّل للأمام فقط داخل البيانات. وهذا بالضبط التي تحتاجه في خلاصات التمرير اللانهائي ومؤشرات الـ API.
ترقيم الصفحات بـ OFFSET يفي بالغرض في لوحات الإدارة والنتائج الصغيرة. لكن لما تكون البيانات قابلة للنمو بلا حدود، الأفضل تروح إلى keyset pagination.
مثال تطبيقي كامل
نجمع كل التي فات في استعلام واحد مرقّم بصفحات، مع تصفية وترتيب ومُرجِّح حاسم لفك التعادل:
فلتَر النتائج لتقتصر على منتجات المكتب، ثم رتّبها تصاعديًا حسب السعر مع الاسم كمعيار لفك التعادل، وخُذ أول صفّين. ولعرض الصفحة الثانية، غيّر OFFSET 0 إلى OFFSET 2. الاستعلام قصير، لكن كل جملة فيه تؤدي دورًا حقيقيًا.
الخطوة التالية: DISTINCT
LIMIT يتحكم في عدد الصفوف العائدة، أما DISTINCT فيتحكم في ظهور التكرارات من الأساس. إنها الجملة التالية في صندوق أدوات SELECT، وقد تبدو سهلة لكنها خادعة عند الاستخدام الخاطئ — وهذا ما سنتناوله في الصفحة التالية.
الأسئلة الشائعة
ما وظيفة LIMIT في SQLite؟
LIMIT n يحدّ عدد الصفوف التي يُرجعها استعلام SELECT بحيث لا تتجاوز n. ويُنفَّذ بعد WHERE و GROUP BY و ORDER BY، أي أنك تقصّ النتيجة النهائية لا الصفوف التي يفحصها المحرك. مثلاً SELECT * FROM users LIMIT 10 يُرجع عشرة صفوف كحد أقصى.
كيف يعمل OFFSET مع LIMIT في SQLite؟
OFFSET k يتخطّى أول k صفوف من النتيجة قبل أن يبدأ LIMIT بالعدّ. فمثلاً LIMIT 10 OFFSET 20 يُرجع الصفوف من 21 إلى 30. لكن انتبه: SQLite يضطر داخلياً للمرور على الصفوف المتخطّاة، ولهذا تصبح القيم الكبيرة لـ OFFSET بطيئة.
هل يمكن استخدام OFFSET بدون LIMIT في SQLite؟
لا يمكن مباشرة، لأن OFFSET صالح فقط ضمن جملة LIMIT. الحل المعروف هو LIMIT -1 OFFSET k، حيث يعني -1 أنه لا يوجد حد أقصى، فيتخطّى SQLite عدد k من الصفوف ويُرجع كل ما تبقّى. حيلة بسيطة لكنها تستحق الحفظ.
لماذا تحتاج استعلامات الترقيم إلى ORDER BY؟
بدون ORDER BY، يحقّ لـ SQLite أن يُرجع الصفوف بأي ترتيب يراه مناسباً، وقد يتغيّر هذا الترتيب بين استعلام وآخر. عندها ينهار الترقيم: قد يظهر الصف نفسه في الصفحة 1 والصفحة 3، أو يختفي تماماً. لذلك استخدم دائماً LIMIT/OFFSET مع ORDER BY على عمود ثابت وفريد.