Cursor Pagination στην ανάπτυξη κινητών — τι είναι, αρχή και υλοποίηση

Συγγραφέας: IT Sectr Δημοσιεύτηκε: 2026-03-11 Χρόνος ανάγνωσης: 9 λεπ

Cursor Pagination — μέθοδος σελιδοποίησης δεδομένων που χρησιμοποιεί ένα μοναδικό δρομέα για πλοήγηση σε ένα ταξινομημένο σύνολο εγγραφών. Σύμφωνα με το GraphQL Specification (2025), η σελιδοποίηση με δρομέα είναι το συνιστώμενο πρότυπο για API που εργάζονται με δυναμικά δεδομένα. Cursor Pagination εξαλείφει τα κύρια μειονεκτήματα της προσέγγισης Offset: αστάθεια κατά την εισαγωγή και πτώση απόδοσης σε μεγάλες μετατοπίσεις.

Κύρια σημεία

  • Cursor Pagination — μέθοδος σελιδοποίησης όπου κάθε εγγραφή έχει ένα μοναδικό αναγνωριστικό-δρομέα για πλοήγηση.
  • Δρομέας — μοναδικός δείκτης θέσης στο σύνολο δεδομένων (συνήθως ID, UUID, timestamp), που δεν αλλάζει κατά την εισαγωγή.
  • Σταθερότητα — νέες εγγραφές που προστίθενται μεταξύ αιτημάτων δεν μετακινούν το δρομέα, εξαλείφοντας διπλότυπα και κενά.
  • Απόδοση — το ερώτημα WHERE id > cursor χρησιμοποιεί αποτελεσματικά το ευρετήριο, χωρίς να χάνει ταχύτητα σε μεγάλα σύνολα.
  • Περιορισμός — η σελιδοποίηση με δρομέα δεν υποστηρίζει πλοήγηση κατά αριθμό σελίδας (δεν μπορείτε να μεταβείτε στη σελίδα 5).

Τι είναι το Cursor Pagination;

Cursor Pagination (σελιδοποίηση με δρομέα) — μέθοδος σελιδοποίησης όπου ο διακομιστής επιστρέφει μαζί με τα δεδομένα έναν ειδικό δείκτη — δρομέα. Ο πελάτης χρησιμοποιεί αυτόν το δρομέα στο επόμενο αίτημα για να λάβει την επόμενη μερίδα εγγραφών. Ο δρομέας είναι το μοναδικό αναγνωριστικό του τελευταίου στοιχείου της τρέχουσας σελίδας.

Σε αντίθεση με το Offset-pagination, όπου ο πελάτης λέει “δώσε μου τη σελίδα 5 με 20 εγγραφές”, η σελιδοποίηση με δρομέα λειτουργεί διαφορετικά: “δώσε μου 20 εγγραφές μετά την εγγραφή με ID = 83”. Ο διακομιστής εκτελεί το ερώτημα με συνθήκη WHERE id > 83 και LIMIT 20. Αυτή η προσέγγιση εγγυάται ότι κάθε εγγραφή καταλήγει ακριβώς σε μία σελίδα ανεξάρτητα από εισαγωγές.

Η έννοια της σελιδοποίησης με δρομέα έχει γίνει ευρέως διαδεδομένη χάρη στην προδιαγραφή Relay Connection (GraphQL), η οποία έκανε το cursor-based pagination πρότυπο για σύγχρονα API. Το Relay ορίζει τη μορφή απάντησης: edges (πίνακας εγγραφών με δρομείς), pageInfo (hasNextPage, hasPreviousPage, startCursor, endCursor).

Ιστορία εμφάνισης

Το cursor-pagination δεν είναι νέα τεχνική — χρησιμοποιούνταν σε βάσεις δεδομένων πολύ πριν από την εμφάνιση του ιστού. Στην SQL ονομάζεται keyset pagination ή seek method. Η μέθοδος έγινε δημοφιλής σε API μετά τη δημοσίευση της προδιαγραφής Relay το 2015, η οποία επισημοποίησε τη μορφή του δρομέα ως κωδικοποιημένο base64 string για ομοιομορφία μετάδοσης μέσω HTTP.

Πώς λειτουργεί η σελιδοποίηση με δρομέα

Η βασική αρχή της σελιδοποίησης με δρομέα — το ερώτημα χρησιμοποιεί συνθήκη WHERE σε ένα indexed πεδίο για τοποθέτηση, όχι μετατόπιση. Για μπροστινή κατεύθυνση εφαρμόζεται WHERE id > last_id, για πίσω κατεύθυνση — WHERE id < first_id. Το ευρετήριο B-tree επιτρέπει την εύρεση της πρώτης εγγραφής μετά το δρομέα σε O(log n), παρέχοντας σταθερό χρόνο απόκρισης.

