Offset Pagination — σελιδοποίηση με μετατόπιση — μέθοδος φόρτωσης δεδομένων ανά σελίδα μέσω HTTP API. Ο πελάτης μεταδίδει τις παραμέτρους offset (μετατόπιση από την αρχή) και limit (μέγεθος σελίδας), και ο διακομιστής επιστρέφει εγγραφές από τη θέση offset. Σύμφωνα με το REST API Tutorial, αυτή η προσέγγιση χρησιμοποιείται ευρέως σε υπηρεσίες RESTful λόγω της απλότητας υλοποίησης. Ωστόσο, σε μεγάλους όγκους δεδομένων, η σελιδοποίηση με offset χάνει απόδοση λόγω της πλήρους σάρωσης του πίνακα μέχρι την απαιτούμενη θέση.
Βασικά σημεία
Offset Pagination — είναι μια μέθοδος διαίρεσης δεδομένων ανά σελίδα, όπου το αίτημα του πελάτη περιέχει δύο παραμέτρους: offset (πόσες εγγραφές να παραλειφθούν) και limit (πόσες εγγραφές να επιστραφούν). Ο διακομιστής εκτελεί ένα ερώτημα SQL με OFFSET και LIMIT, παραλείπει τον καθορισμένο αριθμό γραμμών και επιστρέφει το σύνολο αποτελεσμάτων σταθερού μεγέθους.
Η μέθοδος εμφανίστηκε στις σχεσιακές βάσεις δεδομένων ως ο απλούστερος τρόπος οργάνωσης πλοήγησης μεταξύ σελίδων και μεταφέρθηκε στο HTTP API με την ανάπτυξη της αρχιτεκτονικής REST. Το Offset Pagination δεν απαιτεί αποθήκευση κατάστασης στον διακομιστή — κάθε αίτημα είναι ανεξάρτητο και περιέχει όλες τις απαραίτητες πληροφορίες για την επιλογή.
Σύμφωνα με έρευνα σχεδιασμού API από την Postman (2025), η σελιδοποίηση με offset χρησιμοποιείται στο 72% των δημόσιων REST API, καθιστώντας την κυρίαρχο πρότυπο παρά τους γνωστούς περιορισμούς απόδοσης σε μεγάλους όγκους δεδομένων.
Ένα τυπικό αίτημα REST με Offset Pagination περιλαμβάνει τις παραμέτρους ερωτήματος offset και limit. Η απόκριση περιέχει μια λίστα εγγραφών της ζητούμενης σελίδας και μεταδεδομένα για τη δημιουργία της διεπαφής πλοήγησης.
Η παράμετρος limit περιορίζει τον αριθμό των επιστρεφόμενων εγγραφών και προστατεύει τον διακομιστή και τον πελάτη από υπερβολικό φορτίο. Τυπικές τιμές limit — από 10 έως 50 εγγραφές ανά σελίδα ανάλογα με την πολυπλοκότητα των δεδομένων.
data class PageRequest(
val offset: Int,
val limit: Int
)
data class PageResponse<T>(
val items: List<T>,
val total: Int,
val hasMore: Boolean
)
fun RetrofitApi.fetchPage(request: PageRequest): Call<PageResponse<Item>>
Offset Pagination μετατρέπεται σε ερώτημα SQL με κατασκευές OFFSET και FETCH NEXT (ή LIMIT σε MySQL/SQLite). Ο διακομιστής βάσης δεδομένων σαρώνει τον πίνακα, παραλείπει τον αριθμό γραμμών ίσο με το offset και επιστρέφει τις επόμενες limit γραμμές. Όσο μεγαλύτερο είναι το offset, τόσο περισσότερο διαρκεί το ερώτημα.
Το πρόβλημα απόδοσης σχετίζεται με το γεγονός ότι η βάση δεδομένων δεν μπορεί να πάει απευθείας στη θέση offset — πρέπει να διαβάσει και να απορρίψει όλες τις προηγούμενες γραμμές. Σε offset = 100000 και limit = 20, το DBMS θα διαβάσει 100020 γραμμές και θα επιστρέψει μόνο 20.
SQL — η γλώσσα στην οποία ο διακομιστής εκτελεί τη σελιδοποίηση με offset. Σε PostgreSQL και MySQL χρησιμοποιείται LIMIT, σε SQL Server και Oracle — OFFSET...FETCH. Διαφορετικά DBMS βελτιστοποιούν αυτό το ερώτημα με τον δικό τους τρόπο, αλλά το θεμελιώδες πρόβλημα σάρωσης παραμένει το ίδιο.
SELECT id, title, created_at
FROM articles
ORDER BY created_at DESC
OFFSET 100000 ROWS
FETCH NEXT 20 ROWS ONLY;
Συνέπεια δεδομένων — το κύριο μειονέκτημα του Offset Pagination κατά την εργασία με δυναμικά σύνολα. Εάν μεταξύ δύο αιτημάτων χρήστη προστεθεί μια νέα εγγραφή στην αρχή του πίνακα, όλες οι υπάρχουσες εγγραφές μετατοπίζονται. Ο χρήστης βλέπει διπλότυπα ή παραλείψεις.
Φανταστείτε έναν πίνακα με 100 εγγραφές με limit = 20. Στη σελίδα 1, ο χρήστης βλέπει τις εγγραφές 1-20. Ο διαχειριστής προσθέτει 5 νέες εγγραφές. Στη σελίδα 2, ο χρήστης βλέπει τις εγγραφές 26-45 αντί για τις αναμενόμενες 21-40 — οι εγγραφές 21-25 παραλείπονται και οι εγγραφές 21-25 από το προηγούμενο σύνολο διπλοτυπούνται στη σελίδα 1.
Cursor-based pagination — εναλλακτική λύση για το Offset Pagination, που χρησιμοποιεί έναν δείκτη στην τελευταία εγγραφή της τρέχουσας σελίδας. Αντί για αριθμητική μετατόπιση, ο πελάτης μεταδίδει το αναγνωριστικό της τελευταίας ληφθείσας εγγραφής και ο διακομιστής επιστρέφει τις επόμενες N εγγραφές μετά από αυτήν.
Η προσέγγιση cursor-based λύνει το πρόβλημα συνέπειας: η θέση του δρομέα δεν αλλάζει κατά την εισαγωγή ή διαγραφή, επειδή ο δρομέας αναφέρεται σε μια συγκεκριμένη εγγραφή, όχι σε μια θέση. Ωστόσο, είναι πιο περίπλοκη στην υλοποίηση — απαιτεί ένα μοναδικό ταξινομήσιμο πεδίο (συνήθως ID ή timestamp).
| Παράμετρος | Offset Pagination | Cursor-based Pagination |
|---|---|---|
| Απλότητα | Υψηλή — δύο αριθμητικές παράμετροι | Μέτρια — απαιτεί κωδικοποίηση δρομέα |
| Συνέπεια | Χαμηλή — διπλότυπα κατά την εισαγωγή | Υψηλή — ο δρομέας δεν εξαρτάται από αλλαγές |
| Απόδοση | Μειώνεται με την αύξηση του offset | Σταθερή σε任何 όγκο |
| Άλμα σε σελίδα | Ναι — μπορείτε να πάτε σε οποιαδήποτε σελίδα | Όχι — μόνο διαδοχική πλοήγηση |
| Κατάλληλο για | Πίνακες <10K εγγραφών, UI με αριθμούς σελίδων | Feed, ατελείωτο scroll, μεγάλα σύνολα |
Η επιλογή μεταξύ προσεγγίσεων εξαρτάται από τις απαιτήσεις διεπαφής χρήστη. Εάν χρειάζεται πλοήγηση με αριθμούς σελίδων και άμεσο άλμα — το Offset Pagination είναι απλούστερο. Για ατελείωτο scroll ή feed ειδήσεων, οι δρομείς είναι προτιμότεροι.
Keyset pagination — μια παραλλαγή της προσέγγισης cursor-based, όπου το φιλτράρισμα γίνεται με μοναδικό κλειδί χρησιμοποιώντας WHERE αντί για OFFSET. Το ερώτημα SQL χρησιμοποιεί τη συνθήκη WHERE id > lastId, που επιτρέπει στη βάση δεδομένων να χρησιμοποιεί ευρετήριο χωρίς σάρωση των απορριφθέντων γραμμών.
Σύμφωνα με το PostgreSQL Wiki, η keyset pagination εκτελείται 100-1000 φορές ταχύτερα από ένα ερώτημα offset σε μεγάλες μετατοπίσεις, επειδή η σάρωση ευρετηρίου αντικαθιστά την πλήρη περιήγηση του πίνακα. Μειονέκτημα — αδυναμία άλματος σε αυθαίρετη σελίδα χωρίς διαδοχική διέλευση.
Offset Pagination είναι βέλτιστο για μικρά και μεσαία σύνολα δεδομένων (έως 10000 εγγραφές), όπου ο χρήστης χρειάζεται μια διεπαφή με αριθμούς σελίδων. Τυπικά σενάρια — πίνακες διαχείρισης, λίστες παραγγελιών, κατάλογοι με φιλτράρισμα και σελιδοποίηση ανά σελίδα.
Για εφαρμογές κινητών, η σελιδοποίηση με offset είναι κατάλληλη κατά τη φόρτωση ιστορικών δεδομένων, όπου η εισαγωγή νέων εγγραφών είναι σπάνια ή αδύνατη — για παράδειγμα, ιστορικό παραγγελιών χρήστη, λίστα ολοκληρωμένων εργασιών, αρχείο συναλλαγών. Σε αυτά τα σενάρια, το πρόβλημα συνέπειας δεν εμφανίζεται.
Δεν συνιστάται η χρήση του Offset Pagination για feed κοινωνικών δικτύων, λίστες σχολίων, συνομιλίες και άλλα δυναμικά σύνολα με συχνές εισαγωγές. Σε αυτές τις περιπτώσεις, οι παραλείψεις και τα διπλότυπα εγγραφών υποβαθμίζουν την εμπειρία χρήστη και απαιτούν πρόσθετη λογική αφαιρετικοποίησης στον πελάτη.
Υβριδική σελιδοποίηση συνδυάζει offset και cursor: το πρώτο αίτημα χρησιμοποιεί offset για την εμφάνιση της αρχικής σελίδας και τα επόμενα χρησιμοποιούν cursor για τη φόρτωση ατελείωτου scroll. Αυτή η προσέγγιση εφαρμόζεται στο Instagram και το Twitter, όπου η πρώτη σελίδα φορτώνεται μέσω cursor, αλλά το offset χρησιμοποιείται για τον υπολογισμό της θέσης κατά την επιστροφή στην προηγούμενη προβολή.
Η υλοποίηση της υβριδικής προσέγγισης απαιτεί αποθήκευση της εικονικής θέσης του χρήστη στον πελάτη και συντονισμό δύο μηχανισμών σελιδοποίησης στον διακομιστή. Σύμφωνα με το blog Instagram Engineering, η ομάδα τους χρησιμοποιεί cursor-based σελιδοποίηση με ένα επιπλέον πεδίο startCursor, που αντικαθιστά το offset για αρχική φόρτωση.
Εφαρμογές κινητών χρησιμοποιούν το Offset Pagination σε συνδυασμό με Retrofit/OkHttp σε Android και URLSession/Combine σε iOS. Το τυπικό μοτίβο — φόρτωση της επόμενης σελίδας κατά την κύλιση στο τέλος της λίστας μέσω RecyclerView.OnScrollListener ή UICollectionView prefetching.
Η υλοποίηση της σελιδοποίησης με offset σε κινητό πελάτη περιλαμβάνει τρία στοιχεία: διαχειριστή σελιδοποίησης (αποθηκεύει το τρέχον offset και το hasMore), προσαρμογέα λίστας (εμφανίζει στοιχεία και δείκτη φόρτωσης) και αποθετήριο (εκτελεί αιτήματα και χειρίζεται σφάλματα). Το Android Jetpack προσφέρει τη βιβλιοθήκη Paging 3, η οποία υποστηρίζει τόσο offset όσο και cursor-based σελιδοποίηση από το κουτί.
Paging 3 — βιβλιοθήκη Android Jetpack για φόρτωση δεδομένων ανά σελίδα. Ενσωματώνει τη λογική σελιδοποίησης, συμπεριλαμβανομένης της παρακολούθησης offset, διαχείρισης κατάστασης φόρτωσης και αυτόματης φόρτωσης κατά την κύλιση. Το PagingSource ορίζει κλειδιά για την επόμενη και προηγούμενη σελίδα.
class OffsetPagingSource(
private val api: ApiService,
private val limit: Int = 20
) : PagingSource<Int, Item>() {
override suspend fun load(
params: LoadParams<Int>
): LoadResult<Int, Item> {
val offset = params.key ?: 0
return try {
val response = api.getItems(offset, limit)
LoadResult.Page(
data = response.items,
prevKey = null,
nextKey = if (response.hasMore) offset + limit else null
)
} catch (e: Exception) {
LoadResult.Error(e)
}
}
}
PagingSource ορίζει τα κλειδιά prevKey και nextKey για πλοήγηση σελίδων. Στη σελιδοποίηση με offset, το prevKey είναι πάντα null (δεν μπορείτε να πάτε στην προηγούμενη σελίδα χωρίς αποθήκευση ιστορικού) και το nextKey αυξάνεται κατά limit σε κάθε φόρτωση, έως ότου ο διακομιστής επιστρέψει hasMore = false. Αυτό είναι ένα απλό και προβλέψιμο μοντέλο για κινητές λίστες.
Πρώτο λάθος — βασισμός στη σειρά των εγγραφών χωρίς ταξινόμηση. Το Offset Pagination απαιτεί σταθερή ταξινόμηση ORDER BY σε ένα μοναδικό πεδίο. Χωρίς αυτήν, το DBMS μπορεί να επιστρέφει εγγραφές σε αυθαίρετη σειρά, οδηγώντας σε τυχαία διπλότυπα και παραλείψεις μεταξύ σελίδων.
Δεύτερο λάθος — χρήση του offset για υπολογισμό του αριθμού σελίδας στη διεπαφή. Ο τύπος page = offset / limit + 1 λειτουργεί μόνο υπό την προϋπόθεση ότι καμία εγγραφή δεν έχει διαγραφεί ή προστεθεί μεταξύ φορτώσεων. Σε δυναμικά δεδομένα, ο αριθμός σελίδας γίνεται ανακριβής και ο χρήστης βλέπει εσφαλμένες πληροφορίες.
Τρίτο λάθος — αγνόηση χρονικών ορίων ερωτημάτων με μεγάλο offset. Σε offset άνω των 100000, το ερώτημα μπορεί να διαρκέσει δεκάδες δευτερόλεπτα, μπλοκάροντας τη διεπαφή και καταναλώνοντας πόρους διακομιστή. Συνιστάται ο ορισμός μέγιστης τιμής offset σε επίπεδο API (π.χ. 10000) και η χρήση cursor-based σελιδοποίησης για μεγάλους όγκους.
Τέταρτο λάθος — μη προσθήκη total count στην απόκριση. Χωρίς τον συνολικό αριθμό εγγραφών, ο πελάτης δεν μπορεί να εμφανίσει τον αριθμό σελίδων και να υλοποιήσει σελιδοποίηση με αριθμούς. Ωστόσο, ο υπολογισμός COUNT(*) σε μεγάλους πίνακες είναι επίσης δαπανηρός — για σύνολα άνω των 100000 εγγραφών, χρησιμοποιήστε κατά προσέγγιση εκτιμήσεις ή περιορίστε τη μέγιστη τιμή total.
Συχνές Ερωτήσεις
Το offset χρησιμοποιεί αριθμητική μετατόπιση (offset) για παράλειψη εγγραφών, ενώ ο δρομέας χρησιμοποιεί δείκτη στην τελευταία εγγραφή της προηγούμενης σελίδας. Το offset είναι απλούστερο στην υλοποίηση, αλλά υποφέρει από διπλότυπα κατά την εισαγωγή και απώλεια απόδοσης σε μεγάλες μετατοπίσεις. Ο δρομέας είναι σταθερός σε οποιεσδήποτε αλλαγές δεδομένων.
Η σελιδοποίηση με offset είναι αναποτελεσματική σε offset άνω των 10000 εγγραφών λόγω πλήρους σάρωσης του πίνακα. Επίσης, είναι ακατάλληλη για δυναμικά σύνολα (feed, συνομιλίες) όπου νέες εγγραφές εμφανίζονται μεταξύ αιτημάτων — ο χρήστης βλέπει παραλείψεις και διπλότυπα κατά την πλοήγηση.
Το βέλτιστο limit εξαρτάται από το μέγεθος εγγραφής και την ταχύτητα δικτύου — από 10 έως 50 στοιχεία ανά σελίδα. Για λίστες με μεγάλες εικόνες, χρησιμοποιήστε limit = 10-15, για δεδομένα κειμένου — 20-50. Πάντα να επιτρέπετε στον πελάτη να καθορίζει το δικό του limit με ένα μέγιστο όριο στον διακομιστή (συνήθως 100).
Για την αντιμετώπιση διπλοτύπων, χρησιμοποιήστε αφαιρετικοποίηση στον πελάτη βάσει μοναδικού ID, εφαρμόστε σταθερή ταξινόμηση σε μοναδικό πεδίο ή μεταβείτε σε cursor-based σελιδοποίηση. Το Android Paging 3 υποστηρίζει κλειδί για αυτόματη αφαιρετικοποίηση στοιχείων λίστας.
Ναι, το GraphQL υποστηρίζει σελιδοποίηση με offset μέσω των ορισμάτων offset και limit στο ερώτημα, αν και η προδιαγραφή Relay συνιστά την προσέγγιση cursor-based. Οι βιβλιοθήκες Apollo GraphQL και Relay προσφέρουν ενσωματωμένη υποστήριξη για σελιδοποίηση με offset με αυτόματη διαχείριση κατάστασης σελίδων.
Σύνοψη
Θα αναπτύξουμε μια εφαρμογή για κινητά έτοιμη για χρήση
Η IT Sectr δημιουργεί εφαρμογές iOS και Android για νεοφυείς επιχειρήσεις και επιχειρήσεις από το 2017. Θα σας συμβουλεύσουμε και θα προτείνουμε την καλύτερη λύση.
Διαβάστε επίσης