কার্সার বনাম অফসেট পেজিনেশন — দ্রুত অ্যাডমিন স্ক্রিন API জন্য
সোজা, দ্রুত এবং পূর্বানুমেয় পেজিং প্যাটার্ন শিখুন—কার্সার বনাম অফসেট পেজিনেশন, স্থিতিশীল সোর্টিং, ফিল্টার, এবং মোট কিভাবে অ্যাডমিন স্ক্রিনগুলোকে ওয়েব ও মোবাইলে দ্রুত রাখে।

কেন পেজিনেশন অ্যাডমিন স্ক্রীনগুলো ধীরবেগে অনুভব করায়
অ্যাডমিন স্ক্রীনগুলো প্রায়ই একটি সাদামাটা টেবিল হিসেবে শুরু হয়: প্রথম 25টি সারি লোড করুন, একটি সার্চ বক্স যোগ করুন—হবে। কয়েকশো রেকর্ড থাকলে এটা তৎক্ষণাত মনে হয়। তারপর ডেটাসেট বড় হয়, এবং একই স্ক্রিন ধীরে ধীরে স্টাটার করে।
সাধারণ সমস্যা UI-তে থাকে না। সমস্যা থাকে API-র কাজগুলোতে, যা পেজ 12 ফেরত দেওয়ার আগে করতে হয়, যখন সোর্টিং ও ফিল্টার প্রয়োগ করা থাকে। টেবিল যত বড় হয়, ব্যাকএন্ড তত বেশি সময় খরচ করে মিল খোঁজা, কাউন্ট করা এবং আগের রেজাল্টগুলো স্কিপ করা। যদি প্রতিটি ক্লিকেই একটা বড় কুয়েরি চালায়, তাহলে স্ক্রিনটি চিন্তা করছে বলে মনে হয়—তুলনায় প্রতিক্রিয়াশীল হওয়ার বদলে।
আপনি সাধারণত একই জায়গায় এটি লক্ষ্য করবেন: পেজ পরিবর্তনগুলো ধীর হয়ে যায়, সোর্টিং অলস হয়ে ওঠে, সার্চ পেজগুলোতে অননুমনীয় মনে হয়, এবং ইনফিনিট স্ক্রল কখনো দ্রুত লোড হয়, তারপর হঠাৎ ধীর হয়ে যায়। ব্যস্ত সিস্টেমে ডেটা অনুরোধের মাঝে পরিবর্তিত হলে আপনি নকল বা গায়েব হওয়া সারি পর্যন্ত দেখতে পাবেন।
ওয়েব ও মোবাইল UI দুইই পেজিনেশনকে ভিন্নভাবে টেনে নিয়ে যায়। একটি ওয়েব অ্যাডমিন টেবিল ব্যবহারকারীকে একটি নির্দিষ্ট পেজে ঝাঁপ দেওয়ার এবং অনেক কলামের উপর সোর্ট করার উৎসাহ দেয়। মোবাইল স্ক্রীনগুলো সাধারণত ইনফিনিট লিস্ট ব্যবহার করে যেখানে পরবর্তী চাঙ্ক লোড হয়, এবং ব্যবহারকারীরা প্রত্যাশা করে প্রতিটি পুল সমান দ্রুত হবে। যদি আপনার API শুধুমাত্র পেজ নম্বর ঘিরে তৈরি হয়, মোবাইল প্রায়ই কষ্টে পড়ে। যদি এটি শুধু next/after ঘিরে করা হয়, ওয়েব টেবিলগুলোর অভিজ্ঞতা সীমাবদ্ধ মনে হতে পারে।
লক্ষ্য কেবল 25টি আইটেম ফেরত দেওয়া নয়। লক্ষ্য হলো দ্রুত, পূর্বানুমেয় পেজিং যা ডেটা বাড়লেও স্থিতিশীল থাকে, এমন নিয়ম যা টেবিল এবং ইনফিনিট লিস্ট দুটোতেই একইভাবে কাজ করে।
পেজিনেশনের মৌলিক ধারণা যা আপনার UI নির্ভর করে
পেজিনেশন হল একটি বড় তালিকাকে ছোট অংশে ভাগ করা যাতে স্ক্রীন দ্রুত লোড ও রেন্ডার করতে পারে। API-কে প্রতিটি রেকর্ড চাওয়ার বদলে, UI পরবর্তী অংশটা চায়।
সবচেয়ে গুরুত্বপূর্ণ নিয়ন্ত্রক হলো পেজ সাইজ (সাধারণত limit বলা হয়)। ছোট পেজ সাধারণত দ্রুত মনে হয় কারণ সার্ভার কম কাজ করে এবং অ্যাপ কম সারি ড্র করে। কিন্তু অতিশয় ছোট পেজ ব্যবহারকারীদের জন্য ঝাঁপছাঁপা মনে হতে পারে কারণ বারবার ক্লিক করতে কিংবা স্ক্রল করতে হয়। অনেক অ্যাডমিন টেবিলে 25 থেকে 100 আইটেম একটি ব্যবহারিক রেঞ্জ, মোবাইল সাধারণত নিম্ন প্রান্ত পছন্দ করে।
একটি স্থির সোর্ট অর্ডার বেশিরভাগ টিম ভাবার চেয়ে বেশি গুরুত্বপূর্ণ। যদি অর্ডার অনুরোধগুলোর মধ্যে বদলে যায়, ব্যবহারকারী দেখতে পাবে ডুপ্লিকেট বা মিসিং সারি। স্থির সোর্টিং সাধারণত মানে একটি প্রাথমিক ফিল্ড (যেমন created_at) এবং একটি টাই-ব্রেকার (যেমন id) দিয়ে অর্ডার করা। এটা অফসেট বা কার্সার উভয়ের ক্ষেত্রেই গুরুত্বপূর্ণ।
ক্লায়েন্টের দিক থেকে, একটি পেজিনেটেড রেসপন্সে থাকা উচিত আইটেমগুলো, একটি পরবর্তী-পেজ ইঙ্গিত (পেজ নম্বর বা কার্সার টোকেন), এবং শুধু সেই কাউন্টগুলো যা UI সত্যিই প্রয়োজন। কিছু স্ক্রীনে সঠিক মোট প্রয়োজন—“1-50 of 12,340” ধরনের। অন্যগুলো শুধুমাত্র has_more চাইতে পারে।
অফসেট পেজিনেশন: কাজ করার পদ্ধতি এবং সমস্যা কোথায়
অফসেট পেজিনেশন হলো ক্লাসিক পেজ N পদ্ধতি। ক্লায়েন্ট নির্দিষ্ট সংখ্যক সারি চায় এবং API-কে বলে প্রথমে কত সারি স্কিপ করতে হবে। এটিকে আপনি limit ও offset হিসেবে দেখবেন, বা page এবং pageSize হিসেবে যা সার্ভার অফসেটে রূপান্তর করে।
একটি সাধারণ অনুরোধ দেখতে এমন:
GET /tickets?limit=50&offset=950- “আমাকে 50টি টিকেট দাও, প্রথম 950টি স্কিপ করে।”
এটি সাধারণ অ্যাডমিন চাহিদার সাথে মেলে: পেজ 20-এ ঝাঁপ দিন, পুরনো রেকর্ড স্ক্যান করুন, বা বড় লিস্ট এক্সপোর্ট করুন চাঙ্ক করে। এটা ভিতরে আলোচনা করাও সহজ: “পেজ 3 দেখো এবং তুমি এটি দেখবে।”
সমস্যা দেখা দেয় গভীর পেজগুলিতে। অনেক ডেটাবেস এখনও স্কিপ করা সারিগুলোর পাশ দিয়ে যেতে হয়, বিশেষত যখন সোর্ট অর্ডার একটি টাইট ইনডেক্স দ্বারা সমর্থিত নয়। পেজ 1 দ্রুত হতে পারে, কিন্তু পেজ 200 উল্লেখযোগ্যভাবে ধীর হয়ে যেতে পারে, এবং ঠিক এইটাই অ্যাডমিন স্ক্রিনগুলোকে ধীর করে তোলে যখন ব্যবহারকারী স্ক্রল বা ঝাঁপ দেয়।
আরেকটি সমস্যা হল ডেটা বদলে গেলে সামঞ্জস্যতা। কল্পনা করুন একটি সাপোর্ট ম্যানেজার সবচেয়ে নতুন অনুযায়ী সাজানো টিকেটগুলোর পেজ 5 খুলেছেন। তিনি দেখার সময় নতুন টিকেট আসে বা পুরনো টিকেট মুছে দেওয়া হয়। ইনসারশনগুলো আইটেমগুলোকে সামনে সরিয়ে দিতে পারে (পেজগুলোর মধ্যে ডুপ্লিকেট)। ডিলিশনগুলো আইটেমগুলোকে পিছনে ঠেলে দিতে পারে (রেকর্ডগুলো ব্যবহারকারীর ব্রাউজিং পথে হারিয়ে যায়)।
অফসেট পেজিনেশন ছোট টেবিল, স্থিতিশীল ডেটাসেট, বা এককালীন এক্সপোর্টের জন্য এখনও ঠিক থাকতে পারে। বড়, সক্রিয় টেবিলে সীমান্ত কেসগুলো দ্রুত দেখা যায়।
কার্সার পেজিনেশন: কাজ করার পদ্ধতি এবং কেন এটি স্থিতিশীল থাকে
কার্সার পেজিনেশন একটি কার্সারকে বুকমার্ক হিসেবে ব্যবহার করে। “পেজ 7 দাও” বলার বদলে ক্লায়েন্ট বলে “এই নির্দিষ্ট আইটেমের পর থেকে চালিয়ে দাও।” কার্সার সাধারণত শেষ আইটেমের সোর্ট মানগুলো (যেমন created_at এবং id) এনকোড করে যাতে সার্ভার সঠিক স্থানে থেকে পুনরায় শুরু করতে পারে।
অনুরোধ সাধারণত কেবল:
limit: কতটি আইটেম ফেরত দিবেcursor: আগের রেসপন্স থেকে একটি অপেক টোকেন (প্রায়ইafterবলা হয়)
রেসপন্স আইটেমগুলো দেয় প্লাস একটি নতুন কার্সার যা সেই স্লাইসের শেষকে নির্দেশ করে। ব্যবহারিক পার্থক্য হলো কার্সার ডেটাবেসকে কাউন্ট ও স্কিপ করতে বলে না। এটি একটি নির্দিষ্ট অবস্থান থেকেই শুরু করতে বলে।
এই কারণেই কার্সার পেজিনেশন স্ক্রল-ফরওয়ার্ড তালিকাগুলিতে দ্রুত থাকে। একটি ভাল ইনডেক্স থাকলে, ডেটাবেস সহজেই “X-এর পরে আইটেমগুলো” এ জাম্প করে এবং পরবর্তী limit সারি পড়তে পারে। অফসেটের ক্ষেত্রে সার্ভার প্রায়ই যতই অফসেট বাড়ে ততই বেশি সারি স্ক্যান (বা কমপক্ষে স্কিপ) করতে হয়।
UI আচরণে, কার্সার পেজিনেশন “Next” করে তোলা সহজ করে: আপনি রিটার্ন করা কার্সার নিয়ে পরের অনুরোধে পাঠান। “Previous” ঐচ্ছিক এবং জটিল। কিছু API before কার্সার সাপোর্ট করে, অন্যরা রিভার্সে ফেচ করে এবং রেজাল্ট উল্টে দেয়।
কখন কার্সার, অফসেট বা হাইব্রিড বেছে নেওয়া উচিত
পছন্দ শুরু হয় মানুষ বাস্তবে তালিকাটি কীভাবে ব্যবহার করে তা দেখে।
কার্সার পেজিনেশন সবচেয়ে ভাল ফিট করে যখন ব্যবহারকারীরা বেশিরভাগই সামনের দিকে এগোয় এবং গতি সবচেয়ে গুরুত্বপূর্ণ: activity logs, চ্যাট, অর্ডার, টিকেট, অডিট ট্রেইল, এবং বেশিরভাগ মোবাইল ইনফিনিট স্ক্রল। এটি নতুন সারি ইনসার্ট বা মুছে গেলে ভালো আচরণ করে কারণ কার্সার পরবর্তী পেজকে একটি নির্দিষ্ট অবস্থানে অ্যাংকর করে।
অফসেট পেজিনেশন মানে যখন ব্যবহারকারীরা প্রায়ই ঝাঁপিয়ে পেজ নম্বরের মধ্যে ঘুরে বেড়ায়: ক্লাসিক অ্যাডমিন টেবিল যেখানে পেজ নম্বর, go-to-page, এবং দ্রুত ব্যাক-এন্ড-ফরথ আমারক। এটি ব্যাখ্যা করা সহজ, কিন্তু বড় ডেটাসেটে ধীর হয়ে পড়তে পারে এবং ডেটা পরিবর্তিত হলে কম স্থিতিশীল হয়।
একটি ব্যবহারিক সিদ্ধান্তের উপায়:
- “next, next, next” কার্যকলাপ প্রধান হলে কার্সার বেছে নিন।
- যখন “jump to page N” বাস্তব চাহিদা থাকে তখন অফসেট বেছে নিন।
- মোটগুলোকে ঐচ্ছিক বিবেচনা করুন। বড় টেবিলে সঠিক মোট বের করা ব্যয়বহুল হতে পারে।
হাইব্রিডগুলো সাধারণ। একটি পদ্ধতি হলো গতির জন্য কার্সার-ভিত্তিক next/prev রাখা, প্লাস ছোট বা ফিল্টার করা সাবসেটগুলির জন্য ঐচ্ছিক পেজ-ঝাঁপ মোড যেখানে অফসেট দ্রুত থাকে। অন্যটি হলো ক্যাশ করা স্ন্যাপশট ভিত্তিক পেজ নম্বর সহ কার্সার রিট্রিভাল, যাতে টেবিলটি পরিচিত লাগে তবে প্রতিটি অনুরোধ ভারী কাজ না হয়।
ওয়েব ও মোবাইল দুটোতেই কাজ করে এমন সমন্বিত API চুক্তি
অ্যাডমিন UI গুলো দ্রুত মনে হয় যখন প্রতিটি লিস্ট এন্ডপয়েন্ট একইভাবে আচরণ করে। UI বদলালেও (ওয়েব টেবিল পেজ নম্বরসহ, মোবাইল ইনফিনিট স্ক্রল), API চুক্তিটি স্থির রাখলে আপনাকে প্রতিটি স্ক্রিনের জন্য আলাদা পেজিনেশন নিয়ম শিখতে হবে না।
একটি ব্যবহারিক চুক্তি তিনটি অংশে: সারি (rows), পেজিং স্টেট, এবং ঐচ্ছিক মোট। এন্ডপয়েন্ট জুড়ে নামগুলো একই রাখুন (tickets, users, orders), যদিও আভ্যন্তরীণ পেজিং মোড আলাদা হতে পারে।
নিচে এমন একটি রেসপন্স শেপ আছে যা ওয়েব ও মোবাইল দুটোতেই ভাল কাজ করে:
{
"data": [ { "id": "...", "createdAt": "..." } ],
"page": {
"mode": "cursor",
"limit": 50,
"nextCursor": "...",
"prevCursor": null,
"hasNext": true,
"hasPrev": false
},
"totals": {
"count": 12345,
"filteredCount": 120
}
}
কয়েকটি বিস্তারিত বিষয় এটিকে পুনরায় ব্যবহারযোগ্য করে তোলে:
page.modeক্লায়েন্টকে বলে সার্ভার কী করছে, ফিল্ড নাম বদলানোর প্রয়োজন নেই।limitসব সময় অনুরোধ করা পেজ সাইজ।nextCursorওprevCursorথাকা উচিত এমনকি যখন একটি null।totalsঐচ্ছিক। যদি এটি ব্যয়বহুল হয়, ক্লায়েন্ট চাইলে দিন।
একটি ওয়েব টেবিল “Page 3” দেখাতে পারে তার নিজস্ব পেজ ইনডেক্স রেখে এবং API বারংবার কল করে। একটি মোবাইল লিস্ট পেজ নম্বর উপেক্ষা করে শুধু পরবর্তী চাঙ্কটি অনুরোধ করতে পারে।
যদি আপনি ওয়েব ও মোবাইল উভয় অ্যাডমিন UI AppMaster-এর সাথে তৈরি করে থাকেন, তাহলে এমন একটি স্থির চুক্তি দ্রুত ফল দেয়। একই লিস্ট আচরণ স্ক্রিন জুড়ে পুনরায় ব্যবহার করা যায়, কোনো এন্ডপয়েন্টে আলাদা পেজিনেশন লজিক দরকার পড়ে না।
সোর্টিং নিয়ম যা পেজিনেশন স্থিতিশীল রাখে
সোর্টিং সেখানে যেখানে পেজিনেশন সাধারণত ভেঙে যায়। যদি অর্ডার অনুরোধগুলোর মধ্যে বদলে যায়, ব্যবহারকারী ডুপ্লিকেট, গ্যাপ বা “মিসিং” সারি দেখতে পাবেন।
সোর্টিংকে একটি চুক্তি হিসেবে বিবেচনা করুন, পরামর্শ হিসেবে নয়। অনুমোদিত সোর্ট ফিল্ড ও দিকগুলো প্রকাশ করুন, এবং অন্য যেকোনো অনুরোধ প্রত্যাখ্যান করুন। এতে API পূর্বানুমেয় থাকে এবং ক্লায়েন্ট বিকাশের সময় দেখতে সরল হলেও ধীর বা ঝামেলায় পড়া সোর্ট অনুরোধ এড়ানো যায়।
একটি স্থির সোর্টের জন্য একটি ইউনিক টাই-ব্রেকার দরকার। যদি আপনি created_at দিয়ে সোর্ট করেন এবং দুইটি রেকর্ড একই টাইমস্ট্যাম্প শেয়ার করে, তাহলে শেষের দিকে id (অথবা অন্য কোনো ইউনিক কলাম) যোগ করুন। এর ছাড়া ডেটাবেস সমমানের ভ্যালুগুলো যেকোনো ক্রমে ফেরত দিতে পারে।
প্রায়োগিক নিয়মগুলো:
- শুধুমাত্র ইনডেক্স করা, সুস্পষ্ট ফিল্ডে সোর্টিং অনুমোদন করুন (যেমন
created_at,updated_at,status,priority)। - সর্বদা একটি ইউনিক টাই-ব্রেকার অন্তর্ভুক্ত করুন (উদাহরণ:
id ASC)। - একটি ডিফল্ট সোর্ট নির্ধারণ করুন (উদাহরণ:
created_at DESC, id DESC) এবং ক্লায়েন্ট জুড়ে এটিকে স্থির রাখুন। - null কিভাবে সাজানো হবে তা ডকুমেন্ট করুন (উদাহরণ: তারিখ ও সংখ্যার জন্য “nulls last”)।
সোর্টিং কার্সার জেনারেশনও চালিত করে। একটি কার্সার শেষ আইটেমের সোর্ট মানগুলোই এনকোড করে, টাই-ব্রেকারসহ, যাতে পরবর্তী পেজ “after” সেই টিউপলের উপর কুয়েরি চালাতে পারে। যদি সোর্ট বদলে যায়, পুরানো কার্সার অবৈধ হয়ে যায়। সোর্ট প্যারামিটারকে কার্সার চুক্তির অংশ হিসেবে বিবেচনা করুন।
ফিল্টার এবং টোটালস চুক্তি ভাঙবেনা এমনভাবে
ফিল্টারগুলোকে পেজিনেশনের থেকে আলাদা রাখুন। UI বলছে, “আমাকে ভিন্ন সেটের সারি দেখাও,” এবং তারপর বলে, “ঐ সেটে পেজিং করো।” যদি আপনি ফিল্টার ফিল্ডগুলোকে পেজিনেশন টোকেনে মিশিয়ে দেন বা ফিল্টারগুলোকে অপশনাল ও অনভ্যালিড রেখে দেন, আপনি পাবেন বোঝা কঠিন আচরণ: খালি পেজ, ডুপ্লিকেট, বা এমন কার্সার যা হঠাৎ অন্য ডেটাসেটে পয়েন্ট করে।
একটি সহজ নিয়ম: ফিল্টারগুলো সাধারণ কুয়েরি প্যারামিটার (বা POST-এর জন্য রিকয়েস্ট বডি) হিসেবে থাকুক, এবং কার্সার অপেক ও শুধুমাত্র সেই নির্দিষ্ট ফিল্টার+সোর্ট কম্বিনেশনের জন্য বৈধ। যদি ব্যবহারকারী কোনো ফিল্টার পরিবর্তন করে (status, date range, assignee), ক্লায়েন্ট পুরনো কার্সার ফেলবে এবং শুরু থেকেই শুরু করবে।
কোন ফিল্টারগুলো অনুমোদিত তা কঠোরভাবে নির্ধারণ করুন। এটি কর্মক্ষমতা রক্ষা করে এবং আচরণ পূর্বানুমেয় রাখে:
- অজানা ফিল্টার ফিল্ড প্রত্যাখ্যান করুন (চুপিচুপি উপেক্ষা করবেন না)।
- টাইপ ও রেঞ্জ যাচাই করুন (তারিখ, enum, ID)।
- চওড়া ফিল্টার ক্যাপ করুন (উদাহরণ: একটি IN লিস্টে সর্বোচ্চ 50টি ID)।
- ডেটা ও টোটাল দুটোতেই একই ফিল্টার প্রয়োগ করুন (মিসম্যাচ সংখ্যা নয়)।
টোটালস হল যেখানে অনেক API ধীর হয়ে যায়। সঠিক কাউন্ট বড় টেবিলে ব্যয়বহুল হতে পারে, বিশেষত একাধিক ফিল্টারের সাথে। সাধারণত আপনার তিনটি অপশন আছে: সঠিক, অনুমানকৃত, বা নেই। ছোট ডেটাসেটের জন্য সঠিক ভালো। অ্যাডমিন স্ক্রিনগুলোর জন্য অনুমানকৃত প্রায়ই যথেষ্ট। যখন কেবল “Load more” দরকার, তখন কোনো টোটাল না দেখালেও চলে।
প্রতি অনুরোধধরে ধরা না পড়তে, টোটালগুলো ঐচ্ছিক রাখুন: ক্লায়েন্ট চাইলে গণনা করুন (উদাহরণ includeTotal=true), ফিল্টার সেট অনুযায়ী সামান্য সময়ের জন্য ক্যাশ করুন, অথবা কেবল প্রথম পেজে টোটাল ফেরত দিন।
ধাপে ধাপে: এন্ডপয়েন্ট ডিজাইন ও বাস্তবায়ন
ডিফল্ট দিয়ে শুরু করুন। একটি লিস্ট এন্ডপয়েন্টের দরকার একটি স্থির সোর্ট অর্ডার, প্লাস সেই সারির জন্য টাই-ব্রেকার। উদাহরণ: createdAt DESC, id DESC। টাই-ব্রেকার (id) নতুন রেকর্ড যোগ হলে ডুপ্লিকেট ও গ্যাপ ঠেকাতে সাহায্য করে।
একটি অনুরোধ শেপ সংজ্ঞায়িত করুন এবং এটিকে সাধারণ রাখুন। সাধারণ প্যারামিটারগুলো: limit, cursor (অথবা offset), sort, এবং filters। যদি আপনি দুটো মোড সাপোর্ট করেন, সেগুলোকে পারস্পরিকভাবে এক্সক্লুসিভ রাখুন: ক্লায়েন্ট হয় cursor পাঠাবে, অথবা offset, কিন্তু উভয় নয়।
একটি সঙ্গত রেসপন্স চুক্তি বজায় রাখুন যাতে ওয়েব এবং মোবাইল UI একই লিস্ট লজিক শেয়ার করতে পারে:
items: রেকর্ডগুলোর পেজnextCursor: পরবর্তী পেজ পেতে কার্সার (অথবাnull)hasMore: বুলিয়ান যাতে UI সিদ্ধান্ত নেবে “Load more” দেখাবে কি নাtotal: মোট মিল খোঁজার রেকর্ড (nullযদি অনুরোধ না করা হয় এবং এটি ব্যয়বহুল হয়)
বাস্তবায়নে দুইটি পদ্ধতি ভিন্ন হয়।
অফসেট কুয়েরি সাধারণত ORDER BY ... LIMIT ... OFFSET ... এবং এটি বড় টেবিলে ধীর হতে পারে।
কার্সার কুয়েরি শেষ আইটেমের উপর ভিত্তি করে seek শর্ত ব্যবহার করে: “দিয়ে আমাকে এমন আইটেম যেখানে (createdAt, id) শেষ (createdAt, id) থেকে ছোট।” এতে পারফরম্যান্স স্থিতিশীল থাকে কারণ ডেটাবেস ইনডেক্স ব্যবহার করতে পারে।
শিপ করার আগে গার্ডরেইল যোগ করুন:
limitক্যাপ করুন (উদাহরণ: সর্বোচ্চ 100) এবং একটি ডিফল্ট সেট করুন।sortকে একটি allowlist-এর বিরুদ্ধে যাচাই করুন।- ফিল্টারগুলো টাইপ অনুযায়ী যাচাই করে অজানা কী প্রত্যাখ্যান করুন।
cursorঅপেক রাখুন (শেষ সোর্ট মানগুলো এনকোড করুন) এবং ভুল-ফরম্যাটেড কার্সার প্রত্যাখ্যান করুন।totalকিভাবে অনুরোধ করা হবে তা ঠিক করুন।
তদন্ত করুন ডেটা পরিবর্তিত অবস্থায়: অনুরোধগুলোর মধ্যে রেকর্ড তৈরি ও মুছুন, সোর্টিং প্রভাবিত করার জন্য ফিল্ড আপডেট করুন, এবং যাচাই করুন যে আপনি ডুপ্লিকেট বা মিসিং সারি দেখতে পাচ্ছেন না।
উদাহরণ: টিকিট তালিকা যা ওয়েব ও মোবাইল দুটিতেই দ্রুত থাকে
একটি সাপোর্ট টিম একটি অ্যাডমিন স্ক্রীন খুলে নতুন টিকিটগুলো রিভিউ করে। তাদের তালিকাটি তৎক্ষণাত মনে হওয়া দরকার, এমনকি নতুন টিকিট আসলে বা এজেন্টরা পুরনোগুলো আপডেট করলে।
ওয়েবে UI একটি টেবিল। ডিফল্ট সোর্ট updated_at (সর্বশেষ আগে) এবং টিম প্রায়ই ফিল্টার করে Open বা Pending-এ। একই এন্ডপয়েন্ট স্থির সোর্ট ও কার্সার টোকেন দিয়ে উভয় আচরণ সাপোর্ট করতে পারে।
GET /tickets?status=open&sort=-updated_at&limit=50&cursor=eyJ1cGRhdGVkX2F0IjoiMjAyNi0wMS0yNVQxMTo0NTo0MloiLCJpZCI6IjE2OTMifQ==
রেসপন্স UI-এর কাছে পূর্বানুমেয় থাকে:
{
"items": [{"id": 1693, "subject": "Login issue", "status": "open", "updated_at": "2026-01-25T11:45:42Z"}],
"page": {"next_cursor": "...", "has_more": true},
"meta": {"total": 128}
}
মোবাইলে একই এন্ডপয়েন্ট ইনফিনিট স্ক্রল চালায়। অ্যাপ একবারে 20টি টিকেট লোড করে, তারপর next_cursor পাঠিয়ে পরবর্তী ব্যাচ নেয়। পেজ নম্বর লজিক নেই, এবং রেকর্ডগুলো পরিবর্তিত হলেও বিস্ময় কম।
মূল কথা হলো কার্সার শেষ দেখা অবস্থানটি এনকোড করে (উদাহরণ: updated_at প্লাস টাই-ব্রেকার হিসেবে id)। যদি একটি টিকেট এজেন্টের কাজের সময় আপডেট হয়, এটি পরবর্তী রিফ্রেশে উপরে চলে আসতে পারে, কিন্তু এটি ইতিমধ্যেই স্ক্রোল করা ফিডে ডুপ্লিকেট বা গ্যাপ ঘটাবে না।
টোটালস উপকারী, কিন্তু বড় ডেটাসেটে ব্যয়বহুল। একটি সহজ নিয়ম হলো meta.total কেবল তখন ফেরত দিন যখন ব্যবহারকারী ফিল্টার প্রয়োগ করে (যেমন status=open) বা স্পষ্টভাবে অনুরোধ করে।
সাধারণ ভুল যা ডুপ্লিকেট, গ্যাপ ও লেগ সৃষ্টি করে
অধিকাংশ পেজিনেশন বাগ ডেটাবেসে থাকে না। এগুলো আসে ছোট API সিদ্ধান্তগুলো থেকে যা টেস্টিং-এ ঠিক মনে হয়, কিন্তু অনুরোধগুলোর মধ্যে ডেটা পরিবর্তন হলে ভেঙে পড়ে।
ডুপ্লিকেট বা মিসিং সারির সবচেয়ে সাধারণ কারণ হলো এমন একটি ফিল্ড দিয়ে সোর্ট করা যা ইউনিক নয়। যদি আপনি created_at দিয়ে সোর্ট করেন এবং দুইটি আইটেম একই টাইমস্ট্যাম্প শেয়ার করে, অর্ডার অনুরোধগুলোর মধ্যে বদলে যেতে পারে। সমাধান সহজ: সর্বদা একটি স্থির টাই-ব্রেকার যোগ করুন, সাধারণত প্রাইমারি কী, এবং সোর্টকে জোড়া হিসেবে বিবেচনা করুন যেমন (created_at desc, id desc)।
আরেকটি সাধারণ সমস্যা ক্লায়েন্টকে যেকোনো পেজ সাইজ অনুরোধ করতে দেওয়া। একটি বড় অনুরোধ CPU, মেমরি এবং রেসপন্স টাইম spike করতে পারে, যা প্রতিটি অ্যাডমিন স্ক্রিনকে ধীর করে দেয়। একটি সঠিক ডিফল্ট ও একটি কঠোর সর্বোচ্চ সীমা বেছে নিন, এবং ক্লায়েন্ট বেশি চাইলে error ফেরত দিন।
টোটালসও ভয়ঙ্কর হতে পারে। প্রতিটি অনুরোধে সব মিলিত সারি কাউন্ট করা সবচেয়ে ধীর অংশ হয়ে দাঁড়াতে পারে, বিশেষত ফিল্টারসহ। যদি UI টোটাল চায়, কেবল অনুরোধ করলে নিন (অথবা একটি অনুমান ফেরত দিন), এবং পূর্ণ কাউন্ট ব্লক করে লিস্ট স্ক্রলিং আটকাবেন না।
সবচেয়ে সাধারণ ভুলগুলো যা গ্যাপ, ডুপ্লিকেট ও লেগ তৈরি করে:
- ইউনিক টাই-ব্রেকার ছাড়া সোর্টিং (অস্থির অর্ডার)
- অনিয়ন্ত্রিত পেজ সাইজ (সার্ভারের উপর ভর)
- প্রতিবার টোটাল ফেরত দেওয়া (ধীর কুয়েরি)
- একই এন্ডপয়েন্টে অফসেট ও কার্সার নিয়ম মিশানো (ক্লায়েন্টের আচরণ বিভ্রান্ত করে)
- ফিল্টার বা সোর্ট পরিবর্তনের পর একই কার্সার পুনঃব্যবহার করা (ভুল রেজাল্ট)
যখন ফিল্টার বা সোর্ট বদলে যায় তখন পেজিনেশন রিসেট করুন। নতুন ফিল্টারকে একটি নতুন সার্চ হিসেবে দেখুন: কার্সার/অফসেট মুছে প্রথম পেজ থেকে শুরু করুন।
শিপ করার আগে দ্রুত চেকলিস্ট
API ও UI পাশাপাশি রেখে একবার এটি চালান। বেশিরভাগ সমস্যা লিস্ট স্ক্রীন ও সার্ভারের মধ্যকার চুক্তিতে ঘটে।
- ডিফল্ট সোর্ট স্থির এবং ইউনিক টাই-ব্রেকার অন্তর্ভুক্ত করে (উদাহরণ
created_at DESC, id DESC)। - সোর্টিং ফিল্ড ও দিকগুলি হোয়াইটলিস্ট করা আছে।
- সর্বোচ্চ পেজ সাইজ আরোপ করা আছে, সাথে একটি যুক্তিসংগত ডিফল্ট।
- কার্সার টোকেনগুলি অপেক এবং ভুল কার্সার যৌক্তিকভাবে ব্যর্থ করে।
- কোনো ফিল্টার বা সোর্ট পরিবর্তন হলে পেজিনেশন স্টেট রিসেট হয়।
- টোটাল নীতি স্পষ্ট: সঠিক, অনুমানকৃত, বা বাদ।
- একই চুক্তি একটি টেবিল এবং ইনফিনিট স্ক্রলের জন্য বিশেষ কেস ছাড়াই সমর্থন করে।
পরবর্তী ধাপ: আপনার লিস্টগুলো স্ট্যান্ডার্ডাইজ করুন এবং সেগুলো সঙ্গত রাখুন
একটি অ্যাডমিন লিস্ট বাছুন যা মানুষ প্রত্যেক দিন ব্যবহার করে এবং সেটিকে আপনার গোল্ড স্ট্যান্ডার্ড বানান। একটি ব্যস্ত টেবিল যেমন Tickets, Orders, বা Users শুরু করার জন্য ভালো। সেই এন্ডপয়েন্ট দ্রুত ও পূর্বানুমেয় হয়ে গেলে, একই চুক্তি বাকী অ্যাডমিন স্ক্রিনগুলোতে কপি করুন।
চুক্তি লিখে রাখুন, সংক্ষিপ্ত হলেও। স্পষ্টভাবে বলুন API কী গ্রহণ করে এবং কী ফেরত দেয় যাতে UI টিম অনুমান না করে এবং একেক এন্ডপয়েন্টে আলাদা নিয়ম আবিষ্কার না করে।
প্রতিটি লিস্ট এন্ডপয়েন্টে প্রয়োগ করার জন্য একটি সহজ স্ট্যান্ডার্ড:
- অনুমোদিত সোর্ট: সঠিক ফিল্ড নাম, দিক, এবং একটি পরিষ্কার ডিফল্ট (সহ একটি টাই-ব্রেকার যেমন
id)। - অনুমোদিত ফিল্টার: কোন ফিল্ডে ফিল্টার করা যাবে, মানের ফর্ম্যাট, এবং ভুল ফিল্টারের উপর কী হবে।
- টোটাল নীতি: কখন কाउंट ফেরত দেবেন, কখন “unknown” বলবেন, এবং কখন এটিকে বাদ দেবেন।
- রেসপন্স শেপ: সঙ্গত কী (
items, paging info, applied sort/filters, totals)। - এরর নিয়ম: সঙ্গত স্ট্যাটাস কোড ও পাঠযোগ্য ভ্যালিডেশন মেসেজ।
যদি আপনি AppMaster (appmaster.io) দিয়ে এই অ্যাডমিন স্ক্রিনগুলো তৈরি করছেন, তাহলে শুরুতেই পেজিনেশন চুক্তি স্ট্যান্ডার্ড করতে সুবিধা হয়। একই লিস্ট আচরণ ওয়েব অ্যাপ ও নেটিভ মোবাইল অ্যাপ জুড়ে পুনরায় ব্যবহার করা যাবে এবং পরে পেজিনেশন এজ কেসের পিছনে সময় কম লাগবে।
প্রশ্নোত্তর
Offset pagination uses limit plus offset (or page/pageSize) to skip rows, so deeper pages often get slower as the database has to walk past more records. Cursor pagination uses an after token based on the last item’s sort values, so it can jump to a known position and stay fast as you keep moving forward.
Because page 1 is usually cheap, but page 200 forces the database to skip a large number of rows before it can return anything. If you also sort and filter, the work grows, so each click feels more like a new heavy query than a quick fetch.
Always use a stable sort with a unique tie-breaker, such as created_at DESC, id DESC or updated_at DESC, id DESC. Without the tie-breaker, records with the same timestamp can swap order between requests, which is a common cause of duplicates and “missing” rows.
Use cursor pagination for lists where people mostly move forward and speed matters, like activity logs, tickets, orders, and mobile infinite scroll. It stays consistent when new rows are inserted or deleted, because the cursor anchors the next page to an exact last-seen position.
Offset pagination fits best when “jump to page N” is a real UI feature and users regularly bounce around. It’s also convenient for small tables or stable datasets, where deep-page slowdown and shifting results are unlikely to matter.
Keep one response shape across endpoints and include the items, paging state, and optional totals. A practical default is returning items, a page object (with limit, nextCursor/prevCursor or offset), and a lightweight flag like hasNext so both web tables and mobile lists can reuse the same client logic.
Because exact COUNT(*) on large, filtered datasets can become the slowest part of the request and make every page change feel laggy. A good default is to make totals optional, return them only when requested, or return has_more when the UI only needs “Load more.”
Treat filters as part of the dataset, and treat the cursor as valid only for that exact filter and sort combination. If a user changes any filter or sort, reset pagination and start from the first page; reusing an old cursor after changes is a common way to get empty pages or confusing results.
Whitelist allowed sort fields and directions, and reject anything else so clients can’t accidentally request slow or unstable ordering. Prefer sorting on indexed fields and always append a unique tie-breaker like id to keep the order deterministic across requests.
Enforce a maximum limit, validate filters and sort parameters, and make cursor tokens opaque and strictly validated. If you’re building admin screens in AppMaster, keeping these rules consistent across all list endpoints makes it easier to reuse the same table and infinite-scroll behavior without custom pagination fixes per screen.


