Cursor Pagination — tartiblangan yozuvlar to'plamida navigatsiya qilish uchun noyob kursor yordamida ma'lumotlarni sahifalash usulidir. GraphQL Specification (2025) ga ko'ra, kursorli sahifalash dinamik ma'lumotlar bilan ishlaydigan API-lar uchun tavsiya etilgan standartdir. Cursor Pagination Offset yondashuvining asosiy kamchiliklarini bartaraf qiladi: qo'shishlardagi beqarorlik va katta siljishlarda ishlashning pasayishi.
Asosiy fikrlar
Cursor Pagination (kursorli sahifalash) — server ma'lumotlar bilan birga maxsus ko'rsatkich — kursor qaytaradigan sahifalash usuli. Mijoz keyingi so'rovda ushbu kursor yordamida yozuvlarning keyingi qismini oladi. Kursor joriy sahifaning oxirgi elementining noyob identifikatoridir.
Offset-sahifalashdan farqli o'laroq, bu yerda mijoz “menga 20 yozuvdan 5-sahifani ber” demaydi, kursorli sahifalash boshqacha ishlaydi: “menga ID = 83 bo'lgan yozuvdan keyin 20 yozuv ber”. Server WHERE id > 83 sharti va LIMIT 20 bilan so'rovni bajaradi. Bunday yondashuv har bir yozuv qo'shilishlardan qat'iy nazar aniq bir sahifaga tushishini kafolatlaydi.
Kursorli sahifalash kontseptsiyasi Relay Connection (GraphQL) spetsifikatsiyasi tufayli keng tarqalgan va cursor-based paginationni zamonaviy API-lar uchun standartga aylantirgan. Relay javob formatini belgilaydi: edges (kursorlar bilan yozuvlar massivi), pageInfo (hasNextPage, hasPreviousPage, startCursor, endCursor).
Cursor-pagination yangi texnika emas — veb paydo bo'lishidan ancha oldin ma'lumotlar bazalarida ishlatilgan. SQLda bu keyset pagination yoki seek method deb ataladi. Usul 2015-yilda Relay spetsifikatsiyasi nashr etilgandan so'ng API-larda ommalashdi va kursor formatini HTTP orqali uzatish uchun base64 kodlangan satr sifatida rasmiylashtirdi.
Kursorli sahifalashning asosiy prinsipi — so'rov siljish emas, balki joylashish uchun indekslangan maydonda WHERE shartidan foydalanadi. Oldinga yo'nalish uchun WHERE id > last_id, orqaga yo'nalish uchun WHERE id < first_id qo'llaniladi. B-tree indeksi kursor orqali birinchi yozuvni O(log n) vaqtida topish imkonini beradi, bu barqaror javob vaqtini ta'minlaydi.
-- '83' kursoridan keyin 20 ta yozuv olish
SELECT id, title, created_at
FROM posts
WHERE id < 83
ORDER BY id DESC
LIMIT 20;
-- '83' kursoridan OLDIN 20 ta yozuv olish (orqaga)
SELECT id, title, created_at
FROM posts
WHERE id > 83
ORDER BY id ASC
LIMIT 20;
Kursor oddiy (ID qiymati) yoki murakkab (bir nechta maydonlardan tashkil topgan) bo'lishi mumkin. Oddiy kursorlar yozuvning asosiy kalitidir, masalan avtomatik o'suvchi id yoki UUID. Murakkab kursorlar noyob bo'lmagan maydonlar bo'yicha saralash uchun ishlatiladi, masalan (created_at, id), bunda id bir xil vaqt belgilarida noyoblikni kafolatlaydi.
Odatdagi API formati — kursor base64 kodlangan satr ko'rinishida. Server kursorni dekodlaydi, qiymatni chiqaradi va SQL so'rovini tuzadi. Base64 kodlash kursorning ichki tuzilishini mijozdan yashiradi va orqaga moslikni yo'qotmasdan formatni o'zgartirish imkonini beradi. Mijoz kursorlarni javobning endCursor maydonida oladi va keyingi so'rovda satr sifatida uzatadi.
Kursorli sahifalash ikki tomonlama navigatsiyani qo'llab-quvvatlaydi. Oldinga harakat (next) uchun joriy sahifaning oxirgi elementining kursori, orqaga (previous) uchun — birinchi elementning kursori ishlatiladi. So'rovdagi after va before parametrlari yo'nalishni belgilaydi: after kursor orqali yozuvlarni oladi, before — kursor oldidan.
Kursor va Offset sahifalash o'rtasidagi tanlov API loyihalashda asosiy arxitektura masalalaridan biridir. Har bir usulning kuchli va zaif tomonlari bor. Cursor-pagination dinamik ma'lumotlar ssenariylarida, Offset — ixtiyoriy navigatsiya ssenariylarida ustunlik qiladi.
| Xususiyat | Cursor | Offset |
|---|---|---|
| Qo'shishlardagi barqarorlik | Yuqori (dublikatsiz) | Past (sahifa siljishi) |
| Katta to'plamlardagi unumdorlik | O(log n) — barqaror | O(n) — o'sish bilan kamayadi |
| Sahifa raqami bo'yicha navigatsiya | Yo'q | Ha (page=5) |
| Amalga oshirish murakkabligi | O'rta | Past |
| REST qo'llab-quvvatlashi | cursor/before/after | page/offset |
| GraphQL qo'llab-quvvatlashi | Relay standarti | Tavsiya etilmaydi |
Offset sahifalash OFFSET pozitsiyasigacha to'liq jadvalni skanerlashni amalga oshiradi. Offset=100000 bo'lganda, LIMIT 20 bo'lsa ham, ma'lumotlar bazasi 100000 qatorni o'qiydi va o'tkazib yuboradi. MySQL va PostgreSQL OFFSETni optimallashtira olmaydi — bu SQLda LIMIT/OFFSETni amalga oshirish xususiyati. Cursor-pagination B-tree indeksidan foydalanadi, pozitsiyani O(log n) vaqtida topadi.
Offsetning qo'shimcha muammosi — orqaga sahifalashda yozuvlarning “o'tkazib yuborilishi”. Agar foydalanuvchi 5-sahifani yuklagan bo'lsa va shu vaqtda yangi yozuvlar qo'shilgan bo'lsa, 6-sahifani so'raganda, 5-sahifadagi yozuvni yana ko'radi yoki yangi yozuvlarni o'tkazib yuboradi. Cursor-pagination bu ssenariyni butunlay yo'q qiladi: kursor to'plamdagi aniq joyni ko'rsatadi va qo'shishlar pozitsiyani o'zgartirmaydi.
Kursorli sahifalashni backendda (Kotlin + Spring) va mijozda (Android + Retrofit) amalga oshirishni ko'rib chiqamiz. Server after, before, limit parametrlarini qabul qiladi va kursorlar va pageInfo bilan yozuvlar ro'yxatini qaytaradi. Oddiy javob sahifalash UI-ni boshqarish uchun hasNextPage va hasPreviousPage ni o'z ichiga oladi.
@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
)
)
}
Mijoz tomonida kursorli sahifalash Paging 3 dan PagingSource orqali amalga oshiriladi, bunda kalit kursor (Long) hisoblanadi. PagingSource.load LoadParams.key — oxirgi yuklangan yozuvning kursorini oladi. LoadResult.Page ma'lumotlarni va nextKey — keyingi sahifa uchun kursorini qaytaradi. NextKey = null bo'lganda — sahifalash tugallanadi.
// Retrofit API
interface PostApi {
@GET("posts")
suspend fun getPosts(
@Query("after") after: Long?,
@Query("limit") limit: Int = 20
): CursorResponse<Post>
}
// PagingSource kursor kaliti bilan
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)
}
}
GraphQLda kursorli sahifalash Relay Connection namunasi orqali amalga oshiriladi. Har bir tur Connection (pageInfo va edges bilan) va Edge (node + cursor) ga ega. So'rov first, after, last, before parametrlarini uzatadi. Server qaytaradi kursorlar bilan edges massivi va hasNextPage/hasPreviousPage bilan pageInfo.
Cursor-pagination yozuvlar tez-tez qo'shiladigan yoki o'chiriladigan dinamik ma'lumotlar bilan ishlaydigan API-lar uchun tavsiya etiladi. Klassik misollar: ijtimoiy tarmoqdagi yangiliklar lentasi, chat xabarlari, tranzaksiyalar tarixi, post sharhlari. Barcha bu ssenariylarda izchillik va dublikatlarning yo'qligi muhimdir.
Offset sahifalash qulayroq bo'lgan ssenariylar mavjud: sahifa raqami bo'yicha navigatsiya kerak bo'lgan ma'muriy panellar; natijalar o'zgarishi mumkin bo'lgan sahifalash bilan qidirish; 5-sahifaga barqaror havola kerak bo'lgan hisobotlar va analitika. Bunday hollarda kursorning afzalliklari amalga oshirish murakkabligidan ustun kelmaydi.
Kursorli sahifalash ixtiyoriy sahifaga “sakrashni” qo'llab-quvvatlamaydi — foydalanuvchi “5-Sahifa” tugmasini bosib, unga o'ta olmaydi. Bu arxitektura cheklovidir: sahifalarning umumiy sonini hisoblash uchun alohida COUNT so'rovi talab qilinadi, bu katta jadvallar uchun qimmat bo'lishi mumkin. Bunday hollarda gibrid yondashuv: ma'lumotlar uchun cursor + sahifalash uchun count.
Tez-tez beriladigan savollar
Kursor — ma'lumotlar to'plamida pozitsiyani ko'rsatadigan yozuvning noyob identifikatoridir. Oddiy (yozuv IDsi) yoki murakkab (bir nechta maydon) bo'lishi mumkin. Mijoz sahifaning oxirgi yozuvi kursorini oladi va keyingi qismni olish uchun uni keyingi so'rovda uzatadi.
Cursor-pagination yangi yozuvlar qo'shilganda siljishga duch kelmaydi — har bir element aniq bir sahifaga tushadi. Bundan tashqari, katta hajmlarda indekslardan foydalanish orqali tezlikni saqlaydi. Offset oddiyroq, lekin dinamik ma'lumotlar uchun barqaror emas.
Ha, kursorli sahifalash GraphQLga bog'liq emas. Uni istalgan REST APIda kursorni so'rov parametri sifatida uzatish orqali amalga oshirish mumkin ?after=83&limit=20. Javob endCursor va hasNextPage bilan pageInfo ni o'z ichiga olishi kerak — bu mijozga kursorning ichki tuzilishini bilmasdan yuklashni boshqarish imkonini beradi.
Avtomatik o'suvchi ID — optimal tanlov: monotonik o'sadi, o'zgarmaydi, samarali indekslanadi. UUID v7 (vaqt bo'yicha tartiblangan) ham mos keladi. Timestamp bir xil vaqtda dublikatlar berishi mumkin, shuning uchun ID bilan birlashtiriladi: (created_at, id) kursorning noyobligini kafolatlash uchun.
Kursorli sahifalash umumiy sahifalar sonini bermaydi — bu uning cheklovidir. Total haqida ma'lumot kerak bo'lsa, bir xil filtrlar bilan alohida COUNT so'rovini bajaring. Katta jadvallar uchun EXPLAIN orqali taxminiy hisoblash yoki analitikadan keshlangan totaldan foydalaning.
Xulosa
Biz kalit topshirig'i bilan mobil ilovani ishlab chiqamiz
IT Sectr 2017-yildan beri startaplar va korxonalar uchun iOS va Android ilovalarini yaratadi. Biz sizga maslahat beramiz va eng yaxshi yechimni taklif qilamiz.