कर्सर पेजिनेशन डेटा बदलने पर सूचियों को स्थिर रखता है। जानें क्यों इन्सर्ट और डिलीट के साथ ऑफसेट पेजिंग टूटती है और क्लीन कर्सर कैसे लागू करें।

आप फ़ीड खोलते हैं, थोड़ा स्क्रॉल करते हैं, और सब कुछ सामान्य लगता है—जब तक कि अचानक नहीं लगता। आप वही आइटम दो बार देखते हैं। कोई चीज़ जो आपने देखी थी गायब है। एक रो जिस पर आप टैप करने वाले थे नीचे खिसक जाती है और आप गलत डिटेल पेज पर पहुँच जाते हैं।
ये यूज़र-देखी जाने वाली बग्स हैं, भले ही आपकी API रिस्पॉन्स अलग से “सही” दिखें। आम लक्षण आसानी से पहचानने योग्य हैं:
यह मोबाइल पर और खराब होता है। लोग रुकते हैं, ऐप बदलते हैं, कनेक्टिविटी खो देते हैं, फिर बाद में जारी रखते हैं। उस दौरान नए आइटम आते हैं, पुराने हटाए जाते हैं, और कुछ संपादित होते हैं। अगर आपका ऐप "पेज 3" के लिए ऑफसेट पूछता रहता है, तो पेज सीमा उपयोगकर्ता के बीच-बीच में बदल सकती है और फ़ीड अस्थिर और भरोसेमंद नहीं लगेगा।
लक्ष्य सरल है: एक बार उपयोगकर्ता आगे स्क्रॉल करना शुरू करे तो लिस्ट को स्नैपशॉट जैसा व्यवहार करना चाहिए। नए आइटम मौजूद रह सकते हैं, पर उन्हें उपयोगकर्ता के पहले से पेज किए हुए हिस्से को फिर से व्यवस्थित नहीं करना चाहिए। उपयोगकर्ता को एक चिकना, प्रत्याशित क्रम मिलना चाहिए।
कोई पेजिनेशन पद्धति परफेक्ट नहीं है। असली सिस्टम्स में समवर्ती लिखावटें, संपादन और कई सॉर्ट विकल्प होते हैं। पर कर्सर पेजिनेशन आमतौर पर ऑफसेट पेजिनेशन से सुरक्षित होती है क्योंकि यह मूविंग रो-काउंट की बजाय एक स्थिर ऑर्डर में किसी विशिष्ट पोज़िशन से पेज करती है।
ऑफसेट पेजिनेशन "skip N, take M" तरीका है। आप API को बतاتے हैं कितनी आइटम्स छोड़नी हैं (offset) और कितनी लौटानी हैं (limit)। limit=20 के साथ, आपको प्रति पेज 20 आइटम मिलते हैं।
सैद्धांतिक रूप से:
GET /items?limit=20&offset=0 (पहला पेज)GET /items?limit=20&offset=20 (दूसरा पेज)GET /items?limit=20&offset=40 (तीसरा पेज)रिस्पॉन्स आमतौर पर आइटम्स के साथ अगला पेज पूछने के लिए पर्याप्त जानकारी शामिल करता है.
{
"items": [
{"id": 101, "title": "..."},
{"id": 100, "title": "..."}
],
"limit": 20,
"offset": 20,
"total": 523
}
यह लोकप्रिय है क्योंकि यह टेबल्स, एडमिन लिस्ट्स, सर्च रिजल्ट्स और साधारण फ़ीड्स से अच्छी तरह मेल खाता है। SQL में LIMIT और OFFSET के साथ यह लागू करना भी आसान है।
मसला छिपी हुई धारणा है: डेटासेट के बीच-बीच में स्थिर रहने की उम्मीद। असली ऐप्स में नई पंक्तियाँ डाली जाती हैं, पंक्तियाँ हटती हैं और सॉर्ट की चाबियाँ बदलती हैं। वहीँ से “रहस्यमयी बग” शुरू होते हैं।
ऑफसेट पेजिनेशन मानता है कि सूची दोनों अनुरोधों के बीच स्थिर रहती है। पर असली सूचियाँ हिलती हैं। जब सूची शिफ्ट होती है, तो "skip 20" जैसा ऑफसेट अब वही आइटम्स इंगित नहीं करता।
कल्पना कीजिए created_at desc (नवीनतम पहले) द्वारा सॉर्ट की गई फ़ीड, पेज साईज़ 3।
आप offset=0, limit=3 के साथ पेज 1 लोड करते हैं और पाते हैं [A, B, C]।
अब एक नया आइटम X बनता है और टॉप पर आता है। सूची अब [X, A, B, C, D, E, F, ...] हो जाती है। आप offset=3, limit=3 के साथ पेज 2 लोड करते हैं। सर्वर [X, A, B] को स्किप करता है और [C, D, E] लौटाता है।
आपने अभी C को फिर से देखा (डुप्लिकेट), और बाद में आप एक आइटम मिस कर देंगे क्योंकि सब कुछ नीचे खिसक गया।
डिलीट्स उल्टा विफलता पैदा करते हैं। शुरुआत [A, B, C, D, E, F, ...] से करें। आप पेज 1 लोड करके [A, B, C] देखते हैं। पेज 2 से पहले B डिलीट हो जाता है, तो सूची बन जाती है [A, C, D, E, F, ...]। offset=3 वाला पेज 2 [A, C, D] को स्किप करके [E, F, G] लौटाता है। D एक गैप बन जाता है जिसे आप कभी फ़ेच नहीं करते।
नवीनतम-फ़र्स्ट फ़ीड्स में, इन्सर्ट्स ऊपर होते हैं, जो ठीक वही है जो हर आगे के ऑफसेट को शिफ्ट कर देता है।
"स्टेबल लिस्ट" वही है जिसकी उपयोगकर्ता उम्मीद करते हैं: जैसे ही वे आगे स्क्रॉल करते हैं, आइटम्स झांकते नहीं, दोहराए नहीं जाते या बिना वजह गायब नहीं होते। यह समय को फ्रीज़ करने के बारे में कम और पेजिनेशन को प्रत्याशित बनाने के बारे में ज्यादा है।
दो विचार अक्सर एक साथ मिल जाते हैं:
created_at और टाई-ब्रेकर के रूप में id) ताकि समान इनपुट के साथ दो रिक्वेस्ट एक ही ऑर्डर लौटाएँ।रिफ्रेश और स्क्रॉल-फ़ॉरवर्ड अलग क्रियाएँ हैं। रिफ्रेश का मतलब है “मुझे अभी नया दिखाओ,” इसलिए टॉप बदल सकता है। स्क्रॉल-फ़ॉरवर्ड का मतलब है “जहाँ मैं था वहीं से जारी रखो,” इसलिए आपको शिफ्टिंग पेज बाउंड्रीज़ की वजह से रिपीट्स या गैप्स नहीं दिखने चाहिए।
एक सरल नियम जो ज़्यादातर पेजिनेशन बग्स रोकता है: स्क्रोल-फॉरवर्ड कभी रिपीट्स नहीं दिखाना चाहिए।
कर्सर पेजिनेशन सूची में पेज करने के लिए पेज नंबर की बजाय एक बुकमार्क का उपयोग करती है। "मुझे पेज 3 दो" कहने की बजाय क्लाइंट कहता है "यहीं से जारी रखो।"
कॉन्ट्रैक्ट सीधा है:
यह इन्सर्ट्स और डिलीट्स को बेहतर तरह से सहन करता है क्योंकि कर्सर एक स्थिर क्रम में पोज़िशन को एंकर करता है, न कि एक मूविंग रो-काउंट को।
गैर-इच्छाकृत आवश्यकता एक निर्धारक (deterministic) सॉर्ट ऑर्डर है। आपको एक स्थिर ऑर्डरिंग नियम और एक संगत टाई-ब्रेकर चाहिए, वरना कर्सर भरोसेमंद बुकमार्क नहीं होगा।
एक सॉर्ट ऑर्डर चुनें जो लोगों के पढ़ने के तरीके से मेल खाए। फ़ीड्स, संदेश और एक्टिविटी लॉग आमतौर पर नवीनतम पहले होते हैं। हिस्ट्रीज़ जैसे इनवॉइसेस और ऑडिट लॉग अक्सर पुराने पहले रखने में आसान होते हैं।
कर्सर को उस ऑर्डर में एक पोज़िशन यूनिकली पहचाननी चाहिए। अगर दो आइटम्स एक ही कर्सर मान साझा कर सकते हैं तो आप अंततः डुप्लिकेट्स या गैप्स पाएँगे।
सामान्य विकल्प और ध्यान रखने योग्य बातें:
created_at: सरल, पर असुरक्षित अगर कई पंक्तियाँ एक ही टाइमस्टैम्प साझा करती हैं।id: सुरक्षित अगर IDs मोनोटोनिक हों, पर यह आवश्यक प्रोडक्ट ऑर्डर से मेल न खा सके।created_at + id: आम तौर पर सबसे अच्छा मिश्रण (ऑर्डर के लिए टाइमस्टैम्प, टाई-ब्रेकर के लिए id)।updated_at: इनफिनाइट स्क्रॉल के लिये जोखिमपूर्ण क्योंकि एडिट्स आइटम्स को पन्नों के बीच हिला सकते हैं।अगर आप कई सॉर्ट विकल्प देते हैं, तो हर सॉर्ट मोड को अलग सूची मानें और उसके अपने कर्सर नियम रखें। कर्सर केवल एक सटीक ऑर्डरिंग के लिए ही अर्थपूर्ण होता है।
आप API की सतह को छोटा रख सकते हैं: दो इनपुट, दो आउटपुट।
एक limit भेजें (आप कितनी आइटम्स चाहते हैं) और एक वैकल्पिक cursor (कहाँ से जारी रखना है)। अगर कर्सर गायब है, सर्वर पहला पेज लौटाएगा।
उदाहरण रिक्वेस्ट:
GET /api/messages?limit=30&cursor=eyJjcmVhdGVkX2F0IjoiMjAyNi0wMS0xNlQxMDowMDowMFoiLCJpZCI6Ijk4NzYifQ==
आइटम्स और एक next_cursor लौटाएँ। अगर अगला पेज नहीं है, तो next_cursor: null लौटाएँ। क्लाइंट्स को कर्सर को एक टोकन की तरह मानना चाहिए, जिसे वे एडिट न करें।
{
"items": [ {"id":"9876","created_at":"2026-01-16T10:00:00Z","subject":"..."} ],
"next_cursor": "...",
"has_more": true
}
सर्वर-साइड लॉजिक साधारण शब्दों में: एक स्थिर क्रम में सॉर्ट करें, कर्सर का उपयोग करके फ़िल्टर करें, फिर limit लागू करें।
अगर आप (created_at DESC, id DESC) के अनुसार नवीनतम-फ़र्स्ट सॉर्ट करते हैं, तो कर्सर को (created_at, id) में डिकोड करें, फिर उन रोज़ को फ़ेच करें जहाँ (created_at, id) कर्सर पेयर से सख्ती से छोटा है, उसी ऑर्डर को लागू करें, और limit रो लें।
आप कर्सर को बेस64 JSON ब्लॉब के रूप में एन्कोड कर सकते हैं (आसान) या साइन/एनक्रिप्टेड टोकन के रूप में (ज़्यादा काम)। ओपैक सुरक्षित है क्योंकि इससे आप बाद में इंटरनल बदल सकेंगे बिना क्लाइंट्स को तोड़े।
साथ ही समझदारी से डिफ़ॉल्ट रखें: मोबाइल के लिए एक वाजिब डिफ़ॉल्ट (अक्सर 20–30), वेब के लिए एक डिफ़ॉल्ट (अक्सर 50), और एक हार्ड सर्वर मैक्स ताकि कोई बग्गी क्लाइंट 10,000 रो न माँग सके।
एक स्थिर फ़ीड का वादा मुख्य रूप से यह है: एक बार उपयोगकर्ता आगे स्क्रॉल करना शुरू कर दे तो वे अभी तक न देखे गए आइटम्स दूसरों के क्रिएट/डिलीट/एडिट के कारण इधर-उधर न हों।
कर्सर पेजिनेशन के साथ, इन्सर्ट सबसे आसान हैं। नए रिकॉर्ड्स रिफ्रेश पर दिखने चाहिए, न कि पहले से लोड किए गए पृष्ठों के बीच में। अगर आप (created_at DESC, id DESC) से ऑर्डर करते हैं, तो नए आइटम्स स्वाभाविक रूप से पहले पेज से पहले रहेंगे, इसलिए आपका मौजूदा कर्सर पुराने आइटम्स की ओर जारी रहता है।
डिलीट्स को लिस्ट को फिर से व्यवस्थित नहीं करना चाहिए। अगर कोई आइटम डिलीट हो जाता है, तो जब आप उसे फ़ेच करते तब वह बस वापस नहीं आएगा। अगर आपको पेज साइज लगातार बनाए रखने की ज़रूरत है, तो तब तक फ़ेच करते रहें जब तक आप limit विज़िबल आइटम्स इकट्ठे न कर लें।
एडिट्स वही जगह है जहाँ टीमें गलती से बग्स फिर से ला देती हैं। मुख्य सवाल यह है: क्या कोई एडिट सॉर्ट पोज़िशन बदल सकता है?
स्क्रॉलिंग सूचियों के लिए आम तौर पर स्नैपशॉट-स्टाइल व्यवहार बेहतर होता है: created_at जैसे अपरिवर्तनीय कुंजी से पेज करें। एडिट्स कंटेंट बदल सकते हैं, पर आइटम अपनी पोज़िशन पर कूदता नहीं है।
लाइव-फीड व्यवहार edited_at जैसे किसी पर सॉर्ट करता है। इससे कूद हो सकती है (एक पुराना आइटम एडिट होकर ऊपर आ सकता है)। अगर आप यह चुनते हैं, तो सूची को लगातार बदलता हुआ मानें और UX को रिफ्रेश के अनुसार डिज़ाइन करें।
कर्सर को "इस विशेष रो को ढूँढो" पर निर्भर न बनाएं। स्थिति को वैल्यूज़ के रूप में एन्कोड करें, जैसे आखिरी लौटाए गए आइटम का {created_at, id}। तब अगली क्वेरी मानों के आधार पर होगी, न कि रो के अस्तित्व पर:
WHERE (created_at, id) < (:created_at, :id)id) शामिल करें ताकि डुप्लिकेट्स से बचेंफ़ॉरवर्ड पेजिंग आसान हिस्सा है। मुश्किल UX सवाल पीछे पेजिंग, रिफ्रेश और रैंडम एक्सेस होते हैं।
पीछे पेजिंग के लिए दो तरीके आमतौर पर काम करते हैं:
next_cursor पुराने आइटम्स के लिए और prev_cursor नए आइटम्स के लिए) जबकि ऑन-स्क्रीन एक ही सॉर्ट ऑर्डर रखें।कर्सर के साथ रैंडम जंप कठिन है क्योंकि "पेज 20" का स्थिर मतलब तब नहीं रहता जब लिस्ट बदलती है। अगर आपको सचमुच जंप चाहिए तो किसी एंकर पर जंप करें जैसे “इस टाइमस्टैम्प के चारों ओर” या “इस मेसेज id से शुरू करें,” न कि पेज इंडेक्स पर।
मोबाइल पर कैशिंग मायने रखती है। लिस्ट स्टेट (क्वेरी + फ़िल्टर्स + सॉर्ट) के अनुसार कर्सर स्टोर करें, और हर टैब/व्यू को अपनी अलग लिस्ट समझें। इससे "टैब बदलो और सब कुछ गड़बड़ हो जाए" जैसा व्यवहार रुकता है।
ज्यादातर कर्सर पेजिनेशन समस्याएँ डेटाबेस की वजह से नहीं होतीं। वे अनुरोधों के बीच छोटे असंगतताओं से आती हैं जो केवल रीयल ट्रैफ़िक में दिखती हैं।
सबसे बड़े अपराधी:
created_at) जिससे टाई होने पर डुप्लिकेट्स या मिसिंग आइटम्स बनते हैं।next_cursor लौटाना जो वास्तव में आखिरी लौटाए गए रो से मेल नहीं खाता।अगर आप Koder.ai जैसे प्लेटफ़ॉर्म पर ऐप बनाते हैं तो ये एज केस जल्दी दिखते हैं क्योंकि वेब और मोबाइल क्लाइंट अक्सर एक ही एंडपॉइंट साझा करते हैं। एक स्पष्ट कर्सर कॉन्ट्रैक्ट और एक निर्धारक ऑर्डरिंग नियम दोनों क्लाइंट्स को सुसंगत रखता है।
पेजिनेशन को “पूर्ण” कहने से पहले इन्सर्ट्स, डिलीट्स और रिट्राईज़ के तहत व्यवहार सत्यापित करें।
next_cursor उस अंतिम लौटाए गए रो से लिया गया होlimit का एक सुरक्षित अधिकतम और एक डॉक्युमेंटेड डिफ़ॉल्ट होरिफ्रेश के लिए एक स्पष्ट नियम चुनें: या तो उपयोगकर्ता पुल-टू-रिफ्रेश करें ताकि टॉप पर नए आइटम आएँ, या आप समय-समय पर चेक करें "क्या मेरे पहले आइटम से नया कुछ है?" और एक "नए आइटम" बटन दिखाएँ। सुसंगतता ही वह चीज़ है जो लिस्ट को भूतिया की तरह नहीं बल्कि भरोसेमंद बनाती है।
एक सपोर्ट इनबॉक्स की कल्पना करें जिसे एजेंट वेब पर उपयोग करते हैं, जबकि मैनेजर उसी इनबॉक्स को मोबाइल पर देखता है। लिस्ट नवीनतम पहले सॉर्ट की हुई है। लोग एक ही बात उम्मीद करते हैं: जब वे आगे स्क्रॉल करें तो आइटम्स कूदें नहीं, दोहराएं नहीं, या गायब नहीं हों।
ऑफसेट पेजिंग के साथ, एक एजेंट पेज 1 (आइटम 1–20) लोड करता है और फिर पेज 2 के लिए offset=20 करता है। पढ़ते समय ऊपर दो नए संदेश आते हैं। अब offset=20 उस जगह को इंगित करता है जो पहले दूसरी जगह थी। उपयोगकर्ता डुप्लिकेट्स देखता है या संदेश मिस कर देता है।
कर्सर पेजिनेशन के साथ, ऐप पूछता है “इस कर्सर के बाद अगले 20 आइटम” जहाँ कर्सर उस आखिरी आइटम पर आधारित है जिसे उपयोगकर्ता ने वास्तव में देखा था (आम तौर पर (created_at, id))। नए संदेश दिन भर आ सकते हैं, पर अगला पेज अब भी ठीक उसी जगह से शुरू होता है जहाँ उपयोगकर्ता ने आखिरी संदेश देखा था।
शिप करने से पहले परीक्षण करने का एक सरल तरीका:
अगर आप तेज़ प्रोटोटाइप कर रहे हैं तो Koder.ai चैट प्रॉम्प्ट से एंडपॉइंट और क्लाइंट फ़्लो्स स्कैफ़ोल्ड करने में मदद कर सकता है, फिर Planning Mode, स्नैपशॉट्स और रोलबैक का उपयोग करके सुरक्षित रूप से आगे बढ़ें जब कोई पेजिनेशन बदलाव टेस्टिंग में आश्चर्यजनक हो।
ऑफसेट पेजिंग “N पंक्तियाँ छोड़ो” पर निर्भर करती है, इसलिए जब नई पंक्तियाँ डाली जाती हैं या पुरानी हट जाती हैं तो रो काउंट शिफ्ट हो जाता है। वही ऑफसेट अचानक अलग आइटम्स की ओर इशारा कर सकता है, जिससे स्क्रॉल करते समय डुप्लिकेट्स और गैप्स बनते हैं।
कर्सर पेजिनेशन उस स्थिति का बुकमार्क इस्तेमाल करती है जो “अंतिम देखे गए आइटम के बाद” को दर्शाता है। अगली रिक्वेस्ट उसी निर्धारक क्रम में उस स्थिति से जारी रहती है, इसलिए टॉप पर हुए इन्सर्ट्स और बीच में हुए डिलीट्स आपके पेज बाउंड्री को ऑफसेट की तरह नहीं हिलाते।
एक निर्धारक सॉर्ट और एक टाई-ब्रेकर का उपयोग करें, आमतौर पर (created_at, id) एक साथ। created_at प्रोडक्ट-फ्रेंडली ऑर्डर देता है और id प्रत्येक पोज़िशन को यूनिक बनाता है ताकि टाइमस्टैम्प टाई होने पर आप आइटम रिपीट या स्किप न करें।
अगर आप updated_at के आधार पर सॉर्ट करते हैं तो एडिट्स आइटम्स को पन्नों के बीच ऊपर-नीचे कर सकते हैं, जो “स्टेबल स्क्रोल फ़ॉरवर्ड” अपेक्षा तोड़ देता है। अगर आपको लाइव “हाल ही में अपडेट” व्यू चाहिए, तो UI को रिफ्रेश के लिए डिज़ाइन करें और पुनःक्रमण स्वीकार करें।
एक ओपैक टोकन के रूप में next_cursor लौटाएँ और क्लाइंट से वह बिना बदले वापस भेजवाएँ। सरल तरीका है अंतिम आइटम के (created_at, id) को बेस64 JSON ब्लॉब में एन्कोड करना, पर महत्वपूर्ण यह है कि क्लाइंट उसे ओपैक मानकर रखे ताकि आप बाद में इंटर्नल बदल सकें।
कर्सर मानों से अगली क्वेरी बनाएं, किसी विशेष रो को "ढूँढो" इस पर निर्भर न करें। अगर अंतिम आइटम डिलीट हो गया है, तो भी संग्रहीत (created_at, id) एक स्थिति परिभाषित करता है, इसलिए आप उसी क्रम में आगे बढ़ सकते हैं (उदा. DESC के लिए WHERE (created_at, id) < (:created_at, :id)) ।
सख्त तुलना (< बनाम <=) और यूनिक टाई-ब्रेकर का उपयोग करें, और हमेशा next_cursor उस अंतिम आइटम से लें जिसे आपने वास्तव में रिटर्न किया। अधिकतर रिपीट बग <= का उपयोग, टाई-ब्रेकर न होने, या गलत रो से next_cursor जनरेट करने के कारण होते हैं।
एक स्पष्ट नियम चुनें: रिफ्रेश टॉप पर नए आइटम लाएगा, जबकि स्क्रोल-फॉरवर्ड मौजूदा कर्सर से पुराने आइटमों में जारी रहेगा। रिफ्रेश semantics को उसी कर्सर फ़्लो में मिलाएँ नहीं, वरना उपयोगकर्ता पुनःक्रमण देखेंगे और लिस्ट अविश्वसनीय लगेगी।
एक कर्सर केवल एक ही सटीक ऑर्डरिंग और फ़िल्टर सेट के लिए मान्य होता है। अगर क्लाइंट सॉर्ट मोड, सर्च क्वेरी, या फ़िल्टर्स बदलता है, तो उसे बिना कर्सर के नई पेजिनेशन सत्र शुरू करनी चाहिए और प्रत्येक लिस्ट स्टेट के लिए कर्सर अलग से स्टोर करें।
कर्सर पेजिनेशन अनुक्रमिक ब्राउज़िंग के लिए उत्तम है पर स्थिर “पेज 20” जैसी रैंडम एक्सेस के लिए नहीं, क्योंकि डेटासेट बदल सकता है। अगर आपको जंप करना ज़रूरी है तो किसी एंकर पर जंप करें—जैसे “इस टाइमस्टैम्प के आसपास” या “इस id के बाद से”—और वहाँ से कर्सर से पेजिनेशन करें।