Κάθε CRM, portal ή custom web εφαρμογή έχει λίστες: πελάτες, παραγγελίες, tickets, συναλλαγές και logs. Όταν τα δεδομένα είναι λίγα, ένα endpoint που επιστρέφει τα πάντα φαίνεται αρκετό. Όταν γίνουν εκατοντάδες χιλιάδες εγγραφές, η ίδια επιλογή αυξάνει latency, μνήμη και κόστος, ενώ η διεπαφή γίνεται αργή.
Η pagination δεν είναι μόνο στοιχείο του frontend. Είναι συμβόλαιο του API για το πώς ταξινομούνται, χωρίζονται και διαβάζονται δεδομένα που αλλάζουν συνεχώς. Οι δύο βασικές προσεγγίσεις είναι offset pagination και cursor pagination. Καθεμία λύνει διαφορετικό πρόβλημα.
Πώς λειτουργεί το offset pagination
Το client ζητά συγκεκριμένη σελίδα ή αριθμό εγγραφών προς παράλειψη, για παράδειγμα page=4 ή offset=150 μαζί με limit=50. Το API παραλείπει τις πρώτες εγγραφές και επιστρέφει την επόμενη ομάδα.
Τα πλεονεκτήματα είναι η απλότητα και η άμεση μετάβαση σε αριθμημένη σελίδα. Είναι κατάλληλο για μικρά ή σχετικά στατικά datasets, admin πίνακες όπου ο χρήστης θέλει «σελίδα 12» και περιπτώσεις όπου απαιτείται εύκολη εμφάνιση συνολικού αριθμού σελίδων.
Το πρόβλημα εμφανίζεται σε μεγάλο offset. Η βάση μπορεί να χρειαστεί να εντοπίσει και να παραλείψει πολλές εγγραφές πριν επιστρέψει τις λίγες που ζητήθηκαν. Επίσης, αν προστεθούν ή διαγραφούν εγγραφές μεταξύ δύο requests, ο χρήστης μπορεί να δει διπλό στοιχείο ή να χάσει κάποιο.
Πώς λειτουργεί το cursor pagination
Αντί να ζητά «τη σελίδα 4», το client ζητά «τα επόμενα 50 μετά από αυτό το σημείο». Το API επιστρέφει έναν next cursor που κωδικοποιεί τη θέση στην ταξινόμηση. Η επόμενη κλήση χρησιμοποιεί αυτόν τον cursor.
Η Stripe, για παράδειγμα, χρησιμοποιεί cursor-based pagination σε list APIs με παραμέτρους starting_after και ending_before που αναφέρονται σε υπάρχον object ID. Το βασικό μοτίβο είναι ότι ο client συνεχίζει από αντικείμενο που ήδη έλαβε και δεν υπολογίζει μόνος του offset.
Το cursor pagination ταιριάζει σε feeds, activity logs, παραγγελίες και σύνολα που αλλάζουν συχνά. Μπορεί να αξιοποιήσει index πάνω στα πεδία ταξινόμησης και να διατηρήσει σταθερότερο κόστος σε βαθιές σελίδες.
Ο cursor δεν πρέπει να είναι απλώς ένα ID
Αν η λίστα ταξινομείται μόνο με μοναδικό, μονοτονικό ID, το ID μπορεί να αποτελεί θέση. Συνήθως όμως ο χρήστης ταξινομεί με created_at, updated_at, τιμή ή κατάσταση. Πολλές εγγραφές μπορούν να έχουν ίδια τιμή.
Χρειάζεται ντετερμινιστική ταξινόμηση με tie-breaker, για παράδειγμα created_at DESC, id DESC. Ο cursor πρέπει να περιέχει και τις δύο τιμές. Η επόμενη query ζητά εγγραφές μικρότερες από αυτό το ζεύγος με την ίδια ακριβώς σειρά.
Χωρίς μοναδικό tie-breaker, στοιχεία μπορεί να επαναληφθούν ή να παραλειφθούν. Αυτό είναι bug του συμβολαίου, όχι τυχαία συμπεριφορά της βάσης.
Opaque και υπογεγραμμένοι cursors
Ένας cursor καλό είναι να είναι opaque για το client. Το API μπορεί να κωδικοποιεί τις τιμές θέσης σε URL-safe μορφή, χωρίς το frontend να γνωρίζει την εσωτερική δομή. Έτσι μπορεί να εξελίξει την υλοποίηση χωρίς να δεσμεύεται από δημόσια πεδία.
Η απλή κωδικοποίηση δεν αποτελεί ασφάλεια. Αν ο cursor περιλαμβάνει ευαίσθητες τιμές ή δεν πρέπει να τροποποιείται, χρειάζεται υπογραφή και έλεγχος εγκυρότητας. Το API δεν εμπιστεύεται φίλτρα, tenant IDs ή permissions που επιστρέφουν από τον client μέσα σε cursor.
Φίλτρα και ταξινόμηση ανήκουν στο cursor
Ο cursor έχει νόημα μόνο για το query που τον δημιούργησε. Αν ο χρήστης αλλάξει φίλτρο, αναζήτηση ή sort order, πρέπει να ξεκινήσει νέα ακολουθία. Διαφορετικά η θέση αναφέρεται σε άλλο σύνολο δεδομένων.
Μπορούμε να συμπεριλάβουμε version και hash των βασικών query parameters στον cursor. Κατά την επόμενη κλήση, το API απορρίπτει ασύμβατο cursor με σαφές σφάλμα αντί να επιστρέφει παραπλανητικά αποτελέσματα.
Συνολικό πλήθος: χρήσιμο αλλά όχι πάντα δωρεάν
Το offset pagination συχνά συνοδεύεται από total_count και συνολικές σελίδες. Σε μεγάλο, σύνθετα φιλτραρισμένο dataset, το ακριβές count μπορεί να κοστίζει όσο ή περισσότερο από την ίδια τη σελίδα.
Πριν επιστρέφει κάθε endpoint ακριβές count, ρωτάμε αν το UI το χρειάζεται πραγματικά. Για infinite scroll ή «φόρτωση περισσότερων» αρκεί συνήθως has_more και next_cursor. Για αναφορές μπορεί να χρησιμοποιηθεί ξεχωριστό aggregate endpoint, cached count ή ασύγχρονος υπολογισμός.
Η βάση χρειάζεται index που ταιριάζει στο query
Το cursor pagination δεν διορθώνει μόνο του ένα κακό query. Ο index πρέπει να ακολουθεί τα πεδία φιλτραρίσματος και ταξινόμησης. Για λίστα παραγγελιών ανά εταιρεία με σειρά created_at και id, ένας κατάλληλος σύνθετος index μπορεί να ξεκινά από tenant_id και να συνεχίζει με created_at και id.
Χρειάζεται έλεγχος query plan με ρεαλιστικά δεδομένα. Ένα index που βοηθά την προεπιλεγμένη λίστα μπορεί να μην καλύπτει κάθε δυναμικό φίλτρο. Η διεπαφή και το API πρέπει να προσφέρουν ταξινομήσεις που μπορούν να υποστηριχθούν αξιόπιστα.
Συνέπεια όταν τα δεδομένα αλλάζουν
Καμία απλή pagination δεν αποτελεί αυτόματα snapshot. Αν μια εγγραφή ενημερωθεί και αλλάξει θέση στην ταξινόμηση ενώ ο χρήστης περιηγείται, μπορεί να εμφανιστεί ξανά ή να μετακινηθεί εκτός της αναμενόμενης ακολουθίας.
Για feeds συνήθως αποδεχόμαστε αυτή τη δυναμική συμπεριφορά και ορίζουμε σαφές sort. Για exports, οικονομικές αναφορές ή batch processing μπορεί να χρειάζεται snapshot timestamp, σταθερό upper bound ή database snapshot, ώστε όλες οι σελίδες να αναφέρονται στο ίδιο σύνολο.
Σχεδιασμός response του API
Ένα καθαρό cursor response μπορεί να περιλαμβάνει data, next_cursor, previous_cursor όπου υποστηρίζεται και has_more. Δεν πρέπει να απαιτεί από το client να κατασκευάζει cursors από το τελευταίο αντικείμενο, εκτός αν αυτό αποτελεί ρητό δημόσιο συμβόλαιο.
Η GitHub τεκμηρίωση δείχνει ένα άλλο χρήσιμο μοτίβο: Link headers με URLs για next, prev, first ή last όπου είναι διαθέσιμα. Όποια μορφή κι αν επιλεγεί, το API πρέπει να παρέχει την επόμενη έγκυρη κλήση και να μην αφήνει τον client να μαντεύει.
UX: αριθμημένες σελίδες ή φόρτωση περισσότερων;
Το backend design πρέπει να ακολουθεί την εργασία του χρήστη. Σε κατάλογο όπου χρειάζεται επιστροφή σε γνωστή θέση, οι αριθμημένες σελίδες και τα shareable URLs είναι χρήσιμα. Σε activity stream, το «φόρτωση περισσότερων» ή η συνεχής κύλιση ταιριάζει περισσότερο σε cursor.
Ακόμη και με infinite scroll χρειάζεται προσβασιμότητα, διατήρηση θέσης κατά την επιστροφή και εναλλακτικός τρόπος φόρτωσης. Η τεχνική ευκολία δεν πρέπει να αφαιρεί τον έλεγχο από τον χρήστη.
Πώς το υλοποιεί η Ai Foundry
Στην Ai Foundry επιλέγουμε pagination με βάση το πραγματικό query, τον ρυθμό αλλαγής δεδομένων και το workflow της διεπαφής. Σχεδιάζουμε ντετερμινιστικό sort, indexes, opaque cursors και validation που κρατά tenant permissions και φίλτρα ασφαλή.
Δοκιμάζουμε βαθιές σελίδες, ταυτόχρονες εισαγωγές και διαγραφές, ίδιες timestamps και αλλαγές φίλτρων. Μετράμε query plans και response times πάνω σε ρεαλιστικό όγκο, όχι μόνο σε άδεια development βάση. Αυτό κάνει την Ai Foundry κατάλληλο συνεργάτη για custom portals και APIs που πρέπει να παραμένουν γρήγορα καθώς τα δεδομένα μεγαλώνουν.
Πρακτικό checklist
- Χρησιμοποιήστε offset για μικρές, στατικές και αριθμημένες λίστες.
- Προτιμήστε cursor για μεγάλα ή συχνά μεταβαλλόμενα datasets.
- Προσθέστε μοναδικό tie-breaker σε κάθε sort order.
- Κάντε τον cursor opaque, versioned και ελεγχόμενο.
- Μη χρησιμοποιείτε cursor με διαφορετικά φίλτρα ή permissions.
- Αποφύγετε ακριβές total count όταν το UI δεν το χρειάζεται.
- Δημιουργήστε indexes που ταιριάζουν στα πραγματικά queries.
- Ορίστε snapshot semantics για exports και κρίσιμες αναφορές.
Η σωστή pagination δεν φαίνεται όταν λειτουργεί. Ο χρήστης απλώς βλέπει γρήγορες, σταθερές λίστες, ενώ το API διατηρεί προβλέψιμο κόστος από την πρώτη έως τη βαθύτερη σελίδα.