Cursor Pagination در توسعه موبایل — چیست، اصل و پیاده‌سازی

نویسنده: IT Sectr منتشر شده: 2026-03-11 زمان مطالعه: 9 دقیقه

Cursor Pagination — روش صفحه‌بندی داده‌ها است که از یک کursor منحصربه‌فرد برای پیمایش در مجموعه مرتب شده رکوردها استفاده می‌کند. بر اساس GraphQL Specification (2025)، صفحه‌بندی کursor یک استاندارد توصیه‌شده برای APIهایی است که با داده‌های پویا کار می‌کنند. Cursor Pagination معایب اصلی رویکرد Offset را برطرف می‌کند: ناپایداری هنگام درج و کاهش عملکرد در جابجایی‌های بزرگ.

نکات اصلی

  • Cursor Pagination — روش صفحه‌بندی که در آن هر رکورد یک شناسه-کursor منحصربه‌فرد برای پیمایش دارد.
  • کursor — نشانگر منحصربه‌فرد موقعیت در مجموعه داده (معمولاً ID، UUID، timestamp) که هنگام درج تغییر نمی‌کند.
  • پایداری — رکوردهای جدید اضافه شده بین درخواست‌ها، کursor را جابجا نمی‌کنند و از تکراری و افتادگی جلوگیری می‌کنند.
  • عملکرد — کوئری WHERE id > cursor از ایندکس به طور مؤثر استفاده می‌کند و سرعت را در مجموعه‌های بزرگ از دست نمی‌دهد.
  • محدودیت — صفحه‌بندی کursor از پیمایش بر اساس شماره صفحه پشتیبانی نمی‌کند (نمی‌توان به صفحه 5 پرید).

Cursor Pagination چیست؟

Cursor Pagination (صفحه‌بندی کursor) — روش صفحه‌بندی که در آن سرور همراه با داده‌ها یک نشانگر ویژه — کursor — برمی‌گرداند. کلاینت از این کursor در درخواست بعدی برای دریافت بخش بعدی رکوردها استفاده می‌کند. کursor شناسه منحصربه‌فرد آخرین عنصر صفحه فعلی است.

برخلاف Offset-pagination که در آن کلاینت می‌گوید «صفحه 5 از 20 رکورد را به من بده»، صفحه‌بندی کursor متفاوت کار می‌کند: «20 رکورد بعد از رکورد با ID = 83 را به من بده». سرور کوئری با شرط WHERE id > 83 و LIMIT 20 اجرا می‌کند. این رویکرد تضمین می‌کند که هر رکورد صرف‌نظر از درج‌ها دقیقاً در یک صفحه قرار می‌گیرد.

مفهوم صفحه‌بندی کursor به لطف مشخصات Relay Connection (GraphQL) گسترش یافته است که cursor-based pagination را به استانداردی برای APIهای مدرن تبدیل کرده است. Relay قالب پاسخ را مشخص می‌کند: edges (آرایه رکوردها با کursor)، pageInfo (hasNextPage، hasPreviousPage، startCursor، endCursor).

تاریخچه پیدایش

Cursor-pagination یک تکنیک جدید نیست — مدت‌ها قبل از ظهور وب در پایگاه‌های داده استفاده می‌شد. در SQL به آن keyset pagination یا seek method می‌گویند. این روش پس از انتشار مشخصات Relay در سال 2015 در APIها محبوب شد، که قالب کursor را به عنوان رشته base64 شده برای یکپارچگی انتقال از طریق HTTP رسمی کرد.

صفحه‌بندی کursor چگونه کار می‌کند

اصل اساسی صفحه‌بندی کursor — کوئری از شرط WHERE روی فیلد ایندکس شده برای موقعیت‌یابی استفاده می‌کند، نه از جابجایی. برای جهت جلو از WHERE id > last_id، برای جهت عقب از WHERE id < first_id استفاده می‌شود. ایندکس B-tree امکان یافتن اولین رکورد بعد از کursor را در O(log n) فراهم می‌کند که زمان پاسخ پایدار را تضمین می‌کند.

sql
-- دریافت ۲۰ رکورد بعد از نشانگر '83'
SELECT id, title, created_at
FROM posts
WHERE id < 83
ORDER BY id DESC
LIMIT 20;

