Cursor Pagination হল একটি পৃষ্ঠাভিত্তিক ডেটা লোডিং পদ্ধতি যা একটি সুশৃঙ্খল রেকর্ড সেটে নেভিগেট করার জন্য একটি অনন্য কার্সার ব্যবহার করে। GraphQL Specification (2025) অনুসারে, কার্সার-ভিত্তিক পেজিনেশন গতিশীল ডেটার সাথে কাজ করা API-গুলির জন্য প্রস্তাবিত মান। Cursor pagination Offset পদ্ধতির প্রধান ত্রুটিগুলি দূর করে: সন্নিবেশের সময় অস্থিরতা এবং বড় অফসেটে কর্মক্ষমতার অবনতি।
মূল পয়েন্ট
Cursor Pagination একটি পৃষ্ঠাভিত্তিক লোডিং পদ্ধতি যেখানে সার্ভার ডেটার সাথে একটি বিশেষ পয়েন্টার — একটি কার্সার — ফেরত দেয়। ক্লায়েন্ট পরবর্তী ব্যাচের রেকর্ড পেতে পরবর্তী অনুরোধে এই কার্সার ব্যবহার করে। কার্সারটি বর্তমান পৃষ্ঠার শেষ উপাদানের একটি অনন্য শনাক্তকারী।
Offset পেজিনেশনের বিপরীতে, যেখানে ক্লায়েন্ট বলে “আমাকে 20টি রেকর্ডসহ পৃষ্ঠা 5 দিন,” কার্সার পেজিনেশন ভিন্নভাবে কাজ করে: “আমাকে ID = 83 রেকর্ডের পরে 20টি রেকর্ড দিন।” সার্ভার WHERE id > 83 এবং LIMIT 20 সহ একটি কোয়েরি কার্যকর করে। এই পদ্ধতি নিশ্চিত করে যে প্রতিটি রেকর্ড সন্নিবেশ নির্বিশেষে ঠিক একটি পৃষ্ঠায় পড়ে।
কার্সার পেজিনেশনের ধারণাটি Relay Connection (GraphQL) স্পেসিফিকেশনের জন্য ব্যাপকভাবে গৃহীত হয়েছে, যা কার্সার-ভিত্তিক পেজিনেশনকে আধুনিক API-র জন্য মান করে তুলেছে। Relay প্রতিক্রিয়া বিন্যাস সংজ্ঞায়িত করে: edges (কার্সারসহ রেকর্ডের অ্যারে), pageInfo (hasNextPage, hasPreviousPage, startCursor, endCursor)।
কার্সার পেজিনেশন কোনও নতুন কৌশল নয় — এটি ওয়েবের অনেক আগে ডেটাবেসে ব্যবহৃত হত। SQL-এ একে keyset pagination বা seek method বলা হয়। 2015 সালে Relay স্পেসিফিকেশন প্রকাশের পর API-তে এই পদ্ধতি জনপ্রিয় হয়, যা HTTP ট্রান্সপোর্টে একরূপতার জন্য কার্সার বিন্যাসকে base64-এনকোডেড স্ট্রিং হিসেবে আনুষ্ঠানিক রূপ দেয়।
কার্সার পেজিনেশনের মৌলিক নীতি হল যে কোয়েরি অবস্থান নির্ধারণের জন্য অফসেটের পরিবর্তে একটি ইনডেক্সকৃত ক্ষেত্রের উপর WHERE শর্ত ব্যবহার করে। সামনের দিকের জন্য WHERE id > last_id ব্যবহার করা হয়; পিছনের দিকের জন্য WHERE id < first_id। B-tree ইনডেক্স O(log n)-এ কার্সারের পরে প্রথম রেকর্ড খুঁজে পায়, যা স্থিতিশীল প্রতিক্রিয়া সময় প্রদান করে।
-- কার্সার '83'-এর পরে 20টি রেকর্ড পান
SELECT id, title, created_at
FROM posts
WHERE id < 83
ORDER BY id DESC
LIMIT 20;
-- কার্সার '83'-এর আগে 20টি রেকর্ড পান (পিছনে)
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 কার্সারের আগে রেকর্ড নেয়।
কার্সার এবং Offset পেজিনেশনের মধ্যে নির্বাচন একটি API ডিজাইন করার সময় মূল স্থাপত্য সিদ্ধান্তগুলির মধ্যে একটি। প্রতিটি পদ্ধতির শক্তি এবং দুর্বলতা রয়েছে যা এর প্রযোজ্যতা নির্ধারণ করে। Cursor pagination গতিশীল ডেটার পরিস্থিতিতে জয়ী হয়; Offset নির্বিচার নেভিগেশনের পরিস্থিতিতে।
| বৈশিষ্ট্য | Cursor | Offset |
|---|---|---|
| সন্নিবেশে স্থিতিশীলতা | উচ্চ (কোনও ডুপ্লিকেট নয়) | নিম্ন (পৃষ্ঠা সরণ) |
| বড় ডেটাসেটে কর্মক্ষমতা | O(log n) — স্থিতিশীল | O(n) — বৃদ্ধির সাথে হ্রাস পায় |
| পৃষ্ঠা নম্বর দ্বারা নেভিগেশন | না | হ্যাঁ (page=5) |
| বাস্তবায়ন জটিলতা | মধ্যম | নিম্ন |
| REST সমর্থন | cursor/before/after | page/offset |
| GraphQL সমর্থন | Relay মান | সুপারিশকৃত নয় |
Offset পেজিনেশন OFFSET অবস্থান পর্যন্ত সম্পূর্ণ টেবিল স্ক্যান করে। offset=100000-এ, ডেটাবেস 100000 সারি পড়ে এবং এড়িয়ে যায়, এমনকি LIMIT 20 হলেও। MySQL এবং PostgreSQL OFFSET অপটিমাইজ করতে পারে না — এটি SQL-এ LIMIT/OFFSET বাস্তবায়নের একটি বৈশিষ্ট্য। কার্সার পেজিনেশন B-tree ইনডেক্স ব্যবহার করে যা O(log n)-এ অবস্থান খুঁজে পায়।
একটি অতিরিক্ত Offset সমস্যা হল পিছনের দিকে পৃষ্ঠাভুক্ত করার সময় রেকর্ড “বাদ দেওয়া”। যদি একজন ব্যবহারকারী পৃষ্ঠা 5 লোড করে এবং সেই মুহূর্তে নতুন রেকর্ড যুক্ত করা হয়, পৃষ্ঠা 6-এর অনুরোধ করলে তারা পৃষ্ঠা 5-এর রেকর্ড আবার দেখবে বা নতুনগুলি মিস করবে। Cursor pagination এই পরিস্থিতি সম্পূর্ণরূপে দূর করে: কার্সার সেটের একটি নির্দিষ্ট স্থান নির্দেশ করে এবং সন্নিবেশ অবস্থান পরিবর্তন করে না।
আসুন ব্যাকএন্ড (Kotlin + Spring) এবং ক্লায়েন্টে (Android + Retrofit) কার্সার পেজিনেশন বাস্তবায়ন দেখি। সার্ভার after, before, limit প্যারামিটার গ্রহণ করে এবং কার্সার ও pageInfo-সহ রেকর্ডের একটি তালিকা ফেরত দেয়। একটি সাধারণ প্রতিক্রিয়ায় পেজিনেশন UI পরিচালনার জন্য hasNextPage এবং hasPreviousPage থাকে।
@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
)
)
}
ক্লায়েন্টে, কার্সার পেজিনেশন Paging 3 থেকে PagingSource-এর মাধ্যমে বাস্তবায়িত হয়, যেখানে কী একটি কার্সার (Long)। PagingSource.load LoadParams.key গ্রহণ করে — শেষ লোড করা রেকর্ডের কার্সার। LoadResult.Page ডেটা এবং nextKey — পরবর্তী পৃষ্ঠার জন্য কার্সার — ফেরত দেয়। যখন nextKey = null, পেজিনেশন সম্পূর্ণ হয়।
// 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 Connection প্যাটার্নের মাধ্যমে বাস্তবায়িত হয়। প্রতিটি প্রকারের একটি Connection (pageInfo এবং edges-সহ) এবং একটি Edge (node + cursor) থাকে। কোয়েরিটি first, after, last, before প্যারামিটার পাস করে। সার্ভার ফেরত দেয় কার্সার-সহ edges-এর একটি অ্যারে এবং hasNextPage/hasPreviousPage-সহ pageInfo।
কার্সার পেজিনেশন গতিশীল ডেটা নিয়ে কাজ করা API-গুলির জন্য সুপারিশ করা হয় যেখানে রেকর্ড ঘন ঘন যোগ বা মুছে ফেলা হয়। ক্লাসিক উদাহরণ: সোশ্যাল নেটওয়ার্কে নিউজ ফিড, চ্যাট বার্তা, লেনদেন ইতিহাস, পোস্ট মন্তব্য। এই সমস্ত পরিস্থিতিতে, ধারাবাহিকতা এবং ডুপ্লিকেটের অনুপস্থিতি গুরুত্বপূর্ণ।
এমন পরিস্থিতি রয়েছে যেখানে Offset পেজিনেশন বেশি সুবিধাজনক: অ্যাডমিন প্যানেল যেখানে পৃষ্ঠা নম্বর দ্বারা নেভিগেশন প্রয়োজন; পেজিনেশনসহ অনুসন্ধান যেখানে ফলাফল পরিবর্তিত হতে পারে; রিপোর্ট এবং বিশ্লেষণ যেখানে পৃষ্ঠা 5-এর একটি নির্দিষ্ট লিঙ্ক প্রয়োজন। এই ক্ষেত্রে, কার্সারের সুবিধাগুলি বাস্তবায়ন জটিলতাকে অতিক্রম করে না।
কার্সার পেজিনেশন একটি নির্বিচার পৃষ্ঠায় “ঝাঁপ দেওয়া” সমর্থন করে না — ব্যবহারকারী “পৃষ্ঠা 5” ক্লিক করে সেখানে যেতে পারে না। এটি একটি স্থাপত্য সীমাবদ্ধতা: মোট পৃষ্ঠার সংখ্যা গণনা করার জন্য একটি পৃথক COUNT কোয়েরি প্রয়োজন, যা বড় টেবিলের জন্য ব্যয়বহুল হতে পারে। এই ধরনের ক্ষেত্রে, একটি হাইব্রিড পদ্ধতি: ডেটার জন্য কার্সার + পেজিনেশনের জন্য count।
সচরাচর জিজ্ঞাসিত প্রশ্ন
একটি কার্সার একটি অনন্য রেকর্ড শনাক্তকারী যা ডেটা সেটের একটি অবস্থান নির্দেশ করে। এটি সরল (একটি রেকর্ড ID) বা যৌগিক (একাধিক ক্ষেত্র) হতে পারে। ক্লায়েন্ট পৃষ্ঠার শেষ রেকর্ডের কার্সার পায় এবং পরবর্তী ব্যাচ পেতে পরবর্তী অনুরোধে এটি পাস করে।
Cursor pagination সরণের অধীন নয় যখন নতুন রেকর্ড যুক্ত করা হয় — প্রতিটি উপাদান ঠিক একটি পৃষ্ঠায় পড়ে। এটি প্রথম n সারি স্ক্যান না করে ইনডেক্স ব্যবহার করে বড় ভলিউমে গতি বজায় রাখে। Offset সহজ কিন্তু গতিশীল ডেটার জন্য অস্থির।
হ্যাঁ, কার্সার পেজিনেশন GraphQL-এর সাথে আবদ্ধ নয়। এটি যেকোনো REST API-তে কার্সারকে কোয়েরি প্যারামিটার ?after=83&limit=20 হিসেবে পাস করে বাস্তবায়ন করা যেতে পারে। প্রতিক্রিয়ায় endCursor এবং hasNextPage-সহ pageInfo থাকা উচিত — এটি ক্লায়েন্টকে অভ্যন্তরীণ কার্সার কাঠামো না জেনে লোডিং পরিচালনা করতে দেয়।
অটো-ইনক্রিমেন্ট ID সর্বোত্তম পছন্দ: একঘেয়েভাবে বৃদ্ধি পায়, পরিবর্তিত হয় না, কার্যকরভাবে ইনডেক্স করা হয়। UUID v7 (সময়-ক্রমিত)ও কাজ করে। টাইমস্ট্যাম্প একই সময়ে ডুপ্লিকেট তৈরি করতে পারে, তাই এটি ID-র সাথে একত্রিত করুন: (created_at, id) কার্সার অনন্যতার নিশ্চয়তার জন্য।
কার্সার পেজিনেশন মোট পৃষ্ঠার সংখ্যা প্রদান করে না — এটি তার সীমাবদ্ধতা। আপনার যদি মোট তথ্যের প্রয়োজন হয়, তবে একই ফিল্টার সহ একটি পৃথক COUNT কোয়েরি কার্যকর করুন। বড় টেবিলের জন্য, EXPLAIN-এর মাধ্যমে আনুমানিক গণনা বা বিশ্লেষণ থেকে ক্যাশেড মোট ব্যবহার করুন।
সারসংক্ষেপ
আমরা একটি মোবাইল অ্যাপ্লিকেশন টার্নকি তৈরি করব
IT Sectr 2017 সাল থেকে স্টার্টআপ এবং ব্যবসার জন্য iOS এবং Android অ্যাপ্লিকেশন তৈরি করে। আমরা আপনাকে পরামর্শ দেব এবং সেরা সমাধান প্রস্তাব করব।
আরও পড়ুন