Lição 0 · núcleo ~1 hora
Páginas, custo, e o que o planeador está a minimizar
Esta lição não fala de índices. Fala do que um índice serve para evitar — e sem isso, tudo o que vem a seguir vira regras decoradas que falham no primeiro caso invulgar.
1. Objetivo
No fim consegues, dada uma tabela e o seu número de páginas, calcular à mão o custo que o
Postgres atribui a uma varredura sequencial, e conferir que o teu número bate certo com o do
EXPLAIN. E consegues explicar porque é que o tempo de uma query se prevê melhor contando
páginas do que contando linhas.
2. Recuperar
Primeira lição: não há nada para recuperar ainda. Em vez disso, responde de memória — sem correr nada — e guarda o que respondeste. Vais confrontá-lo daqui a pouco.
- Uma tabela com 1 000 000 de linhas. Uma query que devolve 1 linha, sem índice.
Quantas linhas é que achas que a base de dados teve de examinar?
Confere depois de leres «O mecanismo»
Todas. Mas a pergunta está mal feita, e é esse o ponto da lição: ela não examinou «1 000 000 de linhas», leu 8348 páginas — e é esse o número que prevê o tempo.
- A mesma query demora 15 ms. Se a tabela tiver o dobro das linhas, quanto achas que demora?
Confere depois
Aproximadamente o dobro — mas porque o número de páginas duplica, não porque o número de linhas duplica. A distinção parece pedante até apareceres com uma tabela de 3 colunas e outra de 40.
3. O mecanismo
A base de dados não lê linhas
Um disco — mesmo um SSD — não entrega bytes avulsos: entrega blocos. O Postgres organiza tudo o que guarda em páginas de 8 kB (é o valor por omissão, fixado quando o servidor é compilado). Uma página contém tantas linhas quantas lá couberem, e a unidade de leitura é a página inteira.
A consequência é a ideia central de todo este curso:
O custo de uma query prevê-se pelo número de páginas lidas, não pelo número de linhas devolvidas. Ler uma linha de uma página que já ias ler é praticamente grátis. Ler uma linha de uma página que não ias ler custa uma página inteira.
Isto explica de antemão coisas que de outra forma parecem arbitrárias. Porque é que um índice que devolve 1% das linhas pode não acelerar nada? Porque se esse 1% estiver espalhado por todas as páginas, leste a tabela toda na mesma — e ainda por cima em saltos. Vais medir exatamente isso na lição 3.
A tabela é um monte, não uma lista ordenada
Uma tabela em Postgres chama-se heap, e a palavra é literal: é um monte. As linhas ficam onde houver espaço, na ordem em que foram inseridas ou atualizadas, sem ordem lógica nenhuma. Não há «a linha número 500 000»; há a linha que está na página 4172, posição 7.
Por isso, sem mais nada, encontrar uma linha só se faz de uma maneira: ler todas as páginas e
olhar para todas as linhas. É a varredura sequencial — sequential scan, Seq Scan
no EXPLAIN.
Seq Scan lê todas as páginas. É por isso que o tempo não depende de quantas linhas
a query devolve — depende de quantas páginas a tabela tem.O planeador não mede tempo. Calcula um custo.
Quando escreves uma query, o Postgres considera várias formas de a executar e escolhe uma. Para escolher, precisa de comparar — e não pode executar cada alternativa para ver qual é mais rápida. Por isso atribui a cada plano um custo: um número numa unidade inventada, em que 1 = ler uma página sequencialmente. Tudo o resto está calibrado em relação a isso.
| Parâmetro | Omissão | O que representa |
|---|---|---|
seq_page_cost | 1.0 | Ler uma página em sequência. É a régua: vale 1 por definição |
random_page_cost | 4.0 | Ler uma página «ao calhas». Custa 4× mais — número herdado da era dos discos com braço mecânico. Num SSD é exagerado, e mexer nele muda decisões (lição 3) |
cpu_tuple_cost | 0.01 | Processar uma linha lida |
cpu_operator_cost | 0.0025 | Avaliar um operador ou função — por exemplo, o = de um WHERE |
cpu_index_tuple_cost | 0.005 | Processar uma entrada de índice |
Um cost=20848 não são 20 848 de coisa nenhuma — nem ms, nem páginas. É um número que só serve
para comparar dois planos entre si. Quando alguém diz «o custo baixou de 20 000 para 500,
logo ficou 40× mais rápido», está a inventar: pode ter ficado 40× mais rápido, 3× mais rápido, ou mais lento
se as estimativas estiverem erradas. O tempo mede-se com EXPLAIN ANALYZE, que executa mesmo a query.
4. Exemplo trabalhado
Vamos reproduzir à mão, com papel e lápis, o número que o planeador anuncia. Se bater certo, deixas de ter de acreditar em mim — passas a poder verificar.
Prepara a base de treino (faz isto uma vez; usa-se no curso inteiro):
createdb treino_indices
psql -d treino_indices -f preparar.sql
O preparar.sql cria uma tabela encomendas com 1 000 000 de linhas.
Passo 1 — perguntar à tabela o seu tamanho
Porquê primeiro isto: o custo de um Seq Scan depende de duas coisas que o planeador lê do
catálogo — quantas páginas e quantas linhas. Se não souberes estes dois números, não podes prever nada.
SELECT relpages, reltuples::bigint
FROM pg_class WHERE relname = 'encomendas';
relpages | reltuples
----------+-----------
8348 | 1000000
8348 páginas × 8 kB ≈ 65 MB. Confere: 1 000 000 de linhas com ~68 bytes úteis cada, mais o cabeçalho de 23 bytes por linha e o espaço de gestão da página.
Passo 2 — escrever a conta antes de correr o EXPLAIN
Porquê por esta ordem: se correres primeiro, deixas de estar a prever e passas a estar a justificar.
A fórmula de um Seq Scan com um filtro simples tem três parcelas, e cada uma corresponde a
trabalho mesmo feito:
- Ler as páginas: 8348 ×
seq_page_cost(1.0) = 8348 - Processar cada linha: 1 000 000 ×
cpu_tuple_cost(0.01) = 10 000 - Avaliar o
=em cada linha: 1 000 000 ×cpu_operator_cost(0.0025) = 2500
Total previsto: 8348 + 10 000 + 2500 = 20 848.
Repara no que a conta diz: o trabalho de CPU (12 500) é maior do que o de leitura (8348). Não é engano — é o que acontece quando um filtro tem de ser avaliado em todas as linhas.
Passo 3 — conferir
EXPLAIN SELECT * FROM encomendas WHERE estado = 'entregue';
Seq Scan on encomendas (cost=0.00..20848.00 rows=969867 width=36)
Isto foi corrido em Postgres 17.11 com os valores por omissão. O planeador não é uma caixa preta com heurísticas mágicas: é aritmética sobre números que estão no catálogo e que tu podes consultar.
Passo 4 — ler os dois números do cost
cost=0.00..20848.00 são dois números, e confundi-los é o erro seguinte:
- 0.00 — custo de arranque: o trabalho antes de sair a primeira linha. Num
Seq Scané zero, porque a primeira linha sai logo da primeira página. - 20848.00 — custo total: o trabalho até sair a última linha.
A distinção importa mais do que parece. Um Sort tem de ler tudo antes de devolver o que quer que
seja, logo tem custo de arranque alto — e por isso perde contra um índice quando há um LIMIT 10,
mesmo que o custo total seja parecido.
5. Exemplo com lacunas
Agora a mesma conta para uma tabela diferente. Os passos 1 e 3 estão dados; completa os outros.
Completa
Passo 1 (dado). Uma tabela eventos com relpages = 25 000 e
reltuples = 4 000 000.
Passo 2 (completa). Custo previsto de
SELECT * FROM eventos WHERE tipo = 'clique':
- Ler as páginas: 25 000 × ______ = ______
- Processar as linhas: 4 000 000 × ______ = ______
- Avaliar o filtro: 4 000 000 × ______ = ______
- Total: ______
Passo 3 (dado). O EXPLAIN diz cost=0.00..75000.00.
Passo 4 (completa). Bate certo? Se não bate, qual é a explicação mais provável — e qual é a menos provável?
Ver os passos em falta
Passo 2: 25 000 × 1.0 = 25 000 · 4 000 000 × 0.01 = 40 000 · 4 000 000 × 0.0025 = 10 000. Total: 75 000.
Passo 4: bate certo, ao cêntimo.
E se não batesse? A explicação mais provável seria os parâmetros não estarem nos valores
por omissão nesse servidor (SHOW seq_page_cost; resolve em segundos), ou o filtro não
ser um operador simples — uma função como lower(tipo) = 'clique' custa mais do que um
=, e o planeador sabe disso.
A explicação menos provável é a tabela ter mudado de tamanho: relpages e
reltuples são números guardados no catálogo, não medidos na altura. Se estiverem
desatualizados — porque ninguém correu ANALYZE — o planeador faz a conta certa com os
números errados, e o EXPLAIN bate com a tua conta à mesma. Guarda esta ideia: é o assunto
inteiro da lição 3.
6. Erros comuns
| A ideia errada | Como se reconhece | O que é mesmo |
|---|---|---|
«O cost é uma estimativa do tempo» |
«Custo 500, deve demorar meio segundo»; ou dividir custos para obter um factor de aceleração | É uma unidade arbitrária, só comparável entre planos da mesma query, no mesmo servidor.
Tempo só com EXPLAIN ANALYZE, que executa mesmo |
| «Uma query lenta é lenta por devolver muitas linhas» | «Só devolve 3 linhas, não pode ser esta a lenta» | O que se lê e o que se devolve são independentes. Ler 8348 páginas para devolver 1 linha é o caso normal quando não há índice — e é a razão de existirem índices |
«Seq Scan no plano = problema» |
Criar um índice logo que se vê a palavra, sem olhar para mais nada | Numa tabela pequena, ou quando a query devolve grande parte das linhas, o Seq Scan é a
escolha certa e um índice só piora. Ver lição 3 |
| «As páginas são um detalhe de implementação que posso ignorar» | Raciocinar sempre em linhas; surpresa quando 1% das linhas custa o mesmo que 100% | A página é a unidade de I/O. Todo o resto do curso é consequência disso |
7. Praticar
E1 — a régua
Sem correr nada: se random_page_cost vale 4 e seq_page_cost vale 1, quantas
páginas lidas em sequência «valem» uma página lida ao calhas, na contabilidade do planeador? E o que é que
isso implica sobre um índice que obriga a saltar para 3000 páginas dispersas numa tabela de 8348?
Solução
Quatro. Uma leitura aleatória vale o mesmo que quatro sequenciais.
Implicação: 3000 páginas dispersas custam ~12 000 na moeda do planeador, enquanto ler as 8348 em sequência custa 8348. O índice perde, apesar de tocar em menos de metade das páginas. É esta aritmética — e não uma regra decorada sobre percentagens — que decide se um índice é usado. Vais medi-lo na lição 3.
E2 — código que corre
Na base de treino, prevê o custo de SELECT * FROM encomendas (sem WHERE) antes
de o correr. Depois confirma. Atenção: a conta tem uma parcela a menos do que a do exemplo
trabalhado — qual, e porquê?
psql -d treino_indices -c "EXPLAIN SELECT * FROM encomendas;"
Solução
Falta a parcela do cpu_operator_cost: não há WHERE, logo não há operador para
avaliar em cada linha.
Previsão: 8348 × 1.0 + 1 000 000 × 0.01 = 18 348.
Corrido em Postgres 17.11:
Seq Scan on encomendas (cost=0.00..18348.00 rows=1000000 width=36)
Repara em width=36: é a largura média estimada de cada linha devolvida, em bytes. Serve ao
planeador para estimar memória e custo de ordenações — e explica porque é que
SELECT * e SELECT id podem receber planos diferentes.
E3 — quebrar a previsão de propósito
Correr EXPLAIN SELECT * FROM encomendas WHERE estado = 'entregue'; com
seq_page_cost alterado para 2 só na tua sessão. Prevê o novo custo antes,
e explica porque é que a diferença é exatamente a que é.
psql -d treino_indices
SET seq_page_cost = 2;
EXPLAIN SELECT * FROM encomendas WHERE estado = 'entregue';
RESET seq_page_cost;
Solução
Só a primeira parcela muda: 8348 × 2.0 = 16 696, em vez de 8348. As parcelas de CPU não dependem deste parâmetro. Novo total: 16 696 + 10 000 + 2500 = 29 196. A diferença é exatamente 8348 — uma cópia extra do custo de leitura das páginas.
Corrido em Postgres 17.11:
Seq Scan on encomendas (cost=0.00..29196.00 rows=969867 width=36)
⭐ O que isto ensina é maior do que a conta: os limiares de decisão do planeador não são leis da natureza, são consequências de parâmetros configuráveis. Toda a gente que te disser «o Postgres deixa de usar o índice acima de X%» está a descrever a configuração da máquina dele. Na lição 3 vais medir o X da tua.
8. Quiz
Lê a explicação de todas as opções, incluindo as que não escolheste — é aí que está o que faltava perceber.
-
Uma tabela tem 8348 páginas. Uma query sem índice devolve 1 linha. Outra devolve 900 000 linhas. Qual lê mais páginas da tabela?
-
Esta é a intuição de quem raciocina em linhas. Sem índice, ambas fazem um Seq Scan e ambas leem as 8348 páginas — o que muda é quantas linhas passam o filtro depois de lidas, não quantas páginas se leem.
-
Certo. Sem índice não há forma de saber onde estão as linhas, logo leem-se todas as páginas nos dois casos. O número de linhas devolvidas afeta o custo de CPU e o volume enviado ao cliente, não o I/O da tabela.
-
Um ORDER BY acrescenta um passo de ordenação (custo de CPU e possivelmente disco temporário), mas não muda o número de páginas da tabela que é preciso ler para as encontrar. A leitura da tabela é a mesma.
-
-
O
EXPLAINmostracost=0.00..20848.00. O que é o primeiro número?-
Não existe tal conceito no EXPLAIN. O planeador não anuncia limites inferiores teóricos — anuncia o custo estimado do plano que escolheu, em duas fases.
-
Certo — o custo de arranque. É zero num Seq Scan porque a primeira linha sai logo da primeira página. Num Sort é alto, porque é preciso ler tudo antes de devolver o que quer que seja; e é exatamente por isso que um Sort perde para um índice quando há LIMIT.
-
Duas confusões numa: o cost nunca é tempo (é uma unidade arbitrária em que 1 = ler uma página em sequência), e o EXPLAIN sem ANALYZE não executa a query, portanto não tem tempos nenhuns para dar.
-
-
Porque é que
random_page_costvale 4 e não 1, por omissão?-
O custo de CPU está noutros parâmetros (cpu_tuple_cost, cpu_operator_cost). Os parâmetros *_page_cost representam I/O, não processamento.
-
Certo. E daqui sai uma consequência prática: num SSD o factor 4 é exagerado, e muita gente baixa-o para ~1.1. Isso desloca o limiar a partir do qual o planeador deixa de usar índices — é configuração, não lei da natureza.
-
Não há relação nenhuma entre este parâmetro e a altura de uma B-tree — que, já agora, no índice da base de treino é 3 níveis, não 4. A altura vê-se na lição 1.
-
-
O
EXPLAINdizrows=969867mas a query devolve 970 000 linhas. O que é que isto revela?-
Não deviam. O rows do EXPLAIN é uma estimativa calculada a partir de estatísticas amostradas por ANALYZE, não uma contagem. Exigir que bata ao certo é não perceber de onde vem o número.
-
Certo. O que interessa não é a diferença ser zero, é ser pequena. Uma estimativa errada por ordens de grandeza (rows=5 quando são 500 000) é que faz o planeador escolher mal — e é o assunto da lição 3.
-
A qualidade da estimativa não tem relação com haver ou não índices. Depende das estatísticas recolhidas por ANALYZE sobre a distribuição dos valores nas colunas.
-
-
Vês
Seq Scanno plano de uma query. Qual é a primeira coisa a fazer?-
É o reflexo mais comum e falha muitas vezes. Se a tabela for pequena, ou se a query devolver grande parte das linhas, o Seq Scan é a escolha certa e o índice novo só acrescenta custo às escritas e ocupa espaço — sem nunca ser usado.
-
Certo. São os dois números que decidem se um índice pode sequer ajudar. Sem eles estás a adivinhar; com eles, a decisão faz-se por aritmética.
-
Isto trata o Seq Scan como se fosse um defeito. Numa tabela de 50 linhas ele é imbatível, e nenhuma reescrita melhora o que já é ótimo. O Seq Scan é um sintoma a interpretar, não um erro a eliminar.
-
9. Explica por palavras tuas
Entregar
O botão gera o Markdown com estas respostas e o resultado do quiz. Cola-o num ficheiro e corre
/corrigir indices-btree-sql.
Guardar em topicos/indices-btree-sql/respostas/AAAA-MM-DD.md.
10. Resumo
- A base de dados lê páginas de 8 kB, não linhas. O tempo prevê-se contando páginas.
- Uma tabela é um monte sem ordem: sem índice, encontrar uma linha obriga a ler todas as páginas.
- O
costé uma unidade arbitrária em que 1 = ler uma página em sequência. Não é tempo e não se converte em tempo. - O custo de um
Seq Scanérelpages×1 + reltuples×0.01 + reltuples×0.0025(com filtro) — e bate certo ao cêntimo. - Os limiares do planeador vêm de parâmetros configuráveis, não de leis. Quem cita percentagens fixas está a descrever a máquina dele.
11. Fontes
- Documentação do PostgreSQL 17 — Database Page Layout. postgresql.org/docs/17/storage-page-layout.html — fonte primária para o tamanho da página e o que lá vive dentro. 🌐 Verificado a 2026-09-15
- Documentação do PostgreSQL 17 — Planner Cost Constants. runtime-config-query.html — os valores por omissão de cada parâmetro de custo e a advertência explícita de que as unidades são arbitrárias. 🌐 Verificado a 2026-09-15
- Documentação do PostgreSQL 17 — Using EXPLAIN.
using-explain.html
— porquê: é o texto que faz a conta do
Seq Scanpasso a passo, e é o melhor de toda a documentação. O que saltar: nada; são poucas páginas. 🌐 Verificado a 2026-09-15 - Todos os números desta lição foram medidos em PostgreSQL 17.11
(Homebrew, macOS arm64), com os parâmetros nos valores por omissão, sobre a tabela de
preparar.sql. Na tua máquina os valores de
relpagespodem diferir ligeiramente; a fórmula não muda. ✅ Corrido a 2026-09-15