-- دریافت ۲۰ رکورد قبل از نشانگر '83' (به عقب)
SELECT id, title, created_at
FROM posts
WHERE id > 83
ORDER BY id ASC
LIMIT 20;

قالب کursor

کursor می‌تواند ساده (مقدار ID) یا پیچیده (ترکیبی از چند فیلد) باشد. کursorهای ساده کلید اصلی رکورد هستند، مثلاً id خودافزایش‌شونده یا UUID. کursorهای پیچیده برای مرتب‌سازی بر اساس فیلدهای غیر یکتا استفاده می‌شوند، مثلاً (created_at, id)، که در آن id یکتایی را در زمان‌های یکسان تضمین می‌کند.

قالب معمول API — کursor به صورت رشته base64 شده. سرور کursor را دیکد می‌کند، مقدار را استخراج و کوئری SQL می‌سازد. کدگذاری Base64 ساختار داخلی کursor را از کلاینت پنهان می‌کند و امکان تغییر قالب بدون از دست دادن سازگاری معکوس را فراهم می‌کند. کلاینت کursorها را در فیلد endCursor پاسخ دریافت کرده و به عنوان رشته در درخواست بعدی ارسال می‌کند.

پیمایش جلو و عقب

صفحه‌بندی کursor از پیمایش دوطرفه پشتیبانی می‌کند. برای حرکت به جلو (next) از کursor آخرین عنصر صفحه فعلی، برای عقب (previous) از کursor اولین عنصر استفاده می‌شود. پارامترهای after و before در درخواست جهت را مشخص می‌کنند: after رکوردهای بعد از کursor را می‌گیرد، before — قبل از کursor.

مقایسه Cursor vs Offset

انتخاب بین صفحه‌بندی کursor و Offset-pagination یکی از سوالات معماری کلیدی در طراحی API است. هر روش نقاط قوت و ضعفی دارد. Cursor-pagination در سناریوهای داده‌های پویا برنده است، Offset — در سناریوهای پیمایش دلخواه.

ویژگیCursorOffset
پایداری هنگام درجبالا (بدون تکراری)پایین (جابجایی صفحات)
عملکرد در مجموعه‌های بزرگO(log n) — پایدارO(n) — با رشد کاهش می‌یابد
پیمایش بر اساس شماره صفحهخیربله (page=5)
پیچیدگی پیاده‌سازیمتوسطکم
پشتیبانی در RESTcursor/before/afterpage/offset
پشتیبانی در GraphQLاستاندارد Relayتوصیه نمی‌شود

چرا Offset در مقیاس بزرگ شکست می‌خورد

Offset-pagination اسکن کامل جدول را تا موقعیت OFFSET انجام می‌دهد. در offset=100000، پایگاه داده 100000 سطر را می‌خواند و رد می‌کند، حتی اگر LIMIT برابر 20 باشد. MySQL و PostgreSQL نمی‌توانند OFFSET را بهینه کنند — این ویژگی پیاده‌سازی LIMIT/OFFSET در SQL است. Cursor-pagination از ایندکس B-tree استفاده می‌کند که موقعیت را در O(log n) پیدا می‌کند.

مشکل اضافی Offset — «افتادگی» رکوردها هنگام صفحه‌بندی به عقب. اگر کاربر صفحه 5 را بارگذاری کرده و در این لحظه رکوردهای جدید اضافه شوند، هنگام درخواست صفحه 6، رکورد صفحه 5 را دوباره می‌بیند یا رکوردهای جدید را از دست می‌دهد. Cursor-pagination این سناریو را کاملاً حذف می‌کند: کursor به مکان مشخصی در مجموعه اشاره می‌کند و درج‌ها موقعیت را تغییر نمی‌دهند.

پیاده‌سازی Cursor Pagination

پیاده‌سازی صفحه‌بندی کursor را در بک‌اند (Kotlin + Spring) و در کلاینت (Android + Retrofit) بررسی می‌کنیم. سرور پارامترهای after, before, limit را دریافت کرده و لیست رکوردها را با کursor و pageInfo برمی‌گرداند. پاسخ معمولی شامل hasNextPage و hasPreviousPage برای مدیریت UI صفحه‌بندی است.

