Offset Pagination — paginacja z przesunięciem — metoda stronicowego ładowania danych przez HTTP API. Klient przekazuje parametry offset (przesunięcie od początku) i limit (rozmiar strony), a serwer zwraca rekordy od pozycji offset. Według REST API Tutorial, to podejście jest szeroko stosowane w serwisach RESTful ze względu na prostotę implementacji. Jednak przy dużych wolumenach danych offset-paginacja traci wydajność z powodu pełnego skanowania tabeli do wymaganej pozycji.
Główne punkty
Offset Pagination — to metoda stronicowego podziału danych, w której żądanie klienta zawiera dwa parametry: offset (ile rekordów pominąć) i limit (ile rekordów zwrócić). Serwer wykonuje zapytanie SQL z OFFSET i LIMIT, pomija określoną liczbę wierszy i zwraca zestaw wyników o stałym rozmiarze.
Metoda pojawiła się w relacyjnych bazach danych jako najprostszy sposób organizacji nawigacji po stronach i została przeniesiona do HTTP API wraz z rozwojem architektury REST. Offset Pagination nie wymaga przechowywania stanu na serwerze — każde żądanie jest niezależne i zawiera wszystkie informacje potrzebne do pobrania danych.
Według badania projektowania API od Postman (2025), offset-paginacja jest używana w 72% publicznych REST API, co czyni ją dominującym standardem pomimo znanych ograniczeń wydajności przy dużych wolumenach danych.
Typowe żądanie REST z Offset Pagination zawiera parametry zapytania offset i limit. Odpowiedź zawiera listę rekordów danej strony oraz metadane do budowy interfejsu nawigacji.
Parametr limit ogranicza liczbę zwracanych rekordów i chroni serwer oraz klienta przed nadmiernym obciążeniem. Typowe wartości limit — od 10 do 50 rekordów na stronę w zależności od złożoności danych.
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 przekształca się w zapytanie SQL z konstrukcjami OFFSET i FETCH NEXT (lub LIMIT w MySQL/SQLite). Serwer bazy danych skanuje tabelę, pomija liczbę wierszy równą offset i zwraca kolejne limit wierszy. Im większy offset, tym dłużej trwa zapytanie.
Problem wydajności związany jest z tym, że baza danych nie może przejść bezpośrednio do pozycji offset — musi odczytać i odrzucić wszystkie poprzednie wiersze. Przy offset = 100000 i limit = 20 SZBD odczyta 100020 wierszy i zwróci tylko 20.
SQL — język, w którym serwer wykonuje offset-paginację. W PostgreSQL i MySQL używane jest LIMIT, w SQL Server i Oracle — OFFSET...FETCH. Różne SZBD optymalizują to zapytanie na swój sposób, ale fundamentalny problem skanowania pozostaje ten sam.
SELECT id, title, created_at
FROM articles
ORDER BY created_at DESC
OFFSET 100000 ROWS
FETCH NEXT 20 ROWS ONLY;
Spójność danych — główna wada Offset Pagination przy pracy z dynamicznymi zestawami. Jeśli między dwoma żądaniami użytkownika do początku tabeli zostanie dodany nowy rekord, wszystkie istniejące rekordy przesuwają się. Użytkownik widzi duplikaty lub pominięcia.
Wyobraźmy sobie tabelę ze 100 rekordami z limit = 20. Na stronie 1 użytkownik widzi rekordy 1-20. Administrator dodaje 5 nowych rekordów. Na stronie 2 użytkownik widzi rekordy 26-45 zamiast oczekiwanych 21-40 — rekordy 21-25 pominięte, a rekordy 21-25 z poprzedniego zestawu — zduplikowane na stronie 1.
Cursor-based pagination — alternatywa dla Offset Pagination, wykorzystująca wskaźnik na ostatni rekord bieżącej strony. Zamiast numerycznego przesunięcia klient przekazuje identyfikator ostatniego otrzymanego rekordu, a serwer zwraca kolejne N rekordów po nim.
Podejście cursor-based rozwiązuje problem spójności: położenie kursora nie zmienia się przy wstawianiach lub usuwaniach, ponieważ kursor odnosi się do konkretnego rekordu, a nie do pozycji. Jest jednak trudniejsze w implementacji — wymaga unikalnego pola sortowalnego (zazwyczaj ID lub timestamp).
| Parametr | Offset Pagination | Cursor-based Pagination |
|---|---|---|
| Prostota | Wysoka — dwa parametry numeryczne | Umiarkowana — wymaga kodowania kursora |
| Spójność | Niska — duplikaty przy wstawianiach | Wysoka — kursor nie zależy od zmian |
| Wydajność | Spada wraz ze wzrostem offset | Stabilna na każdym wolumenie |
| Skok do strony | Tak — można przejść na dowolną stronę | Nie — tylko nawigacja sekwencyjna |
| Odpowiednie dla | Tabel <10K rekordów, UI z numerami stron | Feedów, nieskończonego scrolla, dużych zbiorów |
Wybór między podejściami zależy od wymagań dotyczących interfejsu użytkownika. Jeśli potrzebna jest nawigacja z numerami stron i bezpośrednim przejściem — Offset Pagination jest prostszy. Do nieskończonego scrolla lub feedów wiadomości preferowane są kursory.
Keyset pagination — odmiana podejścia cursor-based, w której filtrowanie odbywa się po unikalnym kluczu za pomocą WHERE zamiast OFFSET. Zapytanie SQL używa warunku WHERE id > lastId, co pozwala bazie danych użyć indeksu bez skanowania odrzuconych wierszy.
Według PostgreSQL Wiki, keyset pagination działa 100-1000 razy szybciej niż zapytanie offsetowe przy dużych przesunięciach, ponieważ skanowanie indeksowe zastępuje pełny przegląd tabeli. Wadą jest brak możliwości skoku na dowolną stronę bez sekwencyjnego przechodzenia.
Offset Pagination jest optymalna dla małych i średnich zbiorów danych (do 10000 rekordów), gdzie użytkownik potrzebuje interfejsu z numerami stron. Typowe scenariusze — panele administracyjne, listy zamówień, katalogi z filtrowaniem i paginacją po stronach.
Dla aplikacji mobilnych offset-paginacja sprawdza się przy ładowaniu danych historycznych, gdzie wstawianie nowych rekordów jest rzadkie lub niemożliwe — na przykład historia zamówień użytkownika, lista zakończonych zadań, archiwum transakcji. W tych scenariuszach problem spójności nie występuje.
Nie zaleca się używania Offset Pagination dla feedów mediów społecznościowych, list komentarzy, czatów i innych dynamicznych zbiorów z częstymi wstawieniami. W tych przypadkach pominięcia i duplikaty rekordów pogarszają doświadczenie użytkownika i wymagają dodatkowej logiki deduplikacji po stronie klienta.
Paginacja hybrydowa łączy offset i cursor: pierwsze żądanie używa offset do pokazania początkowej strony, a kolejne — cursor do ładowania nieskończonego scrolla. To podejście jest zaimplementowane w Instagramie i Twitterze, gdzie pierwsza strona ładowana jest przez cursor, ale offset jest używany do obliczania pozycji przy powrocie do poprzedniego widoku.
Implementacja podejścia hybrydowego wymaga przechowywania wirtualnej pozycji użytkownika po stronie klienta i koordynacji dwóch mechanizmów paginacji na serwerze. Według bloga Instagram Engineering, ich zespół używa cursor-based paginacji z dodatkowym polem startCursor, które zastępuje offset dla początkowego ładowania.
Aplikacje mobilne używają Offset Pagination w parze z Retrofit/OkHttp na Androidzie i URLSession/Combine na iOS. Typowy wzorzec — ładowanie następnej strony przy przewijanięciu do końca listy przez RecyclerView.OnScrollListener lub UICollectionView prefetching.
Implementacja offset-paginacji na kliencie mobilnym obejmuje trzy komponenty: menedżer paginacji (przechowuje bieżący offset i hasMore), adapter listy (wyświetla elementy i wskaźnik ładowania) oraz repozytorium (wykonuje zapytania i obsługuje błędy). Android Jetpack oferuje Paging 3 Library, która obsługuje zarówno offset, jak i cursor-based paginację od razu po wyjęciu z pudełka.
Paging 3 — biblioteka Android Jetpack do stronicowego ładowania danych. Inkapsuluje logikę paginacji, w tym śledzenie offset, zarządzanie stanem ładowania i automatyczne ładowanie przy przewijaniu. PagingSource określa klucze dla następnej i poprzedniej strony.
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 określa klucze prevKey i nextKey dla nawigacji stronicowej. Przy offset-paginacji prevKey zawsze ma wartość null (nie można przejść do poprzedniej strony bez zapisywania historii), a nextKey zwiększa się o limit przy każdym ładowaniu, aż serwer nie zwróci hasMore = false. To prosty i przewidywalny model dla list mobilnych.
Pierwszy błąd — poleganie na kolejności rekordów bez sortowania. Offset Pagination wymaga stabilnego sortowania ORDER BY po unikalnym polu. Bez niego SZBD może zwracać rekordy w dowolnej kolejności, co prowadzi do przypadkowych duplikatów i pominięć między stronami.
Drugi błąd — używanie offset do obliczania numeru strony w interfejsie. Formuła page = offset / limit + 1 działa tylko pod warunkiem, że żaden rekord nie został usunięty ani dodany między ładowaniami. Przy dynamicznych danych numer strony staje się niedokładny, a użytkownik widzi nieprawidłowe informacje.
Trzeci błąd — ignorowanie limitów czasu zapytań z dużym offset. Przy offset powyżej 100000 zapytanie może wykonywać się dziesiątki sekund, blokując interfejs i zużywając zasoby serwera. Zaleca się ustawienie maksymalnej wartości offset na poziomie API (na przykład 10000) i używanie cursor-based paginacji dla dużych wolumenów.
Czwarty błąd — nie dodawanie total count do odpowiedzi. Bez całkowitej liczby rekordów klient nie może wyświetlić liczby stron i zaimplementować paginacji z numerami. Jednak obliczanie COUNT(*) na dużych tabelach również jest kosztowne — dla zbiorów powyżej 100000 rekordów używaj przybliżonych oszacowań lub ogranicz maksymalną wartość total.
Często zadawane pytania
Offset używa numerycznego przesunięcia (offset) do pomijania rekordów, a cursor — wskaźnika na ostatni rekord poprzedniej strony. Offset jest prostszy w implementacji, ale cierpi na duplikaty przy wstawianiach i utratę wydajności przy dużych przesunięciach. Cursor jest stabilny przy wszelkich zmianach danych.
Offset paginacja jest nieefektywna przy offset powyżej 10000 rekordów z powodu pełnego skanowania tabeli. Jest również nieodpowiednia dla dynamicznych zbiorów (feedy, czaty), gdzie nowe rekordy pojawiają się między żądaniami — użytkownik widzi pominięcia i duplikaty rekordów podczas nawigacji.
Optymalny limit zależy od rozmiaru rekordu i prędkości sieci — od 10 do 50 elementów na stronę. Dla list z dużymi obrazami używaj limit = 10-15, dla danych tekstowych — 20-50. Zawsze zezwalaj klientowi na określenie własnego limitu z ograniczeniem maksimum na serwerze (zazwyczaj 100).
Aby radzić sobie z duplikatami, używaj deduplikacji po stronie klienta po unikalnym ID, stosuj stable sortowanie po unikalnym polu lub przejdź na cursor-based paginację. Android Paging 3 obsługuje key do automatycznej deduplikacji elementów listy.
Tak, GraphQL obsługuje offset-paginację przez argumenty offset i limit w zapytaniu, chociaż specyfikacja Relay zaleca podejście cursor-based. Biblioteki Apollo GraphQL i Relay oferują wbudowaną obsługę offset-paginacji z automatycznym zarządzaniem stanem stron.
Podsumowanie
Opracujemy aplikację mobilną pod klucz
IT Sectr tworzy aplikacje na iOS i Androida dla startupów i firm od 2017 roku. Doradzimy Ci i zaproponujemy najlepsze rozwiązanie.
Przeczytaj również