Offset Pagination — صفحهبندی با افست — روش بارگذاری صفحهبندی شده دادهها از طریق HTTP API است. کلاینت پارامترهای offset (افست از ابتدا) و limit (اندازه صفحه) را ارسال میکند و سرور رکوردها را از موقعیت offset برمیگرداند. به گفته REST API Tutorial، این رویکرد به دلیل سادگی پیادهسازی به طور گسترده در سرویسهای RESTful استفاده میشود. با این حال، در حجمهای زیاد داده، صفحهبندی با افست به دلیل اسکن کامل جدول تا موقعیت مورد نظر عملکرد خود را از دست میدهد.
نکات اصلی
Offset Pagination — روشی برای تقسیم صفحهبندی شده دادهها است که در آن درخواست کلاینت شامل دو پارامتر است: offset (چند رکورد رد شود) و limit (چند رکورد برگردانده شود). سرور کوئری SQL با OFFSET و LIMIT اجرا میکند، تعداد مشخص شده ردیف را رد میکند و مجموعه نتایج با اندازه ثابت را برمیگرداند.
این روش در پایگاه دادههای رابطهای به عنوان سادهترین راه برای سازماندهی ناوبری بین صفحات ظهور کرد و با توسعه معماری REST به HTTP API منتقل شد. 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) تبدیل میشود. سرور پایگاه داده جدول را اسکن میکند، تعداد ردیفهای برابر با offset را رد میکند و limit ردیف بعدی را برمیگرداند. هرچه offset بزرگتر باشد، کوئری طولانیتر اجرا میشود.
مشکل عملکرد به این دلیل است که پایگاه داده نمیتواند مستقیماً به موقعیت offset برود — باید تمام ردیفهای قبلی را بخواند و دور بریزد. در offset = 100000 و limit = 20، DBMS 100020 ردیف میخواند و فقط 20 را برمیگرداند.
SQL — زبانی است که سرور صفحهبندی با افست را در آن اجرا میکند. در PostgreSQL و MySQL از LIMIT استفاده میشود، در SQL Server و Oracle — OFFSET...FETCH. DBMSهای مختلف این کوئری را به روش خود بهینه میکنند، اما مشکل اساسی اسکن کردن همچنان باقی است.
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 کاربر به جای 21-40 مورد انتظار، رکوردهای 26-45 را میبیند — رکوردهای 21-25 جا افتادهاند و رکوردهای 21-25 از مجموعه قبلی در صفحه 1 تکرار شدهاند.
Cursor-based pagination — جایگزینی برای Offset Pagination که از اشارهگر به آخرین رکورد صفحه فعلی استفاده میکند. به جای افست عددی، کلاینت شناسه آخرین رکورد دریافتی را ارسال میکند و سرور N رکورد بعد از آن را برمیگرداند.
رویکرد cursor-based مشکل سازگاری را حل میکند: موقعیت نشانگر با درج یا حذف تغییر نمیکند، زیرا نشانگر به یک رکورد خاص اشاره دارد نه به یک موقعیت. با این حال پیادهسازی آن پیچیدهتر است — به یک فیلد مرتبپذیر یکتا نیاز دارد (معمولاً ID یا timestamp).
| پارامتر | Offset Pagination | Cursor-based Pagination |
|---|---|---|
| سادگی | زیاد — دو پارامتر عددی | متوسط — نیاز به کدگذاری نشانگر |
| سازگاری | کم — تکراریها هنگام درج | زیاد — نشانگر به تغییرات وابسته نیست |
| عملکرد | با افزایش offset کاهش مییابد | در هر حجمی پایدار |
| پرش به صفحه | بله — میتوان به هر صفحهای رفت | خیر — فقط ناوبری ترتیبی |
| مناسب برای | جدولهای <10K رکورد، رابط با شماره صفحات | فیدها، اسکرول بینهایت، مجموعههای بزرگ |
انتخاب بین رویکردها به نیازهای رابط کاربر بستگی دارد. اگر ناوبری با شماره صفحات و انتقال مستقیم نیاز است — Offset Pagination سادهتر است. برای اسکرول بینهایت یا فیدهای خبری، نشانگرها ترجیح داده میشوند.
Keyset pagination — گونهای از رویکرد cursor-based است که در آن فیلتر کردن با استفاده از یک کلید یکتا با WHERE به جای OFFSET انجام میشود. کوئری SQL از شرط WHERE id > lastId استفاده میکند که به پایگاه داده امکان میدهد بدون اسکن ردیفهای دور ریخته شده از ایندکس استفاده کند.
به گفته PostgreSQL Wiki، keyset pagination در جابجاییهای بزرگ 100-1000 برابر سریعتر از کوئری offset اجرا میشود، زیرا اسکن ایندکس جایگزین پیمایش کامل جدول میشود. عیب — عدم امکان پرش به یک صفحه دلخواه بدون عبور ترتیبی.
Offset Pagination برای مجموعههای داده کوچک و متوسط (تا 10000 رکورد) بهینه است، جایی که کاربر به رابطی با شماره صفحات نیاز دارد. سناریوهای معمول — پنلهای مدیریت، لیست سفارشها، کاتالوگها با فیلتر و صفحهبندی.
برای برنامههای موبایل، صفحهبندی با افست برای بارگذاری دادههای تاریخی مناسب است، جایی که درج رکوردهای جدید نادر یا غیرممکن است — به عنوان مثال، تاریخچه سفارشات کاربر، لیست وظایف تکمیل شده، آرشیو تراکنشها. در این سناریوها مشکل سازگاری رخ نمیدهد.
توصیه نمیشود از Offset Pagination برای فیدهای شبکههای اجتماعی، لیست نظرات، چتها و سایر مجموعههای پویا با درجهای مکرر استفاده شود. در این موارد، پرش و تکراری شدن رکوردها تجربه کاربری را بدتر میکند و به منطق اضافی حذف تکراری در کلاینت نیاز دارد.
صفحهبندی ترکیبی offset و cursor را ترکیب میکند: اولین درخواست از offset برای نمایش صفحه اولیه استفاده میکند و درخواستهای بعدی از cursor برای بارگذاری اسکرول بینهایت. این رویکرد در Instagram و Twitter پیادهسازی شده است، جایی که صفحه اول از طریق cursor بارگذاری میشود، اما offset برای محاسبه موقعیت هنگام بازگشت به نمای قبلی استفاده میشود.
پیادهسازی رویکرد ترکیبی نیاز به ذخیره موقعیت مجازی کاربر در کلاینت و هماهنگی دو مکانیزم صفحهبندی در سرور دارد. به گفته وبلاگ Instagram Engineering، تیم آنها از صفحهبندی cursor-based با فیلد اضافی startCursor استفاده میکند که offset را برای بارگذاری اولیه جایگزین میکند.
برنامههای موبایل از Offset Pagination همراه با Retrofit/OkHttp در اندروید و URLSession/Combine در iOS استفاده میکنند. الگوی معمول — بارگذاری صفحه بعدی هنگام اسکرول به انتهای لیست از طریق RecyclerView.OnScrollListener یا UICollectionView prefetching.
پیادهسازی صفحهبندی با افست در کلاینت موبایل شامل سه مؤلفه است: مدیر صفحهبندی (offset فعلی و hasMore را ذخیره میکند)، آداپتر لیست (عناصر و نشانگر بارگذاری را نمایش میدهد) و مخزن (کوئریها را اجرا و خطاها را مدیریت میکند). Android Jetpack کتابخانه Paging 3 را ارائه میدهد که هم offset و هم cursor-based pagination را به صورت آماده پشتیبانی میکند.
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 بر روی یک فیلد یکتا دارد. بدون آن، DBMS ممکن است رکوردها را به ترتیب دلخواه برگرداند که منجر به تکراریها و پرشهای تصادفی بین صفحات میشود.
دومین اشتباه — استفاده از offset برای محاسبه شماره صفحه در رابط کاربری. فرمول page = offset / limit + 1 فقط به شرطی کار میکند که هیچ رکوردی بین بارگذاریها حذف یا اضافه نشده باشد. با دادههای پویا، شماره صفحه نادقیق میشود و کاربر اطلاعات نادرست میبیند.
سومین اشتباه — نادیده گرفتن مهلت زمانی کوئریهای با offset بزرگ. در offset بیش از 100000، کوئری ممکن است دهها ثانیه طول بکشد، رابط کاربری را مسدود کرده و منابع سرور را مصرف کند. توصیه میشود حداکثر مقدار offset را در سطح API (مثلاً 10000) تنظیم کنید و برای حجمهای بزرگ از صفحهبندی cursor-based استفاده کنید.
چهارمین اشتباه — اضافه نکردن total count به پاسخ. بدون تعداد کل رکوردها، کلاینت نمیتواند تعداد صفحات را نمایش داده و صفحهبندی با شماره را پیادهسازی کند. با این حال، محاسبه COUNT(*) روی جداول بزرگ نیز پرهزینه است — برای مجموعههای بیش از 100000 رکورد از تخمینهای تقریبی استفاده کنید یا حداکثر مقدار total را محدود کنید.
سوالات متداول
Offset از جابجایی عددی (offset) برای رد کردن رکوردها استفاده میکند، در حالی که cursor از اشارهگر به آخرین رکورد صفحه قبلی. Offset در پیادهسازی سادهتر است، اما از تکراریها هنگام درج و کاهش عملکرد در جابجاییهای بزرگ رنج میبرد. Cursor در هر تغییری در دادهها پایدار است.
صفحهبندی با offset در offset بیش از 10000 رکورد به دلیل اسکن کامل جدول ناکارآمد است. همچنین برای مجموعههای پویا (فیدها، چتها) که رکوردهای جدید بین درخواستها ظاهر میشوند نامناسب است — کاربر هنگام ناوبری پرشها و تکراریها را میبیند.
limit بهینه به اندازه رکورد و سرعت شبکه بستگی دارد — از 10 تا 50 عنصر در هر صفحه. برای لیستهای با تصاویر بزرگ از limit = 10-15 استفاده کنید، برای دادههای متنی — 20-50. همیشه به کلاینت اجازه دهید limit خود را با محدودیت حداکثر در سرور (معمولاً 100) مشخص کند.
برای مقابله با تکراریها از حذف تکراری در کلاینت بر اساس ID یکتا استفاده کنید، مرتبسازی پایدار بر روی فیلد یکتا اعمال کنید یا به صفحهبندی cursor-based مهاجرت کنید. Android Paging 3 از کلید برای حذف تکراری خودکار عناصر لیست پشتیبانی میکند.
بله، GraphQL از صفحهبندی با افست از طریق آرگومانهای offset و limit در کوئری پشتیبانی میکند، اگرچه مشخصات Relay رویکرد cursor-based را توصیه میکند. کتابخانههای Apollo GraphQL و Relay پشتیبانی داخلی از صفحهبندی با افست با مدیریت خودکار وضعیت صفحات را ارائه میدهند.
خلاصه
ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد
IT Sectr از سال 2017 برنامههای iOS و Android را برای استارتاپها و کسبوکارها ایجاد میکند. ما به شما مشاوره میدهیم و بهترین راهحل را پیشنهاد خواهیم کرد.
همچنین بخوانید