المستوى: متوسط — للمطوّرين اللي بيعملوا pagination على جداول فيها مئات الآلاف لملايين الصفوف.
لو الصفحة رقم 1 في الـ API بترجع في 5 مللي ثانية، والصفحة رقم 10000 بتاخد قرب الثانية على نفس البيانات، المشكلة مش في السيرفر ولا في الشبكة. المشكلة في إنك بتستخدم OFFSET. في المقال ده هتعرف ليه OFFSET بيبطأ كل ما رقم الصفحة يكبر، وإزاي تحوّله لترقيم بالـ Keyset يفضل ثابت السرعة مهما وصلت.
المشكلة باختصار
الترقيم بالإزاحة (LIMIT ... OFFSET ...) هو الطريقة الافتراضية في أغلب الـ ORM. وهو تمام على أول كام صفحة. لكن على جدول كبير، الصفحات البعيدة بتتحول لعبء حقيقي على قاعدة البيانات. الافتراض هنا إن عندك جدول بملايين الصفوف، والمستخدمين بيوصلوا لصفحات عميقة (بحث، سكرول لا نهائي، تصدير بيانات).
ليه OFFSET بيزحف؟ (المفهوم الأول بمثال)
تخيّل معاك كتاب فيه مليون اسم مرتّبين، وطلبت منك الأسماء من رقم 900001 لـ 900010. الطريقة السريعة إنك تفتح على الاسم اللي وقفت عنده المرة اللي فاتت وتكمّل. لكن OFFSET بيعمل حاجة تانية: بيبدأ يعدّ من أول الكتاب اسم اسم، يرمي أول 900000، وبعدها بس يبدأ يديك العشرة اللي انت عايزهم.
ده بالظبط اللي بيحصل فعلاً في قاعدة البيانات. SELECT ... LIMIT 10 OFFSET 900000 بيقرأ 900010 صف من الـ index أو الجدول، يرمي أول 900000، ويرجّع آخر 10. يعني الشغل بيزيد خطيًا مع رقم الصفحة. الصفحة الأخيرة بتكلّف أضعاف الصفحة الأولى، رغم إن عدد النتايج واحد.
الحل: الترقيم بالـ Keyset (Seek Method)
بدل ما تقول "تخطّى 900000 صف"، قول "هاتلي الصفوف اللي مفتاحها بعد آخر صف شفته". ده الفرق الجوهري: بنستخدم قيمة (cursor) من الصفحة السابقة، والـ index بيقفز عليها مباشرة من غير ما يعدّ حاجة.
- رتّب على عمود مفهرس وفريد (زي
id) أو تركيبة أعمدة تضمن ترتيبًا ثابتًا. - خزّن قيمة العمود ده من آخر صف في الصفحة الحالية، وابعتها كـ cursor للصفحة اللي بعدها.
- في الصفحة الجديدة، استخدم
WHEREعلى المفتاح بدلOFFSET.
-- البطيء: بيقرأ مليون صف ويرميهم قبل ما يوصل لنتيجتك
SELECT id, title, created_at
FROM articles
ORDER BY id
LIMIT 10 OFFSET 1000000;
-- السريع (Keyset): بيقفز بالـ index على آخر id شُفته
SELECT id, title, created_at
FROM articles
WHERE id > 1000000 -- آخر id من الصفحة السابقة (الـ cursor)
ORDER BY id
LIMIT 10;
-- شرط أساسي: index على عمود الترتيب
CREATE INDEX idx_articles_id ON articles (id);لو بترتّب على عمود مش فريد (زي created_at اللي ممكن يتكرر)، لازم تضيف عمود فاصل (tie-breaker) عشان متفوّتش صفوف ولا تكرّرها: