Offset Pagination — paginação com deslocamento — método de carregamento paginado de dados via HTTP API. O cliente envia os parâmetros offset (deslocamento desde o início) e limit (tamanho da página), e o servidor retorna os registros a partir da posição offset. De acordo com o REST API Tutorial, essa abordagem é amplamente usada em serviços RESTful devido à simplicidade de implementação. No entanto, em grandes volumes de dados, a paginação offset perde desempenho devido à varredura completa da tabela até a posição desejada.
Principais pontos
Offset Pagination é um método de paginação de dados onde a requisição do cliente contém dois parâmetros: offset (quantos registros pular) e limit (quantos registros retornar). O servidor executa uma consulta SQL com OFFSET e LIMIT, pula a quantidade especificada de linhas e retorna um conjunto de resultados de tamanho fixo.
O método originou-se em bancos de dados relacionais como a forma mais simples de organizar a navegação por páginas e foi transferido para APIs HTTP junto com o desenvolvimento da arquitetura REST. Offset Pagination não requer armazenamento de estado no servidor — cada requisição é independente e contém todas as informações necessárias para a consulta.
De acordo com o relatório de design de API da Postman (2025), a paginação offset é usada em 72% das APIs REST públicas, tornando-a o padrão dominante apesar das limitações conhecidas de desempenho em grandes conjuntos de dados.
Uma requisição REST típica com Offset Pagination inclui os parâmetros de consulta offset e limit. A resposta contém a lista de registros da página solicitada e metadados para construir a interface de navegação.
O parâmetro limit restringe a quantidade de registros retornados e protege o servidor e o cliente de cargas excessivas. Os valores típicos de limit variam de 10 a 50 registros por página dependendo da complexidade dos dados.
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 se traduz em uma consulta SQL com as construções OFFSET e FETCH NEXT (ou LIMIT no MySQL/SQLite). O servidor de banco de dados varre a tabela, pula a quantidade de linhas igual ao offset e retorna as próximas limit linhas. Quanto maior o offset, mais lenta a consulta.
O problema de desempenho decorre do fato de que o banco de dados não pode pular diretamente para a posição do offset — ele deve ler e descartar todas as linhas anteriores. Com offset = 100000 e limit = 20, o DBMS lê 100.020 linhas e retorna apenas 20.
SQL — a linguagem na qual o servidor executa a paginação offset. PostgreSQL e MySQL usam LIMIT, enquanto SQL Server e Oracle usam OFFSET...FETCH. Diferentes DBMSs otimizam essa consulta de maneiras distintas, mas o problema fundamental de varredura permanece o mesmo.
SELECT id, title, created_at
FROM articles
ORDER BY created_at DESC
OFFSET 100000 ROWS
FETCH NEXT 20 ROWS ONLY;
A consistência de dados é a principal desvantagem do Offset Pagination ao trabalhar com conjuntos dinâmicos. Se um novo registro for adicionado ao início da tabela entre duas requisições do usuário, todos os registros existentes se deslocam. O usuário vê duplicatas ou lacunas.
Considere uma tabela de 100 registros com limit = 20. Na página 1, o usuário vê os registros 1-20. Um administrador adiciona 5 novos registros. Na página 2, o usuário vê os registros 26-45 em vez dos esperados 21-40 — os registros 21-25 são pulados, e os registros 21-25 do conjunto anterior são duplicados na página 1.
A paginação Cursor-based é uma alternativa ao Offset Pagination que usa um ponteiro para o último registro da página atual. Em vez de um deslocamento numérico, o cliente envia o identificador do último registro recebido, e o servidor retorna os próximos N registros após ele.
A abordagem cursor-based resolve o problema de consistência: a posição do cursor não muda com inserções ou exclusões porque o cursor se refere a um registro específico, não a uma posição. No entanto, é mais complexa de implementar — requer um campo único ordenável (geralmente ID ou timestamp).
| Parâmetro | Offset Pagination | Cursor-based Pagination |
|---|---|---|
| Simplicidade | Alta — dois parâmetros numéricos | Média — requer codificação do cursor |
| Consistência | Baixa — duplicatas em inserções | Alta — cursor não afetado por alterações |
| Desempenho | Degrada com offset grande | Estável em qualquer volume |
| Salto para página | Sim — pode navegar para qualquer página | Não — apenas navegação sequencial |
| Ideal para | Tabelas <10K registros, UI com números de página | Feeds, scroll infinito, grandes conjuntos |
A escolha entre abordagens depende dos requisitos de interface do usuário. Se for necessária navegação com números de página e salto direto — Offset Pagination é mais simples. Para scroll infinito ou feeds de notícias, cursores são preferíveis.
Keyset pagination é uma variação da abordagem cursor-based onde a filtragem é realizada em uma chave única usando WHERE em vez de OFFSET. A consulta SQL usa uma condição como WHERE id > lastId, permitindo que o banco de dados use um índice sem varrer linhas descartadas.
De acordo com a Wiki do PostgreSQL, keyset pagination é executada 100-1000 vezes mais rápido que consultas offset em grandes deslocamentos porque a varredura de índice substitui a varredura completa da tabela. A desvantagem é a impossibilidade de pular para uma página arbitrária sem percurso sequencial.
Offset Pagination é ideal para conjuntos de dados pequenos e médios (até 10.000 registros) onde o usuário precisa de uma interface com números de página. Os cenários típicos incluem painéis de administração, listas de pedidos e catálogos filtrados com paginação por páginas.
Para aplicativos móveis, a paginação offset é adequada ao carregar dados históricos onde novas inserções são raras ou impossíveis — por exemplo, histórico de pedidos do usuário, listas de tarefas concluídas ou arquivos de transações. Nesses cenários, o problema de consistência não surge.
Não recomendado para feeds de redes sociais, listas de comentários, chats e outros conjuntos dinâmicos com inserções frequentes. Nesses casos, lacunas e registros duplicados degradam a experiência do usuário e exigem lógica adicional de deduplicação no cliente.
A paginação híbrida combina offset e cursor: a primeira requisição usa offset para mostrar a página inicial, enquanto as requisições subsequentes usam cursor para carregamento de scroll infinito. Essa abordagem é usada no Instagram e Twitter, onde a primeira página é carregada via cursor, mas o offset é usado para calcular a posição ao retornar a uma visualização anterior.
Implementar uma abordagem híbrida requer armazenar a posição virtual do usuário no cliente e coordenar dois mecanismos de paginação no servidor. De acordo com o blog Instagram Engineering, sua equipe usa paginação cursor-based com um campo adicional startCursor que substitui o offset para o carregamento inicial.
Os aplicativos móveis usam Offset Pagination junto com Retrofit/OkHttp no Android e URLSession/Combine no iOS. O padrão típico é carregar a próxima página ao rolar até o final da lista através de RecyclerView.OnScrollListener ou UICollectionView prefetching.
A implementação de paginação offset em um cliente móvel inclui três componentes: um gerenciador de paginação (armazena o offset atual e hasMore), um adaptador de lista (exibe itens e indicador de carregamento) e um repositório (executa requisições e lida com erros). O Android Jetpack oferece a biblioteca Paging 3, que suporta tanto paginação offset quanto cursor-based nativamente.
Paging 3 é uma biblioteca Android Jetpack para carregamento paginado de dados. Ela encapsula a lógica de paginação, incluindo rastreamento de offset, gerenciamento de estado de carregamento e pré-carregamento automático ao rolar. PagingSource define as chaves para as próximas e anteriores páginas.
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 define as chaves prevKey e nextKey para navegação entre páginas. Com paginação offset, prevKey é sempre null (não é possível ir para a página anterior sem salvar histórico), enquanto nextKey aumenta em limit a cada carregamento até que o servidor retorne hasMore = false. Este é um modelo simples e previsível para listas móveis.
Primeiro erro — confiar na ordem dos registros sem ordenação. Offset Pagination requer uma ordenação ORDER BY estável em um campo único. Sem ela, o DBMS pode retornar registros em ordem arbitrária, causando duplicatas e lacunas aleatórias entre páginas.
Segundo erro — usar offset para calcular o número da página na interface. A fórmula page = offset / limit + 1 só funciona se nenhum registro foi excluído ou adicionado entre carregamentos. Com dados dinâmicos, o número da página se torna impreciso e o usuário vê informações incorretas.
Terceiro erro — ignorar os timeouts de consultas com offset grande. Com offset acima de 100.000, a consulta pode levar dezenas de segundos, bloqueando a interface e consumindo recursos do servidor. Recomenda-se definir um valor máximo de offset no nível da API (por exemplo, 10.000) e usar paginação cursor-based para grandes volumes.
Quarto erro — não incluir o total count na resposta. Sem o número total de registros, o cliente não pode exibir a contagem de páginas nem implementar paginação numerada. No entanto, COUNT(*) em tabelas grandes é caro — para conjuntos com mais de 100.000 registros, use estimativas aproximadas ou limite o valor máximo de total.
Perguntas frequentes
Offset usa um deslocamento numérico para pular registros, enquanto cursor usa um ponteiro para o último registro da página anterior. Offset é mais simples de implementar, mas sofre com duplicatas em inserções e perda de desempenho em grandes deslocamentos. Cursor é estável sob quaisquer alterações de dados.
A paginação offset é ineficiente com offsets acima de 10.000 registros devido à varredura completa da tabela. Também é inadequada para conjuntos dinâmicos (feeds, chats) onde novos registros aparecem entre requisições — o usuário vê lacunas e registros duplicados durante a navegação.
O limit ideal depende do tamanho do registro e da velocidade da rede — de 10 a 50 itens por página. Para listas com imagens grandes, use limit = 10-15; para dados textuais, 20-50. Sempre permita que o cliente especifique seu próprio limit com um limite máximo no servidor (geralmente 100).
Para lidar com duplicatas, use deduplicação no cliente por ID único, aplique ordenação estável em um campo único ou mude para paginação cursor-based. O Android Paging 3 suporta key para deduplicação automática de itens de lista.
Sim, GraphQL suporta paginação offset através dos argumentos offset e limit na consulta, embora a especificação Relay recomende a abordagem cursor-based. As bibliotecas Apollo GraphQL e Relay oferecem suporte integrado para paginação offset com gerenciamento automático de estado de páginas.
Resumo
Vamos desenvolver um aplicativo móvel chave na mão
A IT Sectr cria aplicativos para iOS e Android para startups e empresas desde 2017. Nós vamos aconselhá-lo e propor a melhor solução.
Leia também