Índices B-tree — folha de consulta
Só o que se consulta. As explicações estão nas lições — uma folha que explica deixa de caber numa folha.
Ordem de diagnóstico de uma query lenta
EXPLAIN (ANALYZE, BUFFERS)— nunca semBUFFERS. NumUPDATE/DELETE:BEGIN; … ROLLBACK;rows=vsactual rows=— desencontro de ordens de grandeza? →ANALYZEantes de mais nada.- Multiplicar por
loops=antes de concluir seja o que for. - Onde está o tempo — o nó cujo
actual timefinal é próximo do total e cujos filhos são muito mais rápidos. (Os tempos são acumulados: o próprio é a diferença.) - Trabalho desperdiçado —
Rows Removed by Filter,Heap Fetches,Sort Method: external merge,Batches > 1,lossy=. - Páginas —
Buffers. É o único número estável entre execuções.
Sinais do plano → o que fazer
| No plano | Significa | Fazer |
|---|---|---|
rows= muito ≠ actual rows= | Estatísticas cegas | ANALYZE; se a coluna for desequilibrada, SET STATISTICS 500 |
Heap Fetches alto | Index-only a ir à tabela na mesma | VACUUM; ver relallvisible/relpages |
Buffers do índice ≈ tamanho do índice | Índice varrido, não procurado | Ordem das colunas: falta o prefixo mais à esquerda |
Rows Removed by Filter grande | Leu para deitar fora | Índice — provavelmente parcial |
Sort Method: external merge Disk: | Ordenou em disco | work_mem (é por operação); ou índice com a ordem certa; ou LIMIT |
Batches: > 1 num Hash | Dispersão não coube em memória | work_mem |
Heap Blocks: lossy= | Bitmap degradado | work_mem |
Planning Time > Execution Time | Planear custa mais que executar | PREPARE; menos índices para o planeador considerar |
Ordem das colunas de um índice composto
- Igualdade (
=) primeiro. - Intervalo ou
ORDER BYa seguir — e no fim, porque depois de um intervalo as colunas seguintes deixam de estar ordenadas. - Seletividade só para desempatar.
Prefixo mais à esquerda: (a,b,c) serve a · (a,b) ·
(a,b,c). Não serve nada que não fixe a.
Redundância: (a,b) torna um índice só em (a) desnecessário — apaga-se.
O que um B-tree serve (e não serve)
| Serve | Não serve |
|---|---|
= < <= > >=, BETWEEN, IN | LIKE '%x' (sem âncora no início) |
IS NULL, IS NOT NULL | f(coluna) = ? — salvo índice de expressão |
LIKE 'abc%', ~ '^abc' (padrão constante, ancorado) | Procurar por b num índice (a,b) |
ORDER BY pela coluna indexada; MIN/MAX | Desigualdade <> / != |
Comandos
-- ver o plano sem executar um DELETE/UPDATE
BEGIN; EXPLAIN (ANALYZE, BUFFERS) DELETE ...; ROLLBACK;
-- testar um índice sem o deixar (CREATE INDEX é transacional)
BEGIN; CREATE INDEX t ON ...; EXPLAIN (ANALYZE, BUFFERS) ...; ROLLBACK;
-- em produção com escrita: CREATE INDEX CONCURRENTLY (não é transacional)
-- índices nunca usados
SELECT indexrelname, idx_scan, pg_size_pretty(pg_relation_size(indexrelid))
FROM pg_stat_user_indexes WHERE relname = 't' ORDER BY idx_scan;
SELECT stats_reset FROM pg_stat_database WHERE datname = current_database();
-- estatísticas de uma coluna
SELECT attname, n_distinct, most_common_vals, correlation
FROM pg_stats WHERE tablename = 't';
-- tamanhos
SELECT pg_size_pretty(pg_table_size('t')), pg_size_pretty(pg_indexes_size('t'));
SELECT relpages, reltuples, relallvisible FROM pg_class WHERE relname = 't';
-- altura da B-tree (CREATE EXTENSION pageinspect;)
SELECT level FROM bt_metap('idx'); -- folhas = nível 0, logo level=2 são 3 níveis
-- diagnóstico: forçar e comparar. NUNCA deixar ligado.
SET enable_seqscan = off; SET enable_nestloop = off; RESET ALL;
-- parcial e de expressão
CREATE INDEX ON t (col) WHERE estado = 'pendente';
CREATE INDEX ON t ((lower(email))); -- a função tem de ser IMMUTABLE
-- e a query tem de usar a expressão IGUAL
Constantes de custo (omissão)
seq_page_cost 1 · random_page_cost 4 (SSD: ~1.1) ·
cpu_tuple_cost 0.01 · cpu_operator_cost 0.0025 ·
cpu_index_tuple_cost 0.005 · página 8 kB
Seq Scan com filtro:
relpages×1 + reltuples×0.01 + reltuples×0.0025
Números medidos (Postgres 17.11, tabela de 1 000 000 de linhas)
🔴 São desta máquina e desta tabela. Servem de ordem de grandeza, nunca de regra. Mede os teus.
| Medição | Valor |
|---|---|
| Tabela: 1 M linhas | 8348 páginas, 65 MB |
| Altura da B-tree (1 M linhas) | 3 níveis → 3 leituras para localizar |
| Sem índice vs com índice (20 linhas) | 8348 → 23 páginas; 15 ms → 0,075 ms |
| Index-only vs bitmap (mesma query) | 4 → 23 páginas (uma coluna a mais no SELECT) |
| Deduplicação ligada vs desligada | 7600 kB vs 21 MB |
| Limiar índice→Seq Scan (correlação ≈ 0) | ~50% (rpc=4) · ~60% (rpc=1.1). Não são 20% |
| 1% das linhas dispersas | 8359 páginas — o mesmo que ler tudo |
| 1% das linhas contíguas | 118 páginas |
| Ordem errada num índice composto | 1003 páginas vs 4 — 250× |
INSERT 200 k: 0 vs 4 índices | 182 ms vs 1232 ms — 6,8× |
| Índice parcial (2% das linhas) | 384 kB vs 6904 kB — 18× |
| Estatísticas cegas à distribuição | estimou 1789, eram 801 000 — 448× |
SQLite — o mesmo, noutro vocabulário
| Postgres | SQLite |
|---|---|
EXPLAIN (ANALYZE, BUFFERS) | EXPLAIN QUERY PLAN (sem tempos nem páginas) |
Seq Scan | SCAN t |
Index Scan | SEARCH t USING INDEX i (col=?) |
Index Only Scan | USING COVERING INDEX |
| ⚠️ índice varrido sem prefixo | SCAN t USING COVERING INDEX i — SCAN, não SEARCH |
ANALYZE | ANALYZE |