Cursor Pagination в мобилната разработка — какво е, принцип и реализация

Автор: IT Sectr Публикувано: 2026-03-11 Време за четене: 9 мин

Cursor Pagination — метод за постранично зареждане на данни, който използва уникален курсор за навигация в подреден набор от записи. Според GraphQL Specification (2025), курсорната пагинация е препоръчителният стандарт за API, работещи с динамични данни. Cursor Pagination премахва основните недостатъци на Offset подхода: нестабилност при вмъквания и спад на производителността при големи отмествания.

Основни точки

  • Cursor Pagination — метод за пагинация, при който всеки запис има уникален идентификатор-курсор за навигация.
  • Курсор — уникален маркер за позиция в набора от данни (обикновено ID, UUID, timestamp), който не се променя при вмъквания.
  • Стабилност — нови записи, добавени между заявките, не изместват курсора, елиминирайки дубликати и пропуски.
  • Производителност — заявката WHERE id > cursor ефективно използва индекса, без да губи скорост на големи набори.
  • Ограничение — курсорната пагинация не поддържа навигация по номер на страница (не може да се скочи на страница 5).

Какво е Cursor Pagination?

Cursor Pagination (курсорна пагинация) — метод за пагинация, при който сървърът връща заедно с данните специален указател — курсор. Клиентът използва този курсор в следващата заявка, за да получи следващата порция записи. Курсорът е уникалният идентификатор на последния елемент на текущата страница.

За разлика от Offset-пагинацията, където клиентът казва „дай ми страница 5 с 20 записа”, курсорната пагинация работи по различен начин: „дай ми 20 записа след записа с ID = 83”. Сървърът изпълнява заявка с условие WHERE id > 83 и LIMIT 20. Такъв подход гарантира, че всеки запис попада точно на една страница, независимо от вмъкванията.

Концепцията за курсорна пагинация получи широко разпространение благодарение на спецификацията Relay Connection (GraphQL), която направи cursor-based pagination стандарт за съвременните API. Relay определя формата на отговора: edges (масив от записи с курсори), pageInfo (hasNextPage, hasPreviousPage, startCursor, endCursor).

История на възникване

Cursor-pagination не е нова техника — тя се е използвала в базите данни много преди появата на уеб. В SQL се нарича keyset pagination или seek method. Методът става популярен в API след публикуването на спецификацията Relay през 2015 г., която формализира формата на курсора като base64-кодиран низ за унифициране на предаването чрез HTTP.

Как работи курсорната пагинация

Основният принцип на курсорната пагинация — заявката използва условие WHERE върху индексирано поле за позициониране, а не отместване. За посока напред се прилага WHERE id > last_id, за посока назад — WHERE id < first_id. B-tree индексът позволява намиране на първия запис след курсора за O(log n), което дава стабилно време за отговор.

sql
-- Получить 20 записей после курсора '83'
SELECT id, title, created_at
FROM posts
WHERE id < 83
ORDER BY id DESC
LIMIT 20;

-- Получить 20 записей ДО курсора '83' (назад)
SELECT id, title, created_at
FROM posts
WHERE id > 83
ORDER BY id ASC
LIMIT 20;

Формат на курсора

Курсорът може да бъде прост (стойност ID) или сложен (съставен от няколко полета). Простите курсори са първичният ключ на записа, например автоинкрементно id или UUID. Сложните курсори се използват за сортиране по не-уникални полета, например (created_at, id), където id гарантира уникалност при еднакви времеви отпечатъци.

Типичен API формат — курсор под формата на base64-кодиран низ. Сървърът декодира курсора, извлича стойността и изгражда SQL заявката. Base64-кодирането скрива вътрешната структура на курсора от клиента и позволява промяна на формата без загуба на обратна съвместимост. Клиентът получава курсорите в полето endCursor на отговора и ги предава като низ в следващата заявка.

Навигация напред и назад

Курсорната пагинация поддържа двупосочна навигация. За движение напред (next) се използва курсорът на последния елемент на текущата страница, за назад (previous) — курсорът на първия елемент. Параметрите after и before в заявката определят посоката: after взима записи след курсора, before — преди курсора.

Cursor vs Offset pagination

Изборът между курсорна и Offset-пагинация — един от ключовите архитектурни въпроси при проектирането на API. Всеки метод има силни и слаби страни. Cursor-pagination печели в сценарии с динамични данни, Offset — в сценарии с произволна навигация.

ХарактеристикаCursorOffset
Стабилност при вмъкванеВисока (без дубликати)Ниска (изместване на страници)
Производителност на големи набориO(log n) — стабилнаO(n) — спада с растежа
Навигация по номер на страницаНеДа (page=5)
Сложност на реализациятаСреднаНиска
Поддръжка в RESTcursor/before/afterpage/offset
Поддръжка в GraphQLСтандарт RelayНе се препоръчва

Защо Offset губи в мащаб

Offset-пагинацията извършва пълно сканиране на таблицата до позиция 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 напълно елиминира този сценарий: курсорът сочи конкретно място в набора и вмъкванията не променят позицията.

Реализация на Cursor Pagination

Нека разгледаме реализацията на курсорната пагинация на backend (Kotlin + Spring) и на клиента (Android + Retrofit). Сървърът приема параметри after, before, limit и връща списък от записи с курсори и 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