kotlin
@GetMapping("/posts")
fun getPosts(
    @RequestParam after: Long?,
    @RequestParam(defaultValue = "20") limit: Int
): CursorResponse<Post> {
    val cursor = after ?: Long.MAX_VALUE
    val posts = repository.findByIdLessThanOrderByIdDesc(
        cursor, PageRequest.of(0, limit)
    )
    val endCursor = posts.lastOrNull()?.id
    return CursorResponse(
        data = posts,
        pageInfo = PageInfo(
            hasNextPage = posts.size == limit,
            endCursor = endCursor
        )
    )
}

پیاده‌سازی کلاینت در Android

در سمت کلاینت، صفحه‌بندی کursor از طریق PagingSource از Paging 3 پیاده‌سازی می‌شود، که در آن کلید کursor (Long) است. PagingSource.load LoadParams.key — کursor آخرین رکورد بارگذاری شده را دریافت می‌کند. LoadResult.Page داده‌ها و nextKey — کursor برای صفحه بعدی را برمی‌گرداند. وقتی nextKey = null — صفحه‌بندی کامل شده است.

kotlin
// Retrofit API
interface PostApi {
    @GET("posts")
    suspend fun getPosts(
        @Query("after") after: Long?,
        @Query("limit") limit: Int = 20
    ): CursorResponse<Post>
}

// PagingSource با کلید کursor
class PostPagingSource(
    private val api: PostApi
) : PagingSource<Long, Post>() {

    override suspend fun load(
        params: LoadParams<Long>
    ): LoadResult<Long, Post> = try {
        val response = api.getPosts(
            after = params.key,
            limit = params.loadSize
        )
        val nextKey = response.pageInfo.endCursor
        LoadResult.Page(
            data = response.data,
            prevKey = null,
            nextKey = nextKey
        )
    } catch (e: Exception) {
        LoadResult.Error(e)
    }
}

پیاده‌سازی GraphQL از طریق Relay

در GraphQL، صفحه‌بندی کursor از طریق الگوی Connection Relay پیاده‌سازی می‌شود. هر نوع دارای Connection (با pageInfo و edges) و Edge (node + cursor) است. درخواست پارامترهای first، after، last، before را ارسال می‌کند. سرور برمی‌گرداند آرایه edges با کursor و pageInfo با hasNextPage/hasPreviousPage.

چه زمانی از Cursor Pagination استفاده کنیم

Cursor-pagination برای APIهایی که با داده‌های پویا کار می‌کنند توصیه می‌شود، جایی که رکوردها اغلب اضافه یا حذف می‌شوند. مثال‌های کلاسیک: فید خبری در شبکه اجتماعی، پیام‌های چت، تاریخچه تراکنش‌ها، نظرات پست. در همه این سناریوها، سازگاری و عدم وجود تکراری مهم است.

  • چت‌ها و پیام‌رسان‌ها — هر پیام جدید به بالای لیست اضافه می‌شود. Offset-pagination با هر پیام جدید به هم می‌ریزد.
  • شبکه‌های اجتماعی و فیدها — پست‌ها به طور مداوم منتشر می‌شوند. صفحه‌بندی کursor تضمین می‌کند کاربر هیچ پستی را از دست ندهد.
  • تاریخچه سفارشات و تراکنش‌ها — داده‌ها کمتر تغییر می‌کنند، اما سازگاری برای گزارش‌گیری مالی حیاتی است.
  • API با حجم داده بزرگ — میلیون‌ها رکورد. Cursor-pagination عملکرد را در جایی که Offset کند می‌شود حفظ می‌کند.
  • GraphQL API — استاندارد Relay استفاده از cursor-based pagination را مطابق با مشخصات الزامی می‌کند.
  • اپلیکیشن‌های موبایل با اسکرول بی‌نهایت — کاربر به پایین اسکرول کرده و بخش‌های جدید را بارگذاری می‌کند. رویکرد کursor تجربه روان بدون تکراری را فراهم می‌کند.

چه زمانی Cursor-pagination مناسب نیست

سناریوهایی وجود دارد که Offset-pagination راحت‌تر است: پنل‌های مدیریتی که نیاز به پیمایش بر اساس شماره صفحه دارند؛ جستجو با صفحه‌بندی که نتایج ممکن است تغییر کنند؛ گزارش‌ها و تحلیل که نیاز به لینک ثابت به صفحه 5 است. در این موارد مزایای کursor بر پیچیدگی پیاده‌سازی برتری ندارد.

صفحه‌بندی کursor از «پرش» به صفحه دلخواه پشتیبانی نمی‌کند — کاربر نمی‌تواند روی «صفحه 5» کلیک کرده و به آن برود. این محدودیت معماری است: برای محاسبه تعداد کل صفحات یک کوئری COUNT جداگانه لازم است که ممکن است برای جداول بزرگ هزینه‌بر باشد. در چنین مواردی رویکرد ترکیبی: cursor برای داده‌ها + count برای صفحه‌بندی.

سوالات متداول

کursor در Cursor Pagination چیست؟

کursor — شناسه منحصربه‌فرد رکورد است که موقعیت را در مجموعه داده نشان می‌دهد. می‌تواند ساده (ID رکورد) یا پیچیده (چند فیلد) باشد. کلاینت کursor آخرین رکورد صفحه را دریافت کرده و در درخواست بعدی برای دریافت بخش بعدی ارسال می‌کند.

Cursor Pagination چه برتری نسبت به Offset دارد؟

Cursor-pagination هنگام اضافه شدن رکوردهای جدید دچار جابجایی نمی‌شود — هر عنصر دقیقاً در یک صفحه قرار می‌گیرد. همچنین سرعت را در حجم‌های بالا با استفاده از ایندکس‌ها به جای اسکن n سطر اول حفظ می‌کند. Offset ساده‌تر است اما برای داده‌های پویا ناپایدار است.

آیا می‌توان صفحه‌بندی کursor را بدون GraphQL پیاده‌سازی کرد؟

بله، صفحه‌بندی کursor به GraphQL وابسته نیست. می‌توان آن را در هر REST API با ارسال کursor به عنوان پارامتر کوئری ?after=83&limit=20 پیاده‌سازی کرد. پاسخ باید شامل pageInfo با endCursor و hasNextPage باشد — این به کلاینت امکان می‌دهد بدون دانستن ساختار داخلی کursor بارگذاری را مدیریت کند.

از کدام کursor استفاده کنیم — ID، UUID یا timestamp؟

ID خودافزایش‌شونده — انتخاب بهینه: به طور یکنواخت افزایش می‌یابد، تغییر نمی‌کند، به طور مؤثر ایندکس می‌شود. UUID v7 (مرتب شده بر اساس زمان) نیز مناسب است. Timestamp ممکن است در زمان یکسان تکراری ایجاد کند، بنابراین با ID ترکیب می‌شود: (created_at, id) برای تضمین یکتایی کursor.

چگونه تعداد کل صفحات را در صفحه‌بندی کursor بدانیم؟

صفحه‌بندی کursor تعداد کل صفحات را ارائه نمی‌دهد — این محدودیت آن است. اگر اطلاعاتی درباره total نیاز است، یک کوئری COUNT جداگانه با همان فیلترها اجرا کنید. برای جداول بزرگ از شمارش تقریبی از طریق EXPLAIN یا total کش شده از تحلیل استفاده کنید.

خلاصه

  • Cursor Pagination — روش صفحه‌بندی با پیمایش بر اساس شناسه منحصربه‌فرد رکورد به جای جابجایی.
  • کursor پایداری مجموعه را هنگام درج تضمین می‌کند: رکوردهای جدید صفحات بارگذاری شده را جابجا نمی‌کنند.
  • عملکرد در حجم‌های بزرگ داده به لطف استفاده از ایندکس‌های B-tree بالا (O(log n)) باقی می‌ماند.
  • Cursor-pagination برای داده‌های پویا مناسب است: چت‌ها، فیدهای خبری، تراکنش‌ها، نظرات.
  • محدودیت اصلی — عدم پیمایش بر اساس شماره صفحه و عدم امکان پرش به صفحه دلخواه.
  • پیاده‌سازی از WHERE id بعد از کursor، پارامترهای after/before و pageInfo در پاسخ استفاده می‌کند.
  • استاندارد — Relay Connection GraphQL، اما REST API با پارامترهای کursor نیز گسترده است.

ما یک اپلیکیشن موبایل به صورت کلید در دست توسعه خواهیم داد

IT Sectr از سال 2017 برنامه‌های iOS و Android را برای استارتاپ‌ها و کسب‌وکارها ایجاد می‌کند. ما به شما مشاوره می‌دهیم و بهترین راه‌حل را پیشنهاد خواهیم کرد.

بحث درباره پروژه

همچنین بخوانید