Como Projetar a Paginação de API para Milhões de Registros?

Ashley Innocent

Ashley Innocent

13 março 2026

Como Projetar a Paginação de API para Milhões de Registros?

Apidog para empresas

Implantação local

SSO & RBAC

Conforme SOC 2

Explorar Apidog Enterprise

Resumo

Para grandes conjuntos de dados, use paginação baseada em cursor ou em chaves (keyset) em vez de paginação baseada em offset. A paginação por offset (?page=1&limit=20) tem desempenho ruim com milhões de registros e permite inconsistência de dados. A Modern PetstoreAPI implementa paginação baseada em cursor com tokens opacos e links HATEOAS para resultados eficientes e consistentes.

Introdução

Sua API retorna uma lista de pets. Você tem 10 milhões de pets no banco de dados. Um cliente solicita GET /pets?page=500000&limit=20. Seu banco de dados executa OFFSET 10000000 LIMIT 20. A consulta leva 30 segundos. Sua API expira.

Este é o problema da paginação por offset. Funciona bem para conjuntos de dados pequenos, mas falha em escala. O banco de dados precisa escanear milhões de linhas para atingir o offset, mesmo que você retorne apenas 20 resultados.

O antigo Swagger Petstore não aborda paginação. A Modern PetstoreAPI implementa paginação baseada em cursor que escala para milhões de registros com desempenho consistente.

💡
Se você está construindo ou testando APIs REST, o Apidog ajuda a testar o comportamento da paginação, validar formatos de resposta e garantir que sua API lide corretamente com grandes conjuntos de dados. Você pode simular cenários de paginação, testar casos extremos e verificar o desempenho.
button

Neste guia, você aprenderá por que a paginação por offset falha, como funciona a paginação baseada em cursor e como a Modern PetstoreAPI implementa paginação eficiente.

Por que a Paginação por Offset Falha em Escala

A paginação por offset é a abordagem mais comum, mas tem sérios problemas.

Como a Paginação por Offset Funciona

GET /pets?page=1&limit=20    → OFFSET 0 LIMIT 20
GET /pets?page=2&limit=20    → OFFSET 20 LIMIT 20
GET /pets?page=3&limit=20    → OFFSET 40 LIMIT 20

O banco de dados pula as linhas do offset e retorna as linhas do limit.

Problema 1: O Desempenho Degrada com o Número da Página

Página 1:

SELECT * FROM pets OFFSET 0 LIMIT 20;
-- Rápido: escaneia 20 linhas

Página 1000:

SELECT * FROM pets OFFSET 20000 LIMIT 20;
-- Lento: escaneia 20.020 linhas, retorna 20

Página 500.000:

SELECT * FROM pets OFFSET 10000000 LIMIT 20;
-- Muito lento: escaneia 10.000.020 linhas, retorna 20

O banco de dados deve escanear todas as linhas até o offset, mesmo que as descarte. O desempenho degrada linearmente com o número da página.

Problema 2: Resultados Inconsistentes

Enquanto um cliente navega pelos resultados, os dados mudam:

Requisição 1:

GET /pets?page=1&limit=2
Retorna: [Pet A, Pet B]

Alguém adiciona o Pet Z (ordena primeiro alfabeticamente)

Requisição 2:

GET /pets?page=2&limit=2
Retorna: [Pet B, Pet C]  ← Pet B aparece duas vezes!

O Pet B apareceu em ambas as páginas porque um novo pet foi inserido. Por outro lado, pets podem ser pulados se ocorrerem exclusões.

Problema 3: Paginação Profunda é Cara

Os usuários raramente vão além da página 10. Mas se sua API permitir ?page=1000000, você deve lidar com isso. Consultas de paginação profunda são caras e podem ser usadas para ataques de negação de serviço.

Quando a Paginação por Offset é Aceitável

A paginação por offset funciona bem para:

Para APIs públicas ou grandes conjuntos de dados, use paginação baseada em cursor.

Paginação Baseada em Cursor Explicada

A paginação baseada em cursor usa um token opaco para marcar a posição no conjunto de resultados.

Como Funciona

Requisição 1:

GET /pets?limit=20

Resposta 1:

{
  "data": [...],
  "pagination": {
    "nextCursor": "eyJpZCI6IjAxOWI0MTMyLTcwYWEtNzY0Zi1iMzE1LWUyODAzZDg4MmEyNCJ9",
    "hasMore": true
  }
}

Requisição 2:

GET /pets?cursor=eyJpZCI6IjAxOWI0MTMyLTcwYWEtNzY0Zi1iMzE1LWUyODAzZDg4MmEyNCJ9&limit=20

O cursor é um token opaco (geralmente codificado em base64) que codifica a posição. O cliente não o analisa — apenas o repassa.

Benefícios

1. Desempenho Consistente

O banco de dados usa um índice para encontrar a posição do cursor diretamente:

SELECT * FROM pets
WHERE id > '019b4132-70aa-764f-b315-e2803d882a24'
ORDER BY id
LIMIT 20;

Esta consulta é rápida independentemente da posição no conjunto de dados. Ela usa uma busca de índice, não um scan.

2. Resultados Consistentes

Os cursores são estáveis. Se os dados mudarem entre as requisições, você ainda obterá resultados consistentes. Novos registros não causam duplicatas ou pulos.

3. Sem Ataques de Paginação Profunda

Os clientes não podem pular para posições arbitrárias. Eles devem paginar sequencialmente, o que limita o abuso.

Formato do Cursor

Os cursores são tipicamente JSON codificados em base64:

// Cursor decodificado
{
  "id": "019b4132-70aa-764f-b315-e2803d882a24",
  "createdAt": "2026-03-13T10:30:00Z"
}

O cursor contém informações suficientes para retomar a paginação. Para a Modern PetstoreAPI, isso inclui o ID do recurso e o campo de ordenação.

Paginação por Chaves (Keyset) para Dados Ordenados

A paginação por chaves (keyset) é uma variante da paginação baseada em cursor para dados ordenados.

Como Funciona

Em vez de um cursor opaco, você usa o último valor da página anterior:

Requisição 1:

GET /pets?limit=20&sortBy=createdAt

Resposta 1:

{
  "data": [
    {"id": "...", "createdAt": "2026-03-13T10:00:00Z"},
    ...
    {"id": "...", "createdAt": "2026-03-13T10:30:00Z"}
  ]
}

Requisição 2:

GET /pets?limit=20&sortBy=createdAt&after=2026-03-13T10:30:00Z

O parâmetro after usa o último valor de createdAt da página anterior.

Consulta SQL

SELECT * FROM pets
WHERE created_at > '2026-03-13T10:30:00Z'
ORDER BY created_at
LIMIT 20;

Isso é eficiente porque usa um índice em created_at.

Quando Usar Paginação por Chaves (Keyset)

A Modern PetstoreAPI usa paginação baseada em cursor por padrão, mas suporta paginação por chaves para dados de séries temporais.

Como a Modern PetstoreAPI Implementa a Paginação

A Modern PetstoreAPI usa paginação baseada em cursor com links HATEOAS.

Formato da Requisição

GET /pets?limit=20
GET /pets?cursor={token}&limit=20

Parâmetros:

Formato da Resposta

{
  "data": [
    {
      "id": "019b4132-70aa-764f-b315-e2803d882a24",
      "name": "Fluffy",
      "species": "CAT"
    }
  ],
  "pagination": {
    "limit": 20,
    "hasMore": true,
    "nextCursor": "eyJpZCI6IjAxOWI0MTMyLTcwYWEtNzY0Zi1iMzE1LWUyODAzZDg4MmEyNCJ9"
  },
  "links": {
    "self": "https://petstoreapi.com/pets?limit=20",
    "next": "https://petstoreapi.com/pets?cursor=eyJpZCI6IjAxOWI0MTMyLTcwYWEtNzY0Zi1iMzE1LWUyODAzZDg4MmEyNCJ9&limit=20"
  }
}

Principais Recursos

1. Cursores Opacos

Os cursores são codificados em base64. Os clientes não os analisam.

2. Links HATEOAS

O objeto links fornece URLs prontas para uso. Os clientes não precisam construir URLs de paginação.

3. Flag hasMore

Indica se existem mais resultados. Os clientes sabem quando parar de paginar.

4. Validação de Limite

O limite máximo é 100. Impede que os clientes solicitem páginas enormes.

Consulte a documentação de paginação da Modern PetstoreAPI para detalhes completos.

Formato de Resposta da Paginação

A Modern PetstoreAPI encapsula respostas paginadas em uma estrutura consistente.

Wrapper de Coleção

{
  "data": [...],
  "pagination": {...},
  "links": {...}
}

Por que encapsular coleções?

  1. Extensibilidade - Pode adicionar metadados sem quebrar clientes
  2. Consistência - Todos os endpoints paginados usam o mesmo formato
  3. HATEOAS - Links guiam os clientes pela paginação

Metadados de Paginação

"pagination": {
  "limit": 20,
  "hasMore": true,
  "nextCursor": "...",
  "totalCount": 1000  // Opcional, caro para calcular
}

totalCount é opcional porque calculá-lo é caro para grandes conjuntos de dados. A maioria dos clientes não precisa dele.

Testando Paginação com Apidog

O Apidog ajuda a testar o comportamento da paginação de forma abrangente.

Cenários de Teste

1. Primeira Página

GET /pets?limit=20
Esperado: 20 resultados, hasMore=true, nextCursor presente

2. Páginas Subsequentes

GET /pets?cursor={token}&limit=20
Esperado: 20 resultados, hasMore=true/false, nextCursor presente/ausente

3. Última Página

GET /pets?cursor={lastToken}&limit=20
Esperado: < 20 resultados, hasMore=false, sem nextCursor

4. Resultados Vazios

GET /pets?status=NONEXISTENT&limit=20
Esperado: 0 resultados, hasMore=false, sem nextCursor

5. Validação de Limite

GET /pets?limit=1000
Esperado: 400 Bad Request (excede o limite máximo)

Configuração de Teste do Apidog

// Teste: Estrutura da paginação
pm.test("Response has pagination", () => {
  pm.expect(pm.response.json()).to.have.property('pagination');
  pm.expect(pm.response.json().pagination).to.have.property('hasMore');
});

// Teste: Links HATEOAS
pm.test("Response has links", () => {
  const links = pm.response.json().links;
  pm.expect(links).to.have.property('self');
  if (pm.response.json().pagination.hasMore) {
    pm.expect(links).to.have.property('next');
  }
});

Escolhendo a Estratégia de Paginação Correta

Diferentes estratégias se adequam a diferentes casos de uso.

Paginação por Offset

Use quando:

Não use quando:

Paginação Baseada em Cursor

Use quando:

Não use quando:

Paginação por Chaves (Keyset)

Use quando:

Não use quando:

Recomendação da Modern PetstoreAPI: Use paginação baseada em cursor para APIs públicas e grandes conjuntos de dados.

Conclusão

A paginação é crítica para APIs que retornam grandes conjuntos de dados. A paginação por offset é simples, mas não escala. A paginação baseada em cursor oferece desempenho consistente e resultados confiáveis para milhões de registros.

A Modern PetstoreAPI implementa paginação baseada em cursor com tokens opacos, links HATEOAS e metadados apropriados. Este design escala eficientemente e proporciona uma ótima experiência ao desenvolvedor.

Teste sua implementação de paginação com o Apidog para garantir que ela lide com casos extremos, valide limites e retorne resultados consistentes.

Principais pontos:

button

Perguntas Frequentes

Por que não apenas retornar todos os resultados sem paginação?

Retornar milhões de registros em uma única resposta causa problemas de memória, transferência de rede lenta e uma experiência de usuário ruim. A paginação é essencial para grandes conjuntos de dados.

Os clientes podem pular para uma página específica com paginação por cursor?

Não, a paginação por cursor exige acesso sequencial. Se o acesso aleatório for necessário, considere a paginação por offset para pequenos conjuntos de dados ou implemente busca/filtragem em vez disso.

Como eu lido com a paginação com filtragem?

Inclua parâmetros de filtro nas requisições de paginação: GET /pets?status=AVAILABLE&cursor={token}&limit=20. O cursor codifica tanto a posição quanto o estado do filtro.

Devo incluir a contagem total (total count) nas respostas de paginação?

Apenas se os clientes precisarem e seu conjunto de dados for pequeno. Calcular a contagem total é caro para grandes conjuntos de dados (requer uma consulta COUNT separada).

Como eu implemento a paginação por cursor em SQL?

Use uma cláusula WHERE com o valor do cursor: SELECT * FROM pets WHERE id > ? ORDER BY id LIMIT 20. Certifique-se de ter um índice na coluna de ordenação.

E se meus tokens de cursor se tornarem inválidos?

Retorne 400 Bad Request com uma mensagem de erro. Os cursores podem se tornar inválidos se os dados forem excluídos ou se o estado da paginação expirar.

Por quanto tempo os cursores devem permanecer válidos?

Os cursores da Modern PetstoreAPI são válidos indefinidamente, desde que o recurso referenciado exista. Algumas APIs expiram os cursores após 24 horas.

Posso usar paginação por cursor com múltiplos campos de ordenação?

Sim, mas o cursor deve codificar todos os campos de ordenação. Isso torna os cursores mais complexos. Considere usar uma única chave de ordenação composta em vez disso.

Pratique o design de API no Apidog

Descubra uma forma mais fácil de construir e usar APIs

Como Projetar a Paginação de API para Milhões de Registros?