От страна на клиента, курсорната пагинация се реализира чрез PagingSource от Paging 3, където ключът е курсорът (Long). PagingSource.load получава LoadParams.key — курсора на последния зареден запис. LoadResult.Page връща данни и nextKey — курсор за следващата страница. Когато nextKey = null — пагинацията е завършена.

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

// PagingSource с ключ на курсора
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 курсорната пагинация се реализира чрез модела Connection Relay. Всеки тип има Connection (с pageInfo и edges) и Edge (node + cursor). Заявката предава параметри first, after, last, before. Сървърът връща масив от edges с курсори и pageInfo с hasNextPage/hasPreviousPage.

Кога да използваме Cursor Pagination

Cursor-pagination се препоръчва за API, работещи с динамични данни, където записите често се добавят или изтриват. Класически примери: новинарски поток в социална мрежа, съобщения в чат, история на транзакции, коментари към публикация. Във всички тези сценарии важни са консистентността и липсата на дубликати.

  • Чат и месинджъри — всяко ново съобщение се добавя в началото на списъка. Offset-пагинацията се обърква при всяко ново съобщение.
  • Социални мрежи и потоци — публикациите се публикуват непрекъснато. Курсорната пагинация гарантира, че потребителят не пропуска нито една публикация.
  • История на поръчки и транзакции — данните се променят по-рядко, но консистентността е критична за финансовото отчитане.
  • API с големи обеми данни — милиони записи. Cursor-pagination запазва производителността там, където Offset започва да забавя.
  • GraphQL API — стандартът Relay изисква използване на cursor-based pagination според спецификацията.
  • Мобилни приложения с безкрайно скролване — потребителят скролва надолу, зареждайки нови порции. Курсорният подход осигурява плавно UX без дубликати.

Кога Cursor-pagination не е подходяща

Съществуват сценарии, при които Offset-пагинацията е по-удобна: административни панели, където е необходима навигация по номер на страница; търсене с пагинация, където резултатите могат да се променят; отчети и аналитика, където е необходима фиксирана връзка към страница 5. В тези случаи предимствата на курсора не надвишават сложността на реализацията.

Курсорната пагинация не поддържа „скок” до произволна страница — потребителят не може да кликне „Страница 5” и да отиде до нея. Това е архитектурно ограничение: за изчисляване на общия брой страници е необходимо отделна COUNT заявка, която може да бъде скъпа за големи таблици. В такива случаи хибриден подход: cursor за данни + count за пагинация.

Често задавани въпроси

Какво е курсор в Cursor Pagination?

Курсорът е уникален идентификатор на запис, който показва позицията в набора от данни. Може да бъде прост (ID на запис) или сложен (няколко полета). Клиентът получава курсора на последния запис на страницата и го предава в следващата заявка за получаване на следващата порция.

С какво Cursor Pagination е по-добра от Offset?

Cursor-pagination не е податлива на изместване при добавяне на нови записи — всеки елемент попада точно на една страница. Също така запазва скоростта на големи обеми благодарение на използването на индекси вместо сканиране на първите n реда. Offset е по-прост, но нестабилен за динамични данни.

Може ли да се реализира курсорна пагинация без GraphQL?

Да, курсорната пагинация не е обвързана с GraphQL. Може да се реализира във всеки REST API чрез предаване на курсора като query параметър ?after=83&limit=20. Отговорът трябва да съдържа pageInfo с endCursor и hasNextPage — това позволява на клиента да управлява зареждането без познаване на вътрешната структура на курсора.

Кой курсор да използваме — ID, UUID или timestamp?

Автоинкременно ID — оптимален избор: монотонно нараства, не се променя, ефективно се индексира. UUID v7 (подреден по време) също е подходящ. Timestamp може да дава дубликати при еднакво време, затова се комбинира с ID: (created_at, id) за гарантиране на уникалност на курсора.

Как да разберем общия брой страници при курсорна пагинация?

Курсорната пагинация не предоставя общ брой страници — това е нейното ограничение. Ако е необходима информация за total, изпълнете отделна COUNT заявка със същите филтри. За големи таблици използвайте приблизително преброяване чрез EXPLAIN или кеширан total от аналитика.

Резюме

  • Cursor Pagination — метод за пагинация с навигация по уникален идентификатор на запис вместо отместване.
  • Курсорът гарантира стабилност на набора при вмъкване: новите записи не изместват вече заредените страници.
  • Производителността на големи обеми данни остава висока (O(log n)) благодарение на използването на B-tree индекси.
  • Cursor-pagination е подходяща за динамични данни: чатове, новинарски потоци, транзакции, коментари.
  • Основно ограничение — липса на навигация по номер на страница и невъзможност за скок до произволна страница.
  • Реализацията използва WHERE id след курсор, параметри after/before и pageInfo в отговора.
  • Стандарт — Relay Connection GraphQL, но REST API с курсорни параметри също е широко разпространен.

Ще разработим мобилно приложение под ключ

IT Sectr създава iOS и Android приложения за стартъпи и бизнеси от 2017 г. Ще ви консултираме и ще предложим най-доброто решение.

Обсъдете проекта

Прочетете също