Saltar para o conteúdo

professor Índices B-tree Lição 0

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.

  1. 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.

  2. 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:

⭐ A unidade que conta

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.

Uma tabela como sequência de páginas de 8 kB Oito páginas em fila, cada uma com várias linhas lá dentro. Uma seta percorre-as todas, da primeira à última, ilustrando que a varredura sequencial lê todas as páginas independentemente de quantas linhas a query devolve. tabela «encomendas» — 8348 páginas de 8 kB pág. 0 pág. 3 pág. 8347 Seq Scan: lê as 8348, mesmo que só interesse 1 linha da página 3 A página realçada é a que tem a linha procurada. As outras 8347 foram lidas à mesma.
O 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âmetroOmissãoO que representa
seq_page_cost1.0Ler uma página em sequência. É a régua: vale 1 por definição
random_page_cost4.0Ler 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_cost0.01Processar uma linha lida
cpu_operator_cost0.0025Avaliar um operador ou função — por exemplo, o = de um WHERE
cpu_index_tuple_cost0.005Processar uma entrada de índice
⚠️ O custo não é tempo, e isso tem consequências práticas

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:

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)
✅ 20 848.00 — exatamente o número previsto

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:

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 erradaComo se reconheceO 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.

  1. 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?

  2. O EXPLAIN mostra cost=0.00..20848.00. O que é o primeiro número?

  3. Porque é que random_page_cost vale 4 e não 1, por omissão?

  4. O EXPLAIN diz rows=969867 mas a query devolve 970 000 linhas. O que é que isto revela?

  5. Vês Seq Scan no plano de uma query. Qual é a primeira coisa a fazer?

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

11. Fontes