مستوى المقال: محترف — الكلام ده موجّه لمطور عنده تجربة مع SQL و EXPLAIN ANALYZE، وبيدير جداول فيها ملايين الصفوف. لو لسه بتبدأ مع قواعد البيانات، الفكرة هتوصلك من المثال البسيط في الأول، بس التطبيق الكامل يحتاج خلفية في الـ indexing.
ليه OFFSET بيبطّئ صفحاتك الأخيرة، والحل في سطر واحد
لو صفحة 5000 في الـ API بتاعتك بتاخد ثانية كاملة بينما صفحة 1 بترجع في 2 مللي ثانية، المشكلة مش في السيرفر ولا في غياب الـ index. المشكلة في OFFSET نفسه. لو غيّرت أسلوب الترقيم لـ Keyset (المعروف بالـ Seek Method)، أي صفحة هتتحمّل في زمن ثابت تقريبًا مهما كان رقمها.
المشكلة باختصار
تخيّل دليل تليفونات ورقي مرتّب بالاسم. لو طلبت منك الاسم رقم 100,020، إنت مش هتقدر تقفز له على طول. هتعدّ من الأول: واحد، اتنين، تلاتة... لحد ما توصل للرقم 100,000، وبعدها تبدأ تقرأ العشرين اسم اللي إنت عايزهم. الـ 100,000 اسم اللي عدّيت عليهم كانوا شغل ضايع بالكامل.
LIMIT 20 OFFSET 100000 بيعمل بالظبط ده. قاعدة البيانات لازم تولّد وتعدّ 100,000 صف قبل المطلوب، ترميهم، وترجّع العشرين الباقيين. الافتراض إن عندك جدول بـ 2 مليون صف ومستخدم بيتصفّح بعمق، التكلفة بتكبر خطّيًا مع رقم الصفحة. ده اللي بيخلّي الصفحات الأخيرة بطيئة بينما الأولى سريعة.
ليه OFFSET بطيء فعلًا (الشرح الدقيق)
الـ index من نوع B-tree في PostgreSQL مرتّب ويقدر يقفز لأي قيمة بحثًا عنها في زمن لوغاريتمي. لكن OFFSET مش قيمة يبحث عنها، هو عدّاد صفوف. المُحرّك مفيش عنده طريقة يعرف بيها "وين الصف رقم 100,000" غير إنه يمشي على الصفوف المرتّبة واحد واحد ويعدّهم. ده اللي بيسمّوه في خطة التنفيذ تكلفة على شكل rows removed by offset.
-- جدول الطلبات: 2 مليون صف، index على id
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders
ORDER BY id
LIMIT 20 OFFSET 100000;
-- النتيجة المختصرة:
-- Limit (actual time=842.105..842.140 rows=20)
-- -> Index Scan using orders_pkey on orders
-- (actual rows=100020 loops=1)
-- Execution Time: 842.318 msلاحظ actual rows=100020. المُحرّك قرأ 100,020 صف فعلًا عشان يرمي منهم 100,000. ده مش index بطيء، ده استخدام غلط للـ index. والمشكلة بتتضاعف مع كل صفحة أعمق.
الحل: Keyset / Seek Method
الفكرة بسيطة: بدل ما تقول "تخطّ أول 100,000 صف"، قول "ابدأ من بعد آخر صف شفته". إنت بتحوّل الترقيم من عدّاد لـ شرط بحث، والشرط ده الـ index بيخدمه مباشرة.
- رتّب على عمود (أو أعمدة) فريد ومُفهرَس، زي
idأو(created_at, id).