LIMIT OFFSET، وكل صفحة بعيدة بتدفعك تقرا كل اللي قبلها.
المستوى المطلوب: متوسط — يفترض إنك عارف SQL أساسي ومفهوم B-tree index بشكل عام.
Keyset Pagination: بديل OFFSET اللي بيخلّي الصفحة 10000 تتنفّذ في 12ms
المشكلة باختصار
تطبيقات كتيرة بتقدّم pagination بهذه الطريقة:
SELECT id, title, created_at
FROM products
ORDER BY created_at DESC
LIMIT 20 OFFSET 200000;
الـ DB هنا مش بتقفز للصف رقم 200001. هي بتقرا كل الـ 200000 صف اللي قبله، بترتّبهم، ثم بترميهم. الصفحة بتتكلّف عليك 200020 قراءة صف عشان تعرض 20.
الـ OFFSET بيكبر خطّيًا مع رقم الصفحة. يعني الزمن مش ثابت — كل ما المستخدم يدخل أعمق، الـ query بيبطّأ. ده اللي بيخلّي الصفحات الأخيرة في موقعك تحس إنها مكسورة، رغم إن أول صفحة بترد في ميلي ثانية.
مثال للمبتدئ: طابور المخبز
تخيّل المخبز فيه 200,000 رغيف عيش مرتّبين في صف طويل من الأقدم للأحدث. عميل دخل وقال "عايز آخر 20 رغيف". فيه طريقتين تخدمه بيهم:
- الطريقة الأولى (OFFSET): تبدأ من أوّل رغيف، تعدّ 199,980 رغيف وترميهم على الأرض، وتسلّمه آخر 20.
- الطريقة الثانية (Keyset): تروح من الآخر مباشرة، تاخد آخر 20 وخلاص.
الفرق بين الاتنين هو نفس الفرق بين OFFSET و Keyset. الأولى بتدفع تكلفة كل العناصر اللي قبل نقطة البداية. التانية بتقفز للنقطة على طول.
التعريف العلمي
Keyset Pagination (بتسمّى أحيانًا Cursor-based Pagination أو Seek Method) بتستخدم قيمة العمود اللي بترتّب عليه الجدول كنقطة بداية، بدل عدّاد رقم الصفحة. كل طلب من الـ client بيبعت آخر key شافه في الصفحة السابقة، والـ DB بتعمل index seek على الـ B-tree للنقطة دي مباشرة، ثم تقرأ N صف على التوالي.
الافتراضات اللي لازم تتحقّق عشان الطريقة تشتغل صح:
- العمود اللي بترتّب عليه (مثلاً
created_at) عليه index. - القيم تقريبًا فريدة. لو فيه قيم مكرّرة (زي تاريخين بنفس الثانية)، لازم نضيف tiebreaker زي
idعشان الترتيب يفضل deterministic. - الترتيب ثابت بين الطلبات. مينفعش ORDER BY عشوائي.