01
Por que a página 500 custa quinhentas páginas
OFFSET não é um salto. O banco não tem como saber onde começa a linha de número 24.951 de um resultado ordenado sem percorrer as 24.950 anteriores, porque a posição de uma linha no resultado depende do filtro, da ordenação e do estado da tabela naquele instante. Mesmo com o índice perfeito para a consulta, o executor lê as entradas do índice em ordem, busca cada linha correspondente na tabela, conta e descarta até atingir o deslocamento pedido, e só então começa a devolver o que o cliente queria. O trabalho da página N é proporcional a N vezes o tamanho da página.
EXPLAIN (ANALYZE, BUFFERS)
SELECT id, criado_em, status, total
FROM pedidos
WHERE loja_id = 42
ORDER BY criado_em DESC, id DESC
LIMIT 50 OFFSET 24950;
Limit (cost=21418.52..21461.44 rows=50 width=32)
(actual time=1874.221..1874.402 rows=50 loops=1)
Buffers: shared hit=3121 read=21877
-> Index Scan using pedidos_loja_criado_id_idx on pedidos
(cost=0.56..412936.11 rows=481120 width=32)
(actual time=0.041..1872.930 rows=25000 loops=1)
Index Cond: (loja_id = 42)
Buffers: shared hit=3121 read=21877
Planning Time: 0.162 ms
Execution Time: 1874.455 msO plano mostra o problema em duas linhas. O nó de índice produziu vinte e cinco mil linhas para que o Limit devolvesse cinquenta, e para isso tocou quase vinte e cinco mil páginas de dados, a maior parte lida do disco porque linhas antigas de uma loja raramente estão em cache. O índice está sendo usado, o plano é o melhor possível para essa forma de consulta, e mesmo assim a execução levou quase dois segundos. Nenhum ajuste de índice resolve isso, porque o custo não vem de um plano ruim: vem de pedir ao banco uma posição que ele só consegue encontrar contando.
| Página (50 itens) | Linhas lidas com OFFSET | Tempo com OFFSET | Tempo com paginação por chave |
|---|---|---|---|
| 1 | 50 | 0,3 ms | 0,2 ms |
| 50 | 2.500 | 9 ms | 0,2 ms |
| 500 | 25.000 | 1,9 s (páginas frias) | 0,2 ms |
| 4.000 | 200.000 | 14 s (páginas frias) | 0,3 ms |
É por isso que o problema demora a aparecer. Pessoas navegando pela interface quase nunca passam da página cinco, e em desenvolvimento a tabela tem poucos milhares de linhas. O custo só se manifesta quando alguém automatiza a navegação: um script de exportação, uma integração que sincroniza o histórico inteiro, um robô de busca seguindo o link de próxima página, ou um usuário que descobriu que pode trocar o número na URL. E como o custo total de percorrer N páginas é quadrático, uma exportação completa que parecia inofensiva lê, no total, dezenas de bilhões de linhas.