O que é um “índice coberto” (covering index)
Um índice coberto é um índice que contém todas as colunas necessárias para a consulta: as colunas usadas em WHERE, as colunas retornadas no SELECT e (quando aplicável) as colunas usadas em ORDER BY ou GROUP BY. Quando isso acontece, o banco consegue responder a consulta lendo apenas o índice, sem precisar buscar o registro completo na tabela (o “row lookup”, “bookmark lookup” ou “table access”).
Na prática, “cobrir” significa: evitar leituras adicionais na estrutura principal da tabela (heap ou clustered), reduzindo I/O e latência.
Dois usos bem diferentes do índice
- Usar índice para localizar: o índice ajuda a encontrar quais linhas servem, mas depois o banco precisa ir à tabela buscar colunas que não estão no índice.
- Usar índice para responder: o índice já tem tudo o que a consulta precisa; o banco lê as entradas do índice e finaliza a consulta sem tocar na tabela.
Por que isso acelera: comparando custo em páginas lidas
Para visualizar o ganho, pense em custo como “quantas páginas (blocos) de disco/memória foram lidas”. Um índice B-tree costuma ter páginas menores e mais “densas” (mais entradas por página) do que páginas de dados da tabela. Além disso, quando a consulta não é coberta, cada linha encontrada no índice pode disparar uma leitura extra na tabela.
Modelo mental simples de custo
- Consulta não coberta: páginas do índice (para achar as chaves) + páginas da tabela (para buscar colunas faltantes). Em muitos casos, vira:
Index pages + N * (table page)(com N grande). - Consulta coberta: páginas do índice (para achar e retornar colunas). Em muitos casos:
Index pages(e só).
Mesmo quando parte das páginas está em cache, reduzir leituras na tabela costuma reduzir CPU, latches/locks e pressão de memória.
Exemplo 1: Selecionar poucas colunas (evitando “lookup” na tabela)
Suponha uma tabela de pedidos:
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
CREATE TABLE pedidos ( id BIGINT PRIMARY KEY, cliente_id BIGINT NOT NULL, status VARCHAR(20) NOT NULL, criado_em TIMESTAMP NOT NULL, total DECIMAL(10,2) NOT NULL, observacao TEXT);Consulta comum:
SELECT id, criado_em, total FROM pedidos WHERE cliente_id = 42 AND status = 'PAGO';Sem índice coberto
Imagine que existe um índice apenas para filtrar:
CREATE INDEX idx_pedidos_cliente_status ON pedidos (cliente_id, status);O banco usa o índice para localizar os id que batem com cliente_id e status, mas como precisa retornar criado_em e total, ele tende a fazer buscas na tabela para cada linha (ou para muitas delas).
Comparação de páginas lidas (exemplo ilustrativo):
| Cenário | Leituras típicas | O que acontece |
|---|---|---|
| Índice para localizar | páginas do índice + muitas páginas da tabela | encontra chaves no índice e faz lookup na tabela para pegar colunas |
| Índice para responder (coberto) | apenas páginas do índice | retorna colunas diretamente do índice |
Com índice coberto
Crie um índice que inclua também as colunas retornadas:
-- Forma genérica: incluir colunas no índice (varia por SGBD) -- Opção A (índice composto com colunas extras no final): CREATE INDEX idx_pedidos_coberto ON pedidos (cliente_id, status, criado_em, total, id);Agora, para essa consulta, o banco pode ler as entradas do índice e já ter id, criado_em e total sem tocar na tabela.
Passo a passo prático
- 1) Liste o que a consulta precisa: colunas do
WHERE(cliente_id,status) + colunas doSELECT(id,criado_em,total). - 2) Garanta que o índice contenha todas: em SGBDs com
INCLUDE, costuma-se colocar no “include” as colunas só retornadas; em outros, adiciona-se ao final do índice composto. - 3) Verifique o plano: procure por indicadores de “index-only scan”/“covering”/“using index” sem “lookup” na tabela.
- 4) Meça páginas lidas: compare estatísticas de I/O antes/depois (leituras lógicas/físicas). O objetivo é reduzir leituras na tabela.
Exemplo 2: ORDER BY sem “filesort” e sem ler a tabela
Consulta:
SELECT id, criado_em, total FROM pedidos WHERE cliente_id = 42 ORDER BY criado_em DESC LIMIT 20;Para ser eficiente, o banco precisa: (1) filtrar por cliente_id, (2) já entregar ordenado por criado_em, (3) retornar id e total sem ir à tabela.
Índice que ajuda a localizar, mas não cobre tudo
CREATE INDEX idx_pedidos_cliente_criado ON pedidos (cliente_id, criado_em DESC);Esse índice pode evitar uma ordenação cara (porque já está na ordem), mas se total não estiver no índice, o banco ainda faz lookup na tabela para cada uma das 20 linhas (ou mais, se precisar examinar mais linhas antes de aplicar filtros adicionais).
Índice coberto para filtro + ordenação + retorno
CREATE INDEX idx_pedidos_cliente_criado_coberto ON pedidos (cliente_id, criado_em DESC, id, total);Comparação de páginas lidas (exemplo ilustrativo com LIMIT):
- Sem cobertura: lê páginas do índice para achar os 20 primeiros + até 20 lookups na tabela (potencialmente 20 páginas de dados, ou mais, dependendo de dispersão).
- Com cobertura: lê páginas do índice e para ao atingir 20 entradas; não lê páginas da tabela.
Esse padrão é especialmente valioso com LIMIT, porque o banco pode parar cedo (early stop) lendo somente o índice.
Exemplo 3: Agregações simples com menos leituras
Agregações podem se beneficiar quando o índice contém as colunas do filtro e a coluna agregada, reduzindo acesso à tabela.
Consulta:
SELECT SUM(total) FROM pedidos WHERE cliente_id = 42 AND status = 'PAGO';Índice não coberto
CREATE INDEX idx_pedidos_cliente_status ON pedidos (cliente_id, status);O banco encontra as linhas pelo índice, mas precisa ler a tabela para obter total de cada linha. Se houver muitas linhas pagas, isso pode significar muitas páginas de dados lidas.
Índice coberto para a agregação
CREATE INDEX idx_pedidos_cliente_status_total ON pedidos (cliente_id, status, total);Agora o banco pode somar total lendo apenas o índice (dependendo do otimizador e do SGBD, isso pode aparecer como varredura apenas no índice). Mesmo que ainda precise visitar algumas páginas, a tendência é reduzir leituras da tabela.
Comparação de custo em páginas (ilustrativo):
| Cenário | Leituras | Observação |
|---|---|---|
| Filtro no índice, total na tabela | índice + muitas páginas de dados | cada linha agregada pode exigir acesso ao registro |
| Filtro e total no índice | principalmente páginas do índice | agregação pode ser feita com dados do índice |
Como decidir o que “colocar no índice” para cobrir
Checklist rápido
- Consultas que retornam poucas colunas e são muito frequentes são candidatas fortes.
- Consultas com ORDER BY + LIMIT ganham muito quando o índice cobre e já está na ordem.
- Relatórios simples (ex.:
SUM,COUNT) com filtros seletivos podem reduzir leituras se a coluna agregada estiver no índice.
Passo a passo para desenhar um índice coberto
- 1) Pegue a consulta real (com colunas exatas do
SELECT,WHERE,ORDER BY). - 2) Defina o objetivo: eliminar lookup na tabela? eliminar sort? reduzir páginas lidas?
- 3) Monte o índice: comece pelas colunas que determinam o acesso (filtros/ordenação) e adicione as colunas necessárias para retorno/agrupamento.
- 4) Valide no plano: procure por operação “somente índice” e ausência de “lookup” na tabela.
- 5) Compare I/O: antes/depois, compare leituras lógicas/físicas e tempo.
Ressalvas importantes: tamanho do índice e custo de manutenção
Índice coberto tende a ser maior
- Adicionar colunas aumenta o tamanho do índice, o que pode significar mais páginas de índice para ler e manter.
- Índices maiores reduzem a eficiência do cache: menos páginas “cabem” em memória, e isso pode afetar outras consultas.
Mais custo em INSERT/UPDATE/DELETE
- INSERT: precisa inserir entradas maiores no índice.
- UPDATE: se atualizar uma coluna presente no índice, o custo aumenta (pode exigir reescrita/movimentação de entradas).
- DELETE: remove entradas do índice, gerando trabalho extra e possível fragmentação.
Evite “cobrir tudo” indiscriminadamente
- Não tente colocar colunas grandes (ex.:
TEXT,BLOB) no índice apenas para cobrir; isso costuma explodir o tamanho e piorar o custo geral. - Prefira cobrir consultas críticas e com retorno pequeno.
- Se a consulta retorna muitas colunas (quase a linha inteira), o ganho de cobertura pode ser pequeno, e o índice pode ficar grande demais.
Como “enxergar” se a consulta está coberta
Embora a nomenclatura varie por SGBD, procure sinais no plano de execução de que a consulta foi resolvida sem acessar a tabela:
- Operações do tipo index-only scan / covering index / using index (sem etapa de lookup).
- Ausência de operações como table access by rowid, key lookup, bookmark lookup ou similares.
- Queda clara em métricas de I/O: menos leituras de páginas da tabela e, idealmente, menos leituras totais.