Cursor Pagination — sıralanmış qeyd dəstində naviqasiya üçün unikal kursor istifadə edən məlumatların səhifələnmə üsuludur. GraphQL Specification (2025)-ə görə, kursor səhifələnməsi dinamik məlumatlarla işləyən API-lər üçün tövsiyə olunan standartdır. Cursor Pagination Offset yanaşmasının əsas çatışmazlıqlarını aradan qaldırır: əlavələr zamanı qeyri-sabitlik və böyük yerdəyişmələrdə performans itkisi.
Əsas məqamlar
Cursor Pagination (kursor səhifələnməsi) — serverin məlumatlarla birlikdə xüsusi göstərici — kursor qaytardığı səhifələnmə üsuludur. Müştəri növbəti sorğuda bu kursoru istifadə edərək növbəti qeyd hissəsini əldə edir. Kursor cari səhifənin son elementinin unikal identifikatorudur.
Offset səhifələnməsindən fərqli olaraq, burada müştəri “mənə 20 qeyddən 5-ci səhifəni ver” demir, kursor səhifələnməsi fərqli işləyir: “mənə ID = 83 olan qeyddən sonra 20 qeyd ver”. Server WHERE id > 83 şərti və LIMIT 20 ilə sorğu yerinə yetirir. Bu yanaşma hər bir qeydin əlavələrdən asılı olmayaraq dəqiq bir səhifəyə düşəcəyinə zəmanət verir.
Kursor səhifələnməsi konsepsiyası Relay Connection (GraphQL) spesifikasiyası sayəsində geniş yayılmışdır və cursor-based pagination-ı müasir API-lər üçün standart etmişdir. Relay cavab formatını müəyyənləşdirir: edges (kursorlarla qeydlər massivi), pageInfo (hasNextPage, hasPreviousPage, startCursor, endCursor).
Cursor-pagination yeni texnika deyil — veb meydana çıxmamışdan çox əvvəl verilənlər bazalarında istifadə olunurdu. SQL-də buna keyset pagination və ya seek method deyilir. Metod 2015-ci ildə Relay spesifikasiyasının dərcindən sonra API-lərdə populyarlaşdı və kursor formatını HTTP vasitəsilə ötürmə üçün base64 kodlaşdırılmış sətir kimi rəsmiləşdirdi.
Kursor səhifələnməsinin əsas prinsipi — sorğu yerdəyişmə deyil, mövqeləşdirmə üçün indeksləşdirilmiş sahədə WHERE şərtindən istifadə edir. İrəli istiqamət üçün WHERE id > last_id, geri istiqamət üçün isə WHERE id < first_id tətbiq olunur. B-tree indeksi kursor daxil olduqdan sonra ilk qeydi O(log n) müddətində tapmağa imkan verir ki, bu da sabit cavab müddətini təmin edir.
-- Kursor '83'-dən sonra 20 qeyd əldə et
SELECT id, title, created_at
FROM posts
WHERE id < 83
ORDER BY id DESC
LIMIT 20;
-- Kursor '83'-dən ƏVVƏL 20 qeyd əldə et (geri)
SELECT id, title, created_at
FROM posts
WHERE id > 83
ORDER BY id ASC
LIMIT 20;
Kursor sadə (ID dəyəri) və ya mürəkkəb (bir neçə sahədən ibarət) ola bilər. Sadə kursorlar qeydin əsas açarıdır, məsələn autoinkrement id və ya UUID. Mürəkkəb kursorlar qeyri-unikal sahələr üzrə çeşidləmə üçün istifadə olunur, məsələn (created_at, id), burada id eyni zaman damğalarında unikallığı təmin edir.
Tipik API formatı — kursor base64 kodlaşdırılmış sətir şəklindədir. Server kursoru dekodlaşdırır, dəyəri çıxarır və SQL sorğusu qurur. Base64 kodlaşdırılması kursorun daxili strukturunu müştəridən gizlədir və geriyə uyğunluğu itirmədən formatı dəyişməyə imkan verir. Müştəri kursorları cavabın endCursor sahəsində alır və onları növbəti sorğuda sətir kimi ötürür.
Kursor səhifələnməsi ikitərəfli naviqasiyanı dəstəkləyir. İrəli hərəkət (next) üçün cari səhifənin son elementinin kursoru, geri (previous) üçün — ilk elementin kursoru istifadə olunur. Sorğuda after və before parametrləri istiqaməti müəyyənləşdirir: after kursor daxil olduqdan sonra qeydləri götürür, before — kursor daxil olana qədər.
Kursor və Offset səhifələnməsi arasında seçim API dizayn edərkən əsas memarlıq məsələlərindən biridir. Hər metodun güclü və zəif tərəfləri var. Cursor-pagination dinamik məlumat ssenarilərində, Offset — ixtiyari naviqasiya ssenarilərində üstünlük təşkil edir.
| Xüsusiyyət | Cursor | Offset |
|---|---|---|
| Əlavələr zamanı sabitlik | Yüksək (dublikatsız) | Aşağı (səhifə yerdəyişməsi) |
| Böyük dəstlərdə performans | O(log n) — sabit | O(n) — artdıqca azalır |
| Səhifə nömrəsi ilə naviqasiya | Yox | Bəli (page=5) |
| Tətbiq mürəkkəbliyi | Orta | Aşağı |
| REST dəstəyi | cursor/before/after | page/offset |
| GraphQL dəstəyi | Relay standartı | Tövsiyə edilmir |
Offset səhifələnməsi OFFSET mövqeyinə qədər tam cədvəl skan etməsini yerinə yetirir. Offset=100000 olduqda, LIMIT 20 olsa belə, verilənlər bazası 100000 sətri oxuyur və atlayır. MySQL və PostgreSQL OFFSET-i optimallaşdıra bilmir — bu SQL-də LIMIT/OFFSET tətbiqinin xüsusiyyətidir. Cursor-pagination B-tree indeksindən istifadə edir, mövqeyi O(log n) müddətində tapır.
Offset-in əlavə problemi — geri səhifələnmə zamanı qeydlərin „atlanması”. İstifadəçi 5-ci səhifəni yükləyibsə və bu anda yeni qeydlər əlavə olunubsa, 6-cı səhifəni sorğulayarkən 5-ci səhifədən qeydi yenidən görəcək və ya yeni qeydləri atlayacaq. Cursor-pagination bu ssenarini tamamilə aradan qaldırır: kursor dəstdə konkret yeri göstərir və əlavələr mövqeyi dəyişmir.
Kursor səhifələnməsinin backenddə (Kotlin + Spring) və müştəridə (Android + Retrofit) tətbiqini nəzərdən keçirək. Server after, before, limit parametrlərini qəbul edir və kursorlar və pageInfo ilə qeyd siyahısını qaytarır. Tipik cavab səhifələnmə UI-ni idarə etmək üçün hasNextPage və hasPreviousPage ehtiva edir.
@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
)
)
}
Müştəri tərəfdə kursor səhifələnməsi Paging 3-dən PagingSource vasitəsilə tətbiq olunur, burada açar kursor (Long) rolunu oynayır. PagingSource.load LoadParams.key — son yüklənmiş qeydin kursorunu alır. LoadResult.Page məlumatları və nextKey — növbəti səhifə üçün kursoru qaytarır. NextKey = null olduqda — səhifələnmə tamamlanır.
// Retrofit API
interface PostApi {
@GET("posts")
suspend fun getPosts(
@Query("after") after: Long?,
@Query("limit") limit: Int = 20
): CursorResponse<Post>
}
// PagingSource kursor açarı ilə
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-də kursor səhifələnməsi Relay Connection nümunəsi vasitəsilə tətbiq olunur. Hər tip Connection (pageInfo və edges ilə) və Edge (node + cursor) var. Sorğu first, after, last, before parametrlərini ötürür. Server qaytarır kursorlarla edges massivi və hasNextPage/hasPreviousPage ilə pageInfo.
Cursor-pagination qeydlərin tez-tez əlavə olunduğu və ya silindiyi dinamik məlumatlarla işləyən API-lər üçün tövsiyə olunur. Klassik nümunələr: sosial şəbəkədə xəbər lentləri, söhbət mesajları, əməliyyat tarixçəsi, post şərhləri. Bütün bu ssenarilərdə ardıcıllıq və dublikatların olmaması vacibdir.
Offset səhifələnməsinin daha əlverişli olduğu ssenarilər mövcuddur: səhifə nömrəsi ilə naviqasiyanın tələb olunduğu inzibati panellər; nəticələrin dəyişə biləcəyi səhifələmə ilə axtarış; 5-ci səhifəyə sabit keçidin lazım olduğu hesabatlar və analitika. Bu hallarda kursorun üstünlükləri tətbiq mürəkkəbliyindən üstün deyil.
Kursor səhifələnməsi ixtiyari səhifəyə „atlanmanı” dəstəkləmir — istifadəçi “Səhifə 5” düyməsini sıxıb ora keçə bilməz. Bu memarlıq məhdudiyyətidir: ümumi səhifə sayını hesablamaq üçün ayrıca COUNT sorğusu tələb olunur, bu da böyük cədvəllər üçün baha başa gələ bilər. Belə hallarda hibrid yanaşma: məlumatlar üçün cursor + səhifələmə üçün count.
Tez-tez verilən suallar
Kursor — məlumat dəstində mövqeyi göstərən qeydin unikal identifikatorudur. Sadə (qeyd ID-si) və ya mürəkkəb (bir neçə sahə) ola bilər. Müştəri səhifənin son qeydinin kursorunu alır və növbəti hissəni əldə etmək üçün onu növbəti sorğuda ötürür.
Cursor-pagination yeni qeydlər əlavə edildikdə yerdəyişməyə məruz qalmır — hər element dəqiq bir səfihəyə düşür. İlk n sətirləri skan etmək əvəzinə indekslərdən istifadə etməklə böyük həcmlərdə sürəti qoruyur. Offset daha sadədir, lakin dinamik məlumatlar üçün qeyri-sabitdir.
Bəli, kursor səhifələnməsi GraphQL-ə bağlı deyil. İstənilən REST API-də kursoru sorğu parametri kimi ötürməklə tətbiq edilə bilər ?after=83&limit=20. Cavabda endCursor və hasNextPage ilə pageInfo olmalıdır — bu, müştəriyə kursorun daxili strukturunu bilmədən yükləməni idarə etməyə imkan verir.
Autoinkrement ID — optimal seçim: monotonik artır, dəyişmir, səmərəli indekslənir. UUID v7 (zaman sıralı) da uyğundur. Timestamp eyni vaxtda dublikatlar verə bilər, buna görə də ID ilə birləşdirilir: (created_at, id) kursorun unikallığını təmin etmək üçün.
Kursor səhifələnməsi ümumi səhifə sayını təqdim etmir — bu onun məhdudiyyətidir. Total haqqında məlumat lazımdırsa, eyni filtrlərlə ayrıca COUNT sorğusu yerinə yetirin. Böyük cədvəllər üçün EXPLAIN vasitəsilə təxmini hesablama və ya analitikadan keşlənmiş total istifadə edin.
Nəticə
Açar təslim mobil tətbiq hazırlayacağıq
IT Sectr 2017-ci ildən startaplar və bizneslər üçün iOS və Android tətbiqləri yaradır. Sizə məsləhət verəcəyik və ən yaxşı həlli təklif edəcəyik.
Həm də oxuyun