sql
-- Λήψη 20 εγγραφών μετά τον κέρσορα '83'
SELECT id, title, created_at
FROM posts
WHERE id < 83
ORDER BY id DESC
LIMIT 20;

-- Λήψη 20 εγγραφών ΠΡΙΝ τον κέρσορα '83' (πίσω)
SELECT id, title, created_at
FROM posts
WHERE id > 83
ORDER BY id ASC
LIMIT 20;

Μορφή δρομέα

Ο δρομέας μπορεί να είναι απλός (τιμή ID) ή σύνθετος (αποτελούμενος από πολλά πεδία). Απλοί δρομείς είναι το πρωτεύον κλειδί της εγγραφής, για παράδειγμα auto-increment id ή UUID. Σύνθετοι δρομείς χρησιμοποιούνται για ταξινόμηση κατά μη μοναδικά πεδία, για παράδειγμα (created_at, id), όπου το id εγγυάται μοναδικότητα σε πανομοιότυπες χρονικές σημάνσεις.

Τυπική μορφή API — δρομέας ως κωδικοποιημένο base64 string. Ο διακομιστής αποκωδικοποιεί το δρομέα, εξάγει την τιμή και κατασκευάζει το ερώτημα SQL. Η κωδικοποίηση Base64 κρύβει την εσωτερική δομή του δρομέα από τον πελάτη και επιτρέπει την αλλαγή μορφής χωρίς απώλεια συμβατότητας προς τα πίσω. Ο πελάτης λαμβάνει τους δρομείς στο πεδίο endCursor της απάντησης και τους μεταδίδει ως string στο επόμενο αίτημα.

Πλοήγηση μπροστά και πίσω

Η σελιδοποίηση με δρομέα υποστηρίζει αμφίδρομη πλοήγηση. Για κίνηση μπροστά (next) χρησιμοποιείται ο δρομέας του τελευταίου στοιχείου της τρέχουσας σελίδας, για πίσω (previous) — ο δρομέας του πρώτου στοιχείου. Οι παράμετροι after και before στο αίτημα καθορίζουν την κατεύθυνση: after λαμβάνει εγγραφές μετά το δρομέα, before — πριν από το δρομέα.

Cursor vs Offset pagination

Η επιλογή μεταξύ σελιδοποίησης με δρομέα και Offset-pagination — μία από τις βασικές αρχιτεκτονικές ερωτήσεις κατά το σχεδιασμό API. Κάθε μέθοδος έχει δυνατά και αδύνατα σημεία. Cursor-pagination κερδίζει σε σενάρια με δυναμικά δεδομένα, Offset — σε σενάρια με αυθαίρετη πλοήγηση.

ΧαρακτηριστικόCursorOffset
Σταθερότητα κατά την εισαγωγήΥψηλή (χωρίς διπλότυπα)Χαμηλή (μετατόπιση σελίδων)
Απόδοση σε μεγάλα σύνολαO(log n) — σταθερήO(n) — μειώνεται με αύξηση
Πλοήγηση κατά αριθμό σελίδαςΌχιΝαι (page=5)
Πολυπλοκότητα υλοποίησηςΜέτριαΧαμηλή
Υποστήριξη σε RESTcursor/before/afterpage/offset
Υποστήριξη σε GraphQLΠρότυπο RelayΔεν συνιστάται

Γιατί το Offset χάνει σε μεγάλη κλίμακα

Το Offset-pagination εκτελεί πλήρη σάρωση πίνακα μέχρι τη θέση OFFSET. Σε offset=100000, η βάση δεδομένων διαβάζει και παραλείπει 100000 γραμμές, ακόμα κι αν το LIMIT είναι 20. Τα MySQL και PostgreSQL δεν μπορούν να βελτιστοποιήσουν το OFFSET — αυτό είναι χαρακτηριστικό της υλοποίησης LIMIT/OFFSET στην SQL. Το Cursor-pagination χρησιμοποιεί το ευρετήριο B-tree, το οποίο βρίσκει τη θέση σε O(log n).

Ένα πρόσθετο πρόβλημα του Offset — “παράλειψη” εγγραφών κατά τη σελιδοποίηση προς τα πίσω. Εάν ο χρήστης φόρτωσε τη σελίδα 5 και εκείνη τη στιγμή προστέθηκαν νέες εγγραφές, κατά το αίτημα για τη σελίδα 6 θα δει ξανά την εγγραφή από τη σελίδα 5 ή θα παραλείψει νέες. Το Cursor-pagination εξαλείφει πλήρως αυτό το σενάριο: ο δρομέας δείχνει σε συγκεκριμένο σημείο στο σύνολο και οι εισαγωγές δεν αλλάζουν τη θέση.

Υλοποίηση του Cursor Pagination

Ας εξετάσουμε την υλοποίηση της σελιδοποίησης με δρομέα στο backend (Kotlin + Spring) και στον πελάτη (Android + Retrofit). Ο διακομιστής λαμβάνει παραμέτρους after, before, limit και επιστρέφει μια λίστα εγγραφών με δρομείς και pageInfo. Η τυπική απάντηση περιέχει hasNextPage και hasPreviousPage για τη διαχείριση του UI σελιδοποίησης.

kotlin
@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
        )
    )
}

Υλοποίηση πελάτη σε Android

Στην πλευρά του πελάτη, η σελιδοποίηση με δρομέα υλοποιείται μέσω του PagingSource από το Paging 3, όπου το κλειδί είναι ο δρομέας (Long). Το PagingSource.load λαμβάνει το LoadParams.key — δρομέα της τελευταίας φορτωμένης εγγραφής. LoadResult.Page επιστρέφει δεδομένα και nextKey — δρομέα για την επόμενη σελίδα. Όταν nextKey = null — η σελιδοποίηση ολοκληρώθηκε.

kotlin
// 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

Στο GraphQL, η σελιδοποίηση με δρομέα υλοποιείται μέσω του προτύπου Connection Relay. Κάθε τύπος έχει Connection (με pageInfo και edges) και Edge (node + cursor). Το αίτημα μεταδίδει παραμέτρους first, after, last, before. Ο διακομιστής επιστρέφει έναν πίνακα edges με δρομείς και pageInfo με hasNextPage/hasPreviousPage.

Πότε να χρησιμοποιείτε το Cursor Pagination

Το cursor-pagination συνιστάται για API που εργάζονται με δυναμικά δεδομένα, όπου οι εγγραφές προστίθενται ή διαγράφονται συχνά. Κλασικά παραδείγματα: ροή ειδήσεων σε κοινωνικό δίκτυο, μηνύματα chat, ιστορικό συναλλαγών, σχόλια σε αναρτήσεις. Σε όλα αυτά τα σενάρια, η συνέπεια και η απουσία διπλοτύπων είναι σημαντική.

  • Chat και messenger — κάθε νέο μήνυμα προστίθεται στην κορυφή της λίστας. Το Offset-pagination διαταράσσεται με κάθε νέο μήνυμα.
  • Κοινωνικά δίκτυα και ροές — οι αναρτήσεις δημοσιεύονται συνεχώς. Η σελιδοποίηση με δρομέα εγγυάται ότι ο χρήστης δεν χάνει καμία ανάρτηση.
  • Ιστορικό παραγγελιών και συναλλαγών — τα δεδομένα αλλάζουν λιγότερο συχνά, αλλά η συνέπεια είναι κρίσιμη για χρηματοοικονομική αναφορά.
  • API με μεγάλους όγκους δεδομένων — εκατομμύρια εγγραφές. Το Cursor-pagination διατηρεί την απόδοση εκεί που το Offset αρχίζει να επιβραδύνει.
  • GraphQL API — το πρότυπο Relay απαιτεί τη χρήση cursor-based pagination σύμφωνα με την προδιαγραφή.
  • Εφαρμογές κινητών με ατελείωτη κύλιση — ο χρήστης κυλά προς τα κάτω, φορτώνοντας νέες μερίδες. Η προσέγγιση δρομέα παρέχει ομαλή UX χωρίς διπλότυπα.

Πότε το Cursor-pagination δεν είναι κατάλληλο

Υπάρχουν σενάρια όπου το Offset-pagination είναι πιο βολικό: διαχειριστικοί πίνακες όπου απαιτείται πλοήγηση κατά αριθμό σελίδας; αναζήτηση με σελιδοποίηση όπου τα αποτελέσματα μπορεί να αλλάζουν; αναφορές και αναλυτικά στοιχεία όπου χρειάζεται σταθερός σύνδεσμος προς τη σελίδα 5. Σε αυτές τις περιπτώσεις, τα πλεονεκτήματα του δρομέα δεν υπερτερούν της πολυπλοκότητας υλοποίησης.

Η σελιδοποίηση με δρομέα δεν υποστηρίζει “άλμα” σε αυθαίρετη σελίδα — ο χρήστης δεν μπορεί να κάνει κλικ στο “Σελίδα 5” και να μεταβεί εκεί. Αυτός είναι αρχιτεκτονικός περιορισμός: για τον υπολογισμό του συνολικού αριθμού σελίδων απαιτείται ξεχωριστό ερώτημα COUNT, το οποίο μπορεί να είναι δαπανηρό για μεγάλους πίνακες. Σε τέτοιες περιπτώσεις, υβριδική προσέγγιση: cursor για δεδομένα + count για σελιδοποίηση.

Συχνές ερωτήσεις

Τι είναι ο δρομέας στο Cursor Pagination;

Ο δρομέας είναι το μοναδικό αναγνωριστικό της εγγραφής που υποδεικνύει τη θέση στο σύνολο δεδομένων. Μπορεί να είναι απλός (ID εγγραφής) ή σύνθετος (πολλά πεδία). Ο πελάτης λαμβάνει το δρομέα της τελευταίας εγγραφής της σελίδας και τον μεταδίδει στο επόμενο αίτημα για να λάβει την επόμενη μερίδα.

Σε τι είναι καλύτερο το Cursor Pagination από το Offset;

Το cursor-pagination δεν υπόκειται σε μετατόπιση κατά την προσθήκη νέων εγγραφών — κάθε στοιχείο καταλήγει ακριβώς σε μία σελίδα. Επίσης διατηρεί την ταχύτητα σε μεγάλους όγκους χάρη στη χρήση ευρετηρίων αντί της σάρωσης των πρώτων n γραμμών. Το Offset είναι απλούστερο αλλά ασταθές για δυναμικά δεδομένα.

Μπορεί να υλοποιηθεί η σελιδοποίηση με δρομέα χωρίς GraphQL;

Ναι, η σελιδοποίηση με δρομέα δεν είναι συνδεδεμένη με το GraphQL. Μπορεί να υλοποιηθεί σε οποιοδήποτε REST API μεταδίδοντας το δρομέα ως παράμετρο ερωτήματος ?after=83&limit=20. Η απάντηση πρέπει να περιέχει pageInfo με endCursor και hasNextPage — αυτό επιτρέπει στον πελάτη να διαχειρίζεται τη φόρτωση χωρίς γνώση της εσωτερικής δομής του δρομέα.

Ποιο δρομέα να χρησιμοποιήσουμε — ID, UUID ή timestamp;

Auto-increment ID — η βέλτιστη επιλογή: αυξάνεται μονότονα, δεν αλλάζει, ευρετηριάζεται αποτελεσματικά. Το UUID v7 (χρονικά ταξινομημένο) είναι επίσης κατάλληλο. Το timestamp μπορεί να δώσει διπλότυπα στον ίδιο χρόνο, γι' αυτό συνδυάζεται με ID: (created_at, id) για εγγύηση μοναδικότητας του δρομέα.

Πώς να μάθουμε το συνολικό αριθμό σελίδων στη σελιδοποίηση με δρομέα;

Η σελιδοποίηση με δρομέα δεν παρέχει συνολικό αριθμό σελίδων — αυτός είναι ο περιορισμός της. Εάν χρειάζεται πληροφορία για το total, εκτελέστε ξεχωριστό ερώτημα COUNT με τα ίδια φίλτρα. Για μεγάλους πίνακες, χρησιμοποιήστε κατά προσέγγιση μέτρηση μέσω EXPLAIN ή cached total από αναλυτικά στοιχεία.

Σύνοψη

  • Cursor Pagination — μέθοδος σελιδοποίησης με πλοήγηση κατά μοναδικό αναγνωριστικό εγγραφής αντί μετατόπισης.
  • Δρομέας εγγυάται σταθερότητα του συνόλου κατά την εισαγωγή: νέες εγγραφές δεν μετατοπίζουν ήδη φορτωμένες σελίδες.
  • Απόδοση σε μεγάλους όγκους δεδομένων παραμένει υψηλή (O(log n)) χάρη στη χρήση ευρετηρίων B-tree.
  • Cursor-pagination είναι κατάλληλο για δυναμικά δεδομένα: chat, ροές ειδήσεων, συναλλαγές, σχόλια.
  • Κύριος περιορισμός — έλλειψη πλοήγησης κατά αριθμό σελίδας και αδυναμία μετάβασης σε αυθαίρετη σελίδα.
  • Υλοποίηση χρησιμοποιεί WHERE id μετά από δρομέα, παραμέτρους after/before και pageInfo στην απάντηση.
  • Πρότυπο — Relay Connection GraphQL, αλλά το REST API με παραμέτρους δρομέα είναι επίσης ευρέως διαδεδομένο.

Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση

Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.

Συζήτηση έργου

Διαβάστε επίσης