Offset Pagination — الترقيم بالإزاحة — طريقة تحميل البيانات بصفحات عبر HTTP API. يرسل العميل معاملات offset (الإزاحة من البداية) و limit (حجم الصفحة)، ويعيد الخادم السجلات من موضع offset. وفقًا لـ REST API Tutorial، يُستخدم هذا الأسلوب على نطاق واسع في خدمات RESTful بفضل سهولة تنفيذه. ومع ذلك، على كميات البيانات الكبيرة، يفقد الترقيم بالإزاحة أداءه بسبب المسح الكامل للجدول حتى الموضع المطلوب.
النقاط الرئيسية
Offset Pagination هو طريقة لترقيم البيانات حيث يحتوي طلب العميل على معاملين: offset (عدد السجلات المطلوب تخطيها) و limit (عدد السجلات المطلوب إرجاعها). ينفذ الخادم استعلام SQL مع OFFSET و LIMIT، ويتجاوز العدد المحدد من الصفوف ويعيد مجموعة نتائج بحجم ثابت.
نشأت هذه الطريقة في قواعد البيانات العلائقية كأبسط طريقة لتنظيم التنقل بين الصفحات وتم نقلها إلى واجهات HTTP API مع تطور بنية REST. لا تتطلب Offset Pagination تخزين الحالة على الخادم — كل طلب مستقل ويحتوي على جميع المعلومات اللازمة للاستعلام.
وفقًا لتقرير تصميم API من Postman (2025)، يُستخدم الترقيم بالإزاحة في 72% من واجهات REST API العامة، مما يجعله المعيار السائد على الرغم من قيود الأداء المعروفة على مجموعات البيانات الكبيرة.
يتضمن طلب REST النموذجي مع Offset Pagination معاملات الاستعلام offset و limit. تحتوي الاستجابة على قائمة السجلات للصفحة المطلوبة وبيانات وصفية لبناء واجهة التنقل.
يحد معامل limit من عدد السجلات المُعادة ويحمي الخادم والعميل من التحميل الزائد. تتراوح قيم limit النموذجية من 10 إلى 50 سجلًا لكل صفحة حسب تعقيد البيانات.
data class PageRequest(
val offset: Int,
val limit: Int
)
data class PageResponse<T>(
val items: List<T>,
val total: Int,
val hasMore: Boolean
)
fun RetrofitApi.fetchPage(request: PageRequest): Call<PageResponse<Item>>
Offset Pagination يُترجم إلى استعلام SQL مع بنيتي OFFSET و FETCH NEXT (أو LIMIT في MySQL/SQLite). يمسح خادم قاعدة البيانات الجدول، ويتجاوز عدد الصفوف المساوي للإزاحة، ويعيد الصفوف التالية بعدد limit. كلما زادت الإزاحة، زاد وقت تنفيذ الاستعلام.
ترجع مشكلة الأداء إلى أن قاعدة البيانات لا يمكنها الانتقال مباشرة إلى موضع الإزاحة — يجب عليها قراءة وتجاهل جميع الصفوف السابقة. عند offset = 100000 و limit = 20، يقرأ نظام إدارة قواعد البيانات 100,020 صفًا ويعيد 20 فقط.
SQL — اللغة التي ينفذ بها الخادم الترقيم بالإزاحة. تستخدم PostgreSQL و MySQL LIMIT، بينما تستخدم SQL Server و Oracle OFFSET...FETCH. تحسن أنظمة إدارة قواعد البيانات المختلفة هذا الاستعلام بطرق مختلفة، لكن مشكلة المسح الأساسية تبقى كما هي.
SELECT id, title, created_at
FROM articles
ORDER BY created_at DESC
OFFSET 100000 ROWS
FETCH NEXT 20 ROWS ONLY;
اتساق البيانات هو العيب الرئيسي لـ Offset Pagination عند العمل مع المجموعات الديناميكية. إذا تمت إضافة سجل جديد إلى بداية الجدول بين طلبي مستخدم، فإن جميع السجلات الموجودة تنتقل. يرى المستخدم تكرارات أو فجوات.
لنفترض جدولًا من 100 سجل مع limit = 20. في الصفحة 1، يرى المستخدم السجلات 1-20. يضيف المسؤول 5 سجلات جديدة. في الصفحة 2، يرى المستخدم السجلات 26-45 بدلاً من المتوقعة 21-40 — السجلات 21-25 مفقودة، والسجلات 21-25 من المجموعة السابقة مكررة في الصفحة 1.
الترقيم Cursor-based هو بديل لـ Offset Pagination يستخدم مؤشرًا إلى آخر سجل في الصفحة الحالية. بدلاً من الإزاحة الرقمية، يرسل العميل معرف آخر سجل تم استلامه، ويعيد الخادم السجلات N التالية بعده.
يحل أسلوب cursor-based مشكلة الاتساق: موضع المؤشر لا يتغير مع الإدراج أو الحذف لأن المؤشر يشير إلى سجل محدد، وليس إلى موضع. ومع ذلك، فهو أكثر تعقيدًا في التنفيذ — يتطلب حقلًا فريدًا قابلًا للترتيب (عادةً ID أو timestamp).
| المعامل | Offset Pagination | Cursor-based Pagination |
|---|---|---|
| البساطة | عالية — معاملان عدديان | متوسطة — يتطلب ترميز المؤشر |
| الاتساق | منخفض — تكرارات عند الإدراج | عالٍ — المؤشر لا يتأثر بالتغييرات |
| الأداء | يتدهور مع الإزاحة الكبيرة | مستقر في أي حجم |
| القفز إلى صفحة | نعم — يمكن التنقل إلى أي صفحة | لا — تنقل تسلسلي فقط |
| مناسب لـ | جداول <10K سجل، واجهات بأرقام الصفحات | الخلاصات، التمرير اللانهائي، المجموعات الكبيرة |
يعتمد الاختيار بين الأساليب على متطلبات واجهة المستخدم. إذا كنت بحاجة إلى تنقل بأرقام الصفحات وقفز مباشر — Offset Pagination أبسط. للتمرير اللانهائي أو خلاصات الأخبار، يُفضل استخدام المؤشرات.
Keyset pagination هو تنويع من أسلوب cursor-based حيث يتم التصفية على مفتاح فريد باستخدام WHERE بدلاً من OFFSET. يستخدم استعلام SQL شرطًا مثل WHERE id > lastId، مما يسمح لقاعدة البيانات باستخدام فهرس دون مسح الصفوف المهملة.
وفقًا لويكي PostgreSQL، يعمل keyset pagination أسرع 100-1000 مرة من استعلامات offset عند الإزاحات الكبيرة لأن مسح الفهرس يحل محل المسح الكامل للجدول. العيب هو عدم القدرة على القفز إلى صفحة عشوائية دون عبور تسلسلي.
Offset Pagination مثالية لمجموعات البيانات الصغيرة والمتوسطة (حتى 10,000 سجل) حيث يحتاج المستخدم إلى واجهة بأرقام الصفحات. تتضمن السيناريوهات النموذجية لوحات الإدارة وقوائم الطلبات والكتالوجات المفلترة مع ترقيم الصفحات.
للتطبيقات المحمولة، الترقيم بالإزاحة مناسب عند تحميل البيانات التاريخية حيث الإدراجات الجديدة نادرة أو مستحيلة — مثل سجل طلبات المستخدم، قوائم المهام المكتملة، أو أرشيف المعاملات. في هذه السيناريوهات، لا تنشأ مشكلة الاتساق.
غير موصى به لخلاصات وسائل التواصل الاجتماعي، قوائم التعليقات، الدردشات، والمجموعات الديناميكية الأخرى ذات الإدراجات المتكررة. في هذه الحالات، تؤدي الفجوات والسجلات المكررة إلى تدهور تجربة المستخدم وتتطلب منطق إزالة تكرار إضافي على العميل.
الترقيم الهجين يجمع بين offset و cursor: يستخدم الطلب الأول offset لعرض الصفحة الأولية، بينما تستخدم الطلبات اللاحقة cursor لتحميل التمرير اللانهائي. يُستخدم هذا الأسلوب في Instagram و Twitter، حيث تُحمّل الصفحة الأولى عبر cursor، ولكن يُستخدم offset لحساب الموضع عند العودة إلى عرض سابق.
يتطلب تنفيذ الأسلوب الهجين تخزين الموضع الافتراضي للمستخدم على العميل وتنسيق آليتي ترقيم على الخادم. وفقًا لمدونة Instagram Engineering، يستخدم فريقهم الترقيم cursor-based مع حقل إضافي startCursor يحل محل offset للتحميل الأولي.
التطبيقات المحمولة تستخدم Offset Pagination مع Retrofit/OkHttp على Android و URLSession/Combine على iOS. النمط النموذجي هو تحميل الصفحة التالية عند التمرير إلى نهاية القائمة عبر RecyclerView.OnScrollListener أو UICollectionView prefetching.
يتضمن تنفيذ الترقيم بالإزاحة على العميل المحمول ثلاثة مكونات: مدير ترقيم (يخزن offset الحالي و hasMore)، ومحول قائمة (يعرض العناصر ومؤشر التحميل)، ومستودع (ينفذ الطلبات ويعالج الأخطاء). يقدم Android Jetpack مكتبة Paging 3 التي تدعم كلاً من الترقيم بالإزاحة و cursor-based بشكل جاهز.
Paging 3 هي مكتبة Android Jetpack لتحميل البيانات بصفحات. تغلف منطق الترقيم، بما في ذلك تتبع offset، وإدارة حالة التحميل، والتحميل المسبق التلقائي عند التمرير. يحدد PagingSource مفاتيح الصفحات التالية والسابقة.
class OffsetPagingSource(
private val api: ApiService,
private val limit: Int = 20
) : PagingSource<Int, Item>() {
override suspend fun load(
params: LoadParams<Int>
): LoadResult<Int, Item> {
val offset = params.key ?: 0
return try {
val response = api.getItems(offset, limit)
LoadResult.Page(
data = response.items,
prevKey = null,
nextKey = if (response.hasMore) offset + limit else null
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
}
PagingSource يحدد المفاتيح prevKey و nextKey للتنقل بين الصفحات. مع الترقيم بالإزاحة، prevKey دائمًا null (لا يمكن الانتقال إلى الصفحة السابقة دون حفظ السجل)، بينما يزيد nextKey بمقدار limit مع كل تحميل حتى يعيد الخادم hasMore = false. هذا نموذج بسيط ويمكن التنبؤ به للقوائم المحمولة.
الخطأ الأول — الاعتماد على ترتيب السجلات دون فرز. Offset Pagination تتطلب فرزًا ORDER BY مستقرًا على حقل فريد. بدونها، قد يعيد نظام إدارة قواعد البيانات السجلات بترتيب عشوائي، مما يؤدي إلى تكرارات وفجوات عشوائية بين الصفحات.
الخطأ الثاني — استخدام offset لحساب رقم الصفحة في الواجهة. تعمل الصيغة page = offset / limit + 1 فقط إذا لم يتم حذف أو إضافة أي سجل بين التحميلات. مع البيانات الديناميكية، يصبح رقم الصفحة غير دقيق ويرى المستخدم معلومات غير صحيحة.
الخطأ الثالث — تجاهل مهلات الاستعلامات ذات offset الكبير. عند offset يتجاوز 100,000، قد يستغرق الاستعلام عشرات الثواني، مما يعطل الواجهة ويستهلك موارد الخادم. يُنصح بتعيين قيمة قصوى لـ offset على مستوى API (مثل 10,000) واستخدام الترقيم cursor-based للكميات الكبيرة.
الخطأ الرابع — عدم إضافة total count في الاستجابة. بدون العدد الإجمالي للسجلات، لا يمكن للعميل عرض عدد الصفحات وتنفيذ الترقيم بالأرقام. ومع ذلك، فإن COUNT(*) على الجداول الكبيرة مكلف — للمجموعات التي تتجاوز 100,000 سجل، استخدم تقديرات تقريبية أو حدد القيمة القصوى لـ total.
الأسئلة الشائعة
يستخدم Offset إزاحة رقمية لتخطي السجلات، بينما يستخدم cursor مؤشرًا إلى آخر سجل في الصفحة السابقة. Offset أبسط في التنفيذ لكنه يعاني من التكرارات عند الإدراج وفقدان الأداء عند الإزاحات الكبيرة. Cursor مستقر تحت أي تغييرات في البيانات.
الترقيم بالإزاحة غير فعال عند offsets أكثر من 10,000 سجل بسبب المسح الكامل للجدول. كما أنه غير مناسب للمجموعات الديناميكية (الخلاصات، الدردشات) حيث تظهر سجلات جديدة بين الطلبات — يرى المستخدم فجوات وسجلات مكررة أثناء التنقل.
يعتمد limit الأمثل على حجم السجل وسرعة الشبكة — من 10 إلى 50 عنصرًا لكل صفحة. للقوائم ذات الصور الكبيرة، استخدم limit = 10-15؛ للبيانات النصية، 20-50. اسمح دائمًا للعميل بتحديد limit الخاص به مع حد أقصى على الخادم (عادةً 100).
للتعامل مع التكرارات، استخدم إزالة التكرار على العميل بواسطة ID فريد، أو طبق فرزًا مستقرًا على حقل فريد، أو انتقل إلى الترقيم cursor-based. يدعم Android Paging 3 key لإزالة التكرار التلقائي لعناصر القائمة.
نعم، GraphQL يدعم الترقيم بالإزاحة عبر معاملات offset و limit في الاستعلام، على الرغم من أن مواصفات Relay توصي بأسلوب cursor-based. تقدم مكتبات Apollo GraphQL و Relay دعمًا مدمجًا للترقيم بالإزاحة مع إدارة تلقائية لحالة الصفحات.
الملخص
سنقوم بتطوير تطبيق جوال جاهز
تقدم IT Sectr تطبيقات iOS وAndroid للشركات الناشئة والشركات منذ عام 2017. سوف نقدم لك النصح ونقترح أفضل حل.
اقرأ أيضًا