Cursor Pagination — paraan ng pag-pagina ng data na gumagamit ng natatanging cursor para mag-navigate sa isang nakaayos na set ng mga record. Ayon sa GraphQL Specification (2025), ang cursor pagination ay ang inirerekomendang pamantayan para sa mga API na nagtatrabaho sa dinamikong data. Cursor Pagination inaalis ang mga pangunahing disadvantage ng Offset approach: kawalan ng katatagan sa pagpasok at pagbaba ng performance sa malalaking paglilipat.
Mga pangunahing punto
Cursor Pagination (cursor pagina) — paraan ng pagina kung saan ang server ay nagbabalik kasama ng data ng isang espesyal na pointer — cursor. Ginagamit ng client ang cursor na ito sa susunod na kahilingan para makuha ang susunod na bahagi ng mga record. Ang cursor ay ang natatanging identifier ng huling elemento ng kasalukuyang pahina.
Hindi tulad ng Offset-pagination, kung saan sinasabi ng client “bigyan mo ako ng pahina 5 na may 20 record”, ang cursor pagina ay gumagana nang iba: “bigyan mo ako ng 20 record pagkatapos ng record na may ID = 83”. Isinasagawa ng server ang query na may kondisyong WHERE id > 83 at LIMIT 20. Ang pamamaraang ito ay ginagarantiyahan na ang bawat record ay mapupunta nang eksakto sa isang pahina anuman ang mga pagpasok.
Ang konsepto ng cursor pagina ay naging laganap dahil sa specifikasyon ng Relay Connection (GraphQL), na ginawang pamantayan ang cursor-based pagination para sa mga modernong API. Tinutukoy ng Relay ang format ng tugon: edges (array ng mga record na may cursor), pageInfo (hasNextPage, hasPreviousPage, startCursor, endCursor).
Ang cursor-pagination ay hindi isang bagong teknik — ginamit ito sa mga database bago pa lumitaw ang web. Sa SQL ito ay tinatawag na keyset pagination o seek method. Naging popular ang paraan sa mga API pagkatapos ng paglalathala ng specifikasyon ng Relay noong 2015, na nagpormalisa sa format ng cursor bilang base64-encoded string para sa pagkakapareho ng pagpapadala sa pamamagitan ng HTTP.
Ang pangunahing prinsipyo ng cursor pagina — ang query ay gumagamit ng WHERE condition sa isang naka-index na field para sa pagpoposisyon, hindi paglilipat. Para sa pasulong na direksyon ginagamit ang WHERE id > last_id, para sa pabalik na direksyon — WHERE id < first_id. Ang B-tree index ay nagbibigay-daan na mahanap ang unang record pagkatapos ng cursor sa O(log n), na nagbibigay ng stable na oras ng tugon.
-- Kumuha ng 20 record pagkatapos ng cursor '83'
SELECT id, title, created_at
FROM posts
WHERE id < 83
ORDER BY id DESC
LIMIT 20;
-- Kumuha ng 20 record BAGO ang cursor '83' (pabalik)
SELECT id, title, created_at
FROM posts
WHERE id > 83
ORDER BY id ASC
LIMIT 20;
Ang cursor ay maaaring simple (halaga ng ID) o komplikado (binubuo ng maraming field). Ang mga simpleng cursor ay ang pangunahing key ng record, halimbawa auto-increment id o UUID. Ang mga komplikadong cursor ay ginagamit para sa pag-uuri ayon sa hindi natatanging mga field, halimbawa (created_at, id), kung saan ginagarantiyahan ng id ang pagiging natatangi sa magkaparehong timestamp.
Typical na format ng API — cursor sa anyo ng base64-encoded string. Ide-decode ng server ang cursor, kukunin ang halaga at bubuo ng SQL query. Ang Base64 encoding ay nagtatago ng panloob na istraktura ng cursor mula sa client at nagpapahintulot na baguhin ang format nang hindi nawawala ang backward compatibility. Natatanggap ng client ang mga cursor sa endCursor field ng tugon at ipinapasa ang mga ito bilang string sa susunod na kahilingan.
Ang cursor pagina ay sumusuporta sa bidirectional nabigasyon. Para sa pasulong na galaw (next) ginagamit ang cursor ng huling elemento ng kasalukuyang pahina, para sa pabalik (previous) — cursor ng unang elemento. Ang mga parameter na after at before sa kahilingan ay tumutukoy sa direksyon: after kumukuha ng mga record pagkatapos ng cursor, before — bago ang cursor.
Ang pagpili sa pagitan ng cursor at Offset-pagination — isa sa mga pangunahing katanungang arkitektural sa pagdidisenyo ng API. Ang bawat pamamaraan ay may malakas at mahinang panig. Cursor-pagination nananalo sa mga senaryo na may dinamikong data, Offset — sa mga senaryo na may arbitraryong nabigasyon.
| Katangian | Cursor | Offset |
|---|---|---|
| Katatagan sa pagpasok | Mataas (walang duplicate) | Mababa (paglilipat ng pahina) |
| Performance sa malalaking set | O(log n) — stable | O(n) — bumababa sa paglaki |
| Nabigasyon ayon sa numero ng pahina | Hindi | Oo (page=5) |
| Kompleksidad ng implementasyon | Katamtaman | Mababa |
| Suporta sa REST | cursor/before/after | page/offset |
| Suporta sa GraphQL | Pamantayan ng Relay | Hindi inirerekomenda |
Ang Offset-pagination ay nagsasagawa ng buong pag-scan ng talahanayan hanggang sa posisyong OFFSET. Sa offset=100000, binabasa at nilalaktawan ng database ang 100000 na row, kahit na ang LIMIT ay 20. Ang MySQL at PostgreSQL ay hindi maaaring i-optimize ang OFFSET — ito ay katangian ng implementasyon ng LIMIT/OFFSET sa SQL. Ang Cursor-pagination ay gumagamit ng B-tree index, na nakakahanap ng posisyon sa O(log n).
Ang karagdagang problema ng Offset — “paglaktaw” ng mga record sa pabalik na pagina. Kung ang user ay nag-load ng pahina 5, at sa sandaling iyon ay may mga bagong record na idinagdag, sa paghingi ng pahina 6 makikita niya ang record mula sa pahina 5 muli o lalaktawan ang mga bago. Ang Cursor-pagination ay ganap na inaalis ang senaryong ito: ang cursor ay tumuturo sa isang tiyak na lugar sa set, at ang mga pagpasok ay hindi nagbabago ng posisyon.
Tingnan natin ang implementasyon ng cursor pagina sa backend (Kotlin + Spring) at sa client (Android + Retrofit). Ang server ay tumatanggap ng mga parameter na after, before, limit at nagbabalik ng listahan ng mga record na may cursor at pageInfo. Ang typical na tugon ay naglalaman ng hasNextPage at hasPreviousPage para sa pamamahala ng UI ng pagina.
@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
)
)
}
Sa panig ng client, ang cursor pagina ay ipinapatupad sa pamamagitan ng PagingSource mula sa Paging 3, kung saan ang susi ay ang cursor (Long). Ang PagingSource.load ay tumatanggap ng LoadParams.key — cursor ng huling na-load na record. Ang LoadResult.Page ay nagbabalik ng data at nextKey — cursor para sa susunod na pahina. Kapag ang nextKey = null — ang pagina ay natapos na.
// Retrofit API
interface PostApi {
@GET("posts")
suspend fun getPosts(
@Query("after") after: Long?,
@Query("limit") limit: Int = 20
): CursorResponse<Post>
}
// PagingSource na may cursor key
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)
}
}
Sa GraphQL, ang cursor pagina ay ipinapatupad sa pamamagitan ng pattern ng Connection Relay. Ang bawat uri ay may Connection (na may pageInfo at edges) at Edge (node + cursor). Ang kahilingan ay nagpapadala ng mga parameter na first, after, last, before. Ang server ay nagbabalik ng array ng edges na may cursor at pageInfo na may hasNextPage/hasPreviousPage.
Ang cursor-pagination ay inirerekomenda para sa mga API na nagtatrabaho sa dinamikong data, kung saan ang mga record ay madalas na idinadagdag o binubura. Mga klasikong halimbawa: feed ng balita sa social network, mga mensahe sa chat, kasaysayan ng transaksyon, mga komento sa post. Sa lahat ng mga senaryong ito, mahalaga ang pagkakapare-pareho at kawalan ng mga duplicate.
May mga senaryo kung saan ang Offset-pagination ay mas maginhawa: mga administrative panel kung saan kinakailangan ang nabigasyon ayon sa numero ng pahina; paghahanap na may pagina kung saan ang mga resulta ay maaaring magbago; mga ulat at analytics kung saan kailangan ang nakapirming link sa pahina 5. Sa mga kasong ito, ang mga bentahe ng cursor ay hindi hihigit sa kompleksidad ng implementasyon.
Ang cursor pagina ay hindi sumusuporta sa “ pagtalon” sa arbitraryong pahina — hindi maaaring mag-click ang user ng “Pahina 5” at pumunta doon. Ito ay isang limitasyong arkitektural: para sa pagkalkula ng kabuuang bilang ng mga pahina kinakailangan ang isang hiwalay na COUNT query, na maaaring mahal para sa malalaking talahanayan. Sa ganitong mga kaso, hybrid na diskarte: cursor para sa data + count para sa pagina.
Mga madalas itanong
Ang cursor ay ang natatanging identifier ng record na nagpapakita ng posisyon sa set ng data. Maaaring simple (ID ng record) o komplikado (maraming field). Ang client ay tumatanggap ng cursor ng huling record ng pahina at ipinapasa ito sa susunod na kahilingan para makuha ang susunod na bahagi.
Ang cursor-pagination ay hindi napapailalim sa paglilipat sa pagdaragdag ng mga bagong record — bawat elemento ay napupunta nang eksakto sa isang pahina. Pinapanatili rin nito ang bilis sa malalaking volume dahil sa paggamit ng mga index sa halip na pag-scan ng unang n row. Ang Offset ay mas simple ngunit hindi matatag para sa dinamikong data.
Oo, ang cursor pagina ay hindi nakatali sa GraphQL. Maaari itong ipatupad sa anumang REST API sa pamamagitan ng pagpapasa ng cursor bilang query parameter ?after=83&limit=20. Ang tugon ay dapat maglaman ng pageInfo na may endCursor at hasNextPage — ito ay nagbibigay-daan sa client na pamahalaan ang pag-load nang hindi alam ang panloob na istraktura ng cursor.
Auto-increment ID — pinakamainam na pagpili: monotonikong tumataas, hindi nagbabago, mahusay na nai-index. Ang UUID v7 (nakaayos ayon sa oras) ay angkop din. Ang timestamp ay maaaring magbigay ng mga duplicate sa parehong oras, kaya pinagsama ito sa ID: (created_at, id) para sa garantiya ng pagiging natatangi ng cursor.
Ang cursor pagina ay hindi nagbibigay ng kabuuang bilang ng mga pahina — ito ang limitasyon nito. Kung kailangan ang impormasyon tungkol sa total, magsagawa ng hiwalay na COUNT query na may parehong mga filter. Para sa malalaking talahanayan, gumamit ng tinatayang pagbibilang sa pamamagitan ng EXPLAIN o naka-cache na total mula sa analytics.
Buod
Gagawa kami ng mobile application na turnkey
Gumagawa ang IT Sectr ng mga iOS at Android application para sa mga startup at negosyo mula noong 2017. Magpapayo kami sa iyo at magmumungkahi ng pinakamahusay na solusyon.
Basahin din