SQL Quando o Índice Ajuda
Na aula Índices (CREATE INDEX) você aprendeu a criar um índice, e na EXPLAIN QUERY PLAN a ver se o banco usa ele. Agora a pergunta é outra: vale a pena? Nesta aula a Ana monta um banco com um milhão de vendas, mede o tempo de uma consulta antes e depois do índice, descobre quanto o índice custa e vê os casos em que ele é ignorado, ou até deixa tudo mais lento.
- Gerar um banco grande de teste com uma CTE recursiva e medir o tempo com
.timer on. - O ganho de um índice numa busca que pega poucas linhas, medido de verdade.
- O preço do índice: espaço no disco e
INSERTmais lento. - Os casos em que o índice não é usado: função na coluna,
LIKE '%…', conta na coluna, coluna fora de ordem no índice composto. - Quando o índice atrapalha: colunas com poucos valores diferentes.
- Como forçar ou proibir o índice (
NOT INDEXED,INDEXED BY) e um roteiro para decidir.
Todos os tempos foram medidos no terminal, na máquina da Ana (o notebook dela, com o sqlite3 3.50.4). No seu computador os números vão ser outros: mais rápidos ou mais lentos. O que importa é a comparação entre eles (dez vezes mais rápido, duas vezes mais lento), não o valor exato. O editor do guia não mede tempo; nele você confere os planos, que são os mesmos.
A ideia: o índice do livro tem um preço
O índice remissivo no fim do livro de receitas economiza muito tempo de quem procura "cenoura". Mas ele não é de graça:
- ocupa páginas a mais no livro;
- a cada receita nova, alguém tem de atualizar o índice, além de escrever a receita;
- e para uma palavra que aparece em metade das páginas (como "sal"), ele não ajuda nada: é mais fácil folhear o livro do que ir e voltar do índice quinhentas vezes.
No banco é igual. Um índice compensa quando a consulta quer poucas linhas de muitas, e quando a tabela é mais lida do que escrita. O resto desta aula mostra isso com números.
1. Um banco grande para medir
Com os 90 pedidos da Loja Aurora, qualquer consulta é instantânea, com ou sem índice. Para ver diferença, a Ana precisa de muitas linhas. Ela cria, num arquivo separado (vendas.db, para não mexer na loja), uma tabela de vendas e a enche com um milhão de linhas inventadas, geradas por uma CTE recursiva (aula CTE Recursiva):
$ sqlite3 vendas.db
SQLite version 3.50.4 2025-07-30 19:33:53
Enter ".help" for usage hints.
sqlite> CREATE TABLE vendas (
(x1...> id INTEGER PRIMARY KEY,
(x1...> cliente_id INTEGER,
(x1...> produto_id INTEGER,
(x1...> data TEXT,
(x1...> pago INTEGER,
(x1...> valor REAL
(x1...> );
sqlite> .timer on
sqlite> INSERT INTO vendas
...> WITH RECURSIVE n(i) AS (SELECT 1 UNION ALL SELECT i + 1 FROM n WHERE i < 1000000)
...> SELECT i, (i * 7919) % 50000 + 1, (i * 31) % 45 + 1,
...> date('2021-01-01', '+' || (i % 2000) || ' days'),
...> i % 2, round((i * 37) % 50000 / 100.0, 2)
...> FROM n;
Run Time: real 1.479 user 1.383718 sys 0.035837
sqlite> SELECT COUNT(*) FROM vendas;
1000000
Run Time: real 0.012 user 0.004625 sys 0.007986
- (x1...> e ...>
- O
sqlite3mostra esses prompts de continuação enquanto o comando não terminou (falta o;). O(x1avisa que há um parêntese aberto. - .timer on
- Comando de ponto (aula Comandos de Ponto): depois de cada comando, mostra quanto tempo ele levou.
- WITH RECURSIVE n(i) AS (…)
- Gera os números de 1 a 1.000.000, um por linha.
- (i * 7919) % 50000 + 1
- Um
cliente_identre 1 e 50.000, "embaralhado" por uma conta. Não é sorteio: rodando de novo, sai igual, e os números desta aula podem ser conferidos. - date('2021-01-01', '+' || (i % 2000) || ' days')
- Uma data entre 2021 e 2026 (aula Funções de Data).
- i % 2
- A coluna
pago: 0 ou 1, metade das vendas de cada. - Run Time: real 1.479
- O tempo: real é o tempo de relógio, em segundos; user e sys são o tempo de processador gasto pelo programa e pelo sistema. Olhe o
real.
Um milhão de linhas em um segundo e meio. Cada cliente tem 20 vendas: 1.000.000 dividido por 50.000 clientes.
No editor, a mesma ideia funciona com dez mil linhas (cada Rodar tem um limite de cerca de 4 segundos, e um milhão de linhas não cabe nele). É assim que os exemplos desta aula mostram os planos:
CREATE TABLE vendas (id INTEGER PRIMARY KEY, cliente_id INTEGER, produto_id INTEGER,
data TEXT, pago INTEGER, valor REAL);
INSERT INTO vendas
WITH RECURSIVE n(i) AS (SELECT 1 UNION ALL SELECT i + 1 FROM n WHERE i < 10000)
SELECT i, (i * 7919) % 50000 + 1, (i * 31) % 45 + 1,
date('2021-01-01', '+' || (i % 2000) || ' days'),
i % 2, round((i * 37) % 50000 / 100.0, 2)
FROM n;
SELECT * FROM vendas LIMIT 3;
2. Antes e depois do índice
A Ana quer o total comprado pelo cliente 4242. Sem índice, o plano é SCAN vendas: ler o milhão de linhas para achar as 20 dele. Ela roda duas vezes, para ter certeza do tempo:
sqlite> SELECT SUM(valor) FROM vendas WHERE cliente_id = 4242; 7128.6 Run Time: real 0.046 user 0.037990 sys 0.007902 sqlite> SELECT SUM(valor) FROM vendas WHERE cliente_id = 4242; 7128.6 Run Time: real 0.047 user 0.042404 sys 0.004080
Uns 46 milésimos de segundo. Parece pouco, mas é para uma busca; um site que faz essa consulta mil vezes por minuto gasta uns 46 segundos de processador por minuto só nela. Agora, com o índice:
$ sqlite3 vendas.db SQLite version 3.50.4 2025-07-30 19:33:53 Enter ".help" for usage hints. sqlite> .timer on sqlite> CREATE INDEX idx_vendas_cliente ON vendas(cliente_id); Run Time: real 0.400 user 0.365545 sys 0.008074 sqlite> SELECT SUM(valor) FROM vendas WHERE cliente_id = 4242; 7128.6 Run Time: real 0.000 user 0.000130 sys 0.000003 sqlite> SELECT SUM(valor) FROM vendas WHERE cliente_id = 4242; 7128.6 Run Time: real 0.000 user 0.000028 sys 0.000001 sqlite> .timer off sqlite> EXPLAIN QUERY PLAN SELECT SUM(valor) FROM vendas WHERE cliente_id = 4242; QUERY PLAN `--SEARCH vendas USING INDEX idx_vendas_cliente (cliente_id=?)
real 0.000: menos de um milésimo de segundo, abaixo do que o .timer consegue mostrar. Pelo tempo de processador (user: 0,038 s antes, 0,00013 s depois), a busca ficou centenas de vezes mais rápida. O plano mudou de SCAN para SEARCH … USING INDEX: em vez de ler um milhão de linhas, o SQLite achou no índice as 20 do cliente e leu só essas.
| Na máquina da Ana (1.000.000 de vendas) | Tempo (real) |
|---|---|
Total do cliente 4242, sem índice (SCAN) | 0,046 s |
Criar o índice idx_vendas_cliente | 0,400 s (uma vez só) |
Total do cliente 4242, com índice (SEARCH) | menos de 0,001 s |
Esse é o caso ideal do índice: uma tabela grande e uma busca que quer poucas linhas dela (20 de um milhão).
3. O preço do índice
Espaço no disco
O índice é uma cópia ordenada da coluna, com o endereço de cada linha. Ele fica no mesmo arquivo do banco:
$ ls -lh vendas.db -rw-r--r-- 1 ana ana 33M out 18 11:17 vendas.db $ ls -lh vendas.db -rw-r--r-- 1 ana ana 44M out 18 11:19 vendas.db
O primeiro ls foi feito antes de criar o idx_vendas_cliente; o segundo, logo depois. O arquivo cresceu de 33 para 44 megabytes. Quanto ocupa cada parte, a tabela virtual dbstat do SQLite conta (aqui já com mais dois índices, que você vai ver nas seções seguintes):
sqlite> SELECT name, round(SUM(pgsize) / 1048576.0, 1) AS mb FROM dbstat GROUP BY name ORDER BY mb DESC; vendas|32.9 idx_vendas_data|18.2 idx_vendas_cliente|10.8 idx_vendas_pago|8.6 sqlite_schema|0.0
Três índices juntos (37,6 MB) já ocupam mais que a própria tabela (32,9 MB). O dbstat também funciona no editor, com a tabela de dez mil linhas:
CREATE TABLE vendas (id INTEGER PRIMARY KEY, cliente_id INTEGER, produto_id INTEGER,
data TEXT, pago INTEGER, valor REAL);
INSERT INTO vendas
WITH RECURSIVE n(i) AS (SELECT 1 UNION ALL SELECT i + 1 FROM n WHERE i < 10000)
SELECT i, (i * 7919) % 50000 + 1, (i * 31) % 45 + 1,
date('2021-01-01', '+' || (i % 2000) || ' days'),
i % 2, round((i * 37) % 50000 / 100.0, 2)
FROM n;
CREATE INDEX idx_vendas_pago ON vendas(pago);
SELECT name, SUM(pgsize) AS bytes
FROM dbstat
WHERE name LIKE '%vendas%'
GROUP BY name;
INSERT mais lento
A cada linha nova, o SQLite grava a linha na tabela e uma entrada em cada índice, no lugar certo da ordem. A Ana copiou o milhão de vendas para duas tabelas vazias: uma sem índice e outra com três índices (em cliente_id, data e pago):
sqlite> CREATE TABLE sem_indice AS SELECT * FROM vendas WHERE 0; sqlite> CREATE TABLE com_indices AS SELECT * FROM vendas WHERE 0; sqlite> CREATE INDEX ci_cliente ON com_indices(cliente_id); sqlite> CREATE INDEX ci_data ON com_indices(data); sqlite> CREATE INDEX ci_pago ON com_indices(pago); sqlite> .timer on sqlite> INSERT INTO sem_indice SELECT * FROM vendas; Run Time: real 0.309 user 0.219081 sys 0.041150 sqlite> INSERT INTO com_indices SELECT * FROM vendas; Run Time: real 7.627 user 4.260789 sys 3.274614
De 0,31 segundo para 7,63 segundos: quase vinte e cinco vezes mais lento, na máquina da Ana. (O WHERE 0 é um truque: copia só a estrutura da tabela, sem nenhuma linha.) Por isso, quem vai carregar muitos dados de uma vez costuma apagar os índices, carregar e criar os índices de novo no fim: criar um índice numa tabela cheia (0,40 s, na seção 2) é bem mais rápido que mantê-lo linha por linha.
4. Quando o índice não é usado
O índice guarda os valores da coluna como estão escritos, em ordem. Ele só serve quando o WHERE compara a coluna pura com um valor. Os casos abaixo são os mais comuns de "criei o índice e nada mudou".
4.1. Função na coluna
A Ana quer as vendas de março de 2022 e escreve, de um jeito bem natural, "os 7 primeiros caracteres da data são 2022-03":
CREATE TABLE vendas (id INTEGER PRIMARY KEY, cliente_id INTEGER, produto_id INTEGER,
data TEXT, pago INTEGER, valor REAL);
INSERT INTO vendas
WITH RECURSIVE n(i) AS (SELECT 1 UNION ALL SELECT i + 1 FROM n WHERE i < 10000)
SELECT i, (i * 7919) % 50000 + 1, (i * 31) % 45 + 1,
date('2021-01-01', '+' || (i % 2000) || ' days'),
i % 2, round((i * 37) % 50000 / 100.0, 2)
FROM n;
CREATE INDEX idx_vendas_data ON vendas(data);
EXPLAIN QUERY PLAN
SELECT COUNT(*), SUM(valor) FROM vendas WHERE substr(data, 1, 7) = '2022-03';
EXPLAIN QUERY PLAN
SELECT COUNT(*), SUM(valor) FROM vendas WHERE data >= '2022-03-01' AND data < '2022-04-01';
Com o substr(): SCAN vendas. O índice tem as datas inteiras em ordem ("2022-03-01", "2022-03-02"…), não os pedaços que o substr() calcula. Para saber o substr() de cada linha, o SQLite tem de ler todas. Com a faixa de datas (de 1º de março até antes de 1º de abril), a coluna fica pura e o plano é SEARCH … (data>? AND data<?). As duas consultas dão o mesmo resultado:
CREATE TABLE vendas (id INTEGER PRIMARY KEY, cliente_id INTEGER, produto_id INTEGER,
data TEXT, pago INTEGER, valor REAL);
INSERT INTO vendas
WITH RECURSIVE n(i) AS (SELECT 1 UNION ALL SELECT i + 1 FROM n WHERE i < 10000)
SELECT i, (i * 7919) % 50000 + 1, (i * 31) % 45 + 1,
date('2021-01-01', '+' || (i % 2000) || ' days'),
i % 2, round((i * 37) % 50000 / 100.0, 2)
FROM n;
SELECT COUNT(*), SUM(valor) FROM vendas WHERE substr(data, 1, 7) = '2022-03';
SELECT COUNT(*), SUM(valor) FROM vendas WHERE data >= '2022-03-01' AND data < '2022-04-01';
No milhão de vendas, na máquina da Ana:
sqlite> CREATE INDEX idx_vendas_data ON vendas(data); Run Time: real 0.381 user 0.318901 sys 0.028327 sqlite> SELECT COUNT(*), SUM(valor) FROM vendas WHERE substr(data, 1, 7) = '2022-03'; 15500|3847665.0 Run Time: real 0.081 user 0.068671 sys 0.012100 sqlite> SELECT COUNT(*), SUM(valor) FROM vendas WHERE data >= '2022-03-01' AND data < '2022-04-01'; 15500|3847665.0 Run Time: real 0.026 user 0.018327 sys 0.007816
O mesmo resultado, três vezes mais rápido. O mesmo vale para lower(cidade) = 'teresina', strftime('%Y', data) = '2025' e qualquer outra função em volta da coluna.
Missão: o índice na data dos pedidos existe, mas a consulta dos pedidos de 2025 não o usa (veja o plano). Reescreva as duas consultas, o plano e a contagem, com uma faixa de datas no lugar do substr(). A contagem tem de continuar dando 32.
CREATE INDEX idx_pedidos_data ON pedidos(data); EXPLAIN QUERY PLAN SELECT COUNT(*) FROM pedidos WHERE substr(data, 1, 4) = '2025'; SELECT COUNT(*) FROM pedidos WHERE substr(data, 1, 4) = '2025';
CREATE INDEX idx_pedidos_data ON pedidos(data); EXPLAIN QUERY PLAN SELECT COUNT(*) FROM pedidos WHERE data >= '2025-01-01' AND data < '2026-01-01'; SELECT COUNT(*) FROM pedidos WHERE data >= '2025-01-01' AND data < '2026-01-01';
O SQLite aceita um índice sobre uma expressão, não só sobre uma coluna. Aí a consulta tem de usar exatamente a mesma expressão:
CREATE INDEX idx_pedidos_ano ON pedidos(substr(data, 1, 4)); EXPLAIN QUERY PLAN SELECT * FROM pedidos WHERE substr(data, 1, 4) = '2025';
O plano mostra (<expr>=?): o índice da expressão. Funciona, mas é mais um índice para manter. Quando dá para escrever a faixa, a faixa com o índice comum serve para muito mais consultas.
4.2. LIKE com % no começo (e o problema das maiúsculas)
Com um índice no nome dos clientes, veja três buscas:
CREATE INDEX idx_clientes_nome ON clientes(nome); EXPLAIN QUERY PLAN SELECT * FROM clientes WHERE nome = 'Tiago Fontes'; EXPLAIN QUERY PLAN SELECT * FROM clientes WHERE nome LIKE '%Lima'; EXPLAIN QUERY PLAN SELECT * FROM clientes WHERE nome LIKE 'Ana%';
nome = 'Tiago Fontes':SEARCH, como esperado.LIKE '%Lima':SCAN. Um nome que termina com "Lima" pode começar com qualquer letra; num índice em ordem alfabética, ele pode estar em qualquer lugar. É como procurar no dicionário todas as palavras que terminam com "ção": o dicionário não ajuda.LIKE 'Ana%': tambémSCAN, e esse surpreende. O começo é conhecido, então o índice deveria ajudar. O problema é que, no SQLite, oLIKEignora maiúsculas (aula LIKE):'ana%'também acha "Ana". E o índice comum está em ordem que diferencia maiúsculas ("Ana" e "ana" ficam longe um do outro). Por isso o SQLite não confia no índice.
Há dois consertos. Um é escrever a faixa à mão, que diferencia maiúsculas: nome >= 'Ana' AND nome < 'Anb'. O outro é criar o índice na mesma regra do LIKE, com COLLATE NOCASE ("sem diferenciar maiúsculas"):
CREATE INDEX idx_clientes_nome_nc ON clientes(nome COLLATE NOCASE); EXPLAIN QUERY PLAN SELECT * FROM clientes WHERE nome LIKE 'Ana%'; SELECT nome FROM clientes WHERE nome LIKE 'ana%';
Agora o plano é SEARCH … (nome>? AND nome<?): o SQLite transformou o LIKE 'Ana%' numa faixa do índice. E a busca com 'ana%' minúsculo continua achando a Ana Beatriz e a Ana Carvalho. O LIKE '%Lima', com % no começo, não tem conserto com índice comum; para isso existe a busca de texto (aula Busca de Texto (FTS5)).
4.3. Conta na coluna
CREATE INDEX idx_produtos_preco ON produtos(preco); EXPLAIN QUERY PLAN SELECT * FROM produtos WHERE preco * 1.1 > 50; EXPLAIN QUERY PLAN SELECT * FROM produtos WHERE preco > 50 / 1.1;
"Os produtos que, com 10% de aumento, passam de 50 reais." Escrito como preco * 1.1 > 50, a conta está na coluna, e é SCAN. Passando a conta para o outro lado (preco > 50 / 1.1), a coluna fica pura e é SEARCH. É a mesma regra da função: a coluna sozinha de um lado, o valor do outro.
4.4. Índice composto fora de ordem
Um índice pode ter várias colunas. O índice (estado, cidade) é como uma lista telefônica ordenada por estado e, dentro de cada estado, por cidade:
CREATE INDEX idx_clientes_estado_cidade ON clientes(estado, cidade); EXPLAIN QUERY PLAN SELECT * FROM clientes WHERE estado = 'CE' AND cidade = 'Fortaleza'; EXPLAIN QUERY PLAN SELECT * FROM clientes WHERE estado = 'CE'; EXPLAIN QUERY PLAN SELECT * FROM clientes WHERE cidade = 'Fortaleza';
WHERE | Plano | Por quê |
|---|---|---|
estado = 'CE' AND cidade = 'Fortaleza' | SEARCH … (estado=? AND cidade=?) | Usa as duas colunas do índice |
estado = 'CE' | SEARCH … (estado=?) | Usa a primeira coluna: todos do CE estão juntos |
cidade = 'Fortaleza' | SCAN | A cidade só está em ordem dentro de cada estado; as Fortalezas podem estar espalhadas pela lista |
A regra: o índice composto serve para buscas que usam as colunas a partir da primeira, da esquerda para a direita. Na hora de criar, ponha primeiro a coluna que aparece em mais consultas com =.
5. Quando o índice atrapalha
A coluna pago tem só dois valores, 0 e 1, cada um em metade das vendas. A Ana criou um índice nela e mediu o total das vendas pagas, com o índice e proibindo o índice (NOT INDEXED, "não indexado", obriga o SQLite a ler a tabela direto):
sqlite> CREATE INDEX idx_vendas_pago ON vendas(pago); Run Time: real 0.269 user 0.213390 sys 0.026817 sqlite> SELECT SUM(valor) FROM vendas WHERE pago = 1; 125000000.0 Run Time: real 0.140 user 0.119608 sys 0.019961 sqlite> SELECT SUM(valor) FROM vendas NOT INDEXED WHERE pago = 1; 125000000.0 Run Time: real 0.055 user 0.051250 sys 0.003889
Com o índice: 0,140 s. Sem: 0,055 s. O índice deixou a consulta duas vezes e meia mais lenta, na máquina da Ana. O motivo é o "sal" do livro de receitas: para 500 mil linhas, o SQLite vai ao índice, pega o endereço, pula para a linha na tabela, volta ao índice… quinhentas mil vezes. Ler a tabela de uma vez, em ordem, é mais rápido.
E o SQLite escolheu o índice mesmo assim:
sqlite> EXPLAIN QUERY PLAN SELECT SUM(valor) FROM vendas WHERE pago = 1; QUERY PLAN `--SEARCH vendas USING INDEX idx_vendas_pago (pago=?)
Sem informação sobre os dados, ele supõe que um índice com = pega poucas linhas. Nem depois de um ANALYZE (que junta estatísticas sobre os índices; aula VACUUM e ANALYZE) o plano mudou, na versão 3.50.4. A lição é simples: não crie índice em coluna com poucos valores diferentes (sim/não, 0/1, sexo, status com 3 opções), a não ser que você busque quase sempre o valor raro.
No editor, você confere os dois planos. Além do NOT INDEXED, um + na frente da coluna também faz o SQLite ignorar o índice (a coluna deixa de ser "pura"):
CREATE TABLE vendas (id INTEGER PRIMARY KEY, cliente_id INTEGER, produto_id INTEGER,
data TEXT, pago INTEGER, valor REAL);
INSERT INTO vendas
WITH RECURSIVE n(i) AS (SELECT 1 UNION ALL SELECT i + 1 FROM n WHERE i < 10000)
SELECT i, (i * 7919) % 50000 + 1, (i * 31) % 45 + 1,
date('2021-01-01', '+' || (i % 2000) || ' days'),
i % 2, round((i * 37) % 50000 / 100.0, 2)
FROM n;
CREATE INDEX idx_vendas_pago ON vendas(pago);
EXPLAIN QUERY PLAN SELECT SUM(valor) FROM vendas WHERE pago = 1;
EXPLAIN QUERY PLAN SELECT SUM(valor) FROM vendas NOT INDEXED WHERE pago = 1;
EXPLAIN QUERY PLAN SELECT SUM(valor) FROM vendas WHERE +pago = 1;
Experimente: crie outros índices na tabela vendas (em produto_id, em (cliente_id, data)) e peça os planos de consultas diferentes: WHERE produto_id = 7, WHERE cliente_id = 4242 AND data >= '2024-01-01', WHERE data = '2023-05-10'.
CREATE TABLE vendas (id INTEGER PRIMARY KEY, cliente_id INTEGER, produto_id INTEGER,
data TEXT, pago INTEGER, valor REAL);
INSERT INTO vendas
WITH RECURSIVE n(i) AS (SELECT 1 UNION ALL SELECT i + 1 FROM n WHERE i < 10000)
SELECT i, (i * 7919) % 50000 + 1, (i * 31) % 45 + 1,
date('2021-01-01', '+' || (i % 2000) || ' days'),
i % 2, round((i * 37) % 50000 / 100.0, 2)
FROM n;
CREATE INDEX idx_vendas_cli_data ON vendas(cliente_id, data);
EXPLAIN QUERY PLAN
SELECT * FROM vendas WHERE cliente_id = 4242 AND data >= '2024-01-01';
6. Roteiro: criar ou não criar?
- A tabela é grande? Com algumas centenas de linhas, qualquer consulta é instantânea; o índice só ocupa espaço.
- A consulta é lenta e frequente? Comece pela consulta, não pelo índice. Rode-a com
.timer one peça o plano. - O plano mostra
SCANnuma tabela grande, buscando poucas linhas? Crie o índice na coluna doWHERE(ou doJOIN). - A coluna tem muitos valores diferentes?
cliente_id,data,email: sim.pago,vip: não. - O
WHEREusa a coluna pura? Tire funções e contas de cima da coluna, e o%do começo doLIKE. - Meça de novo e confira o plano. Se não ficou mais rápido, apague o índice (
DROP INDEX): ele vai deixar todoINSERTmais lento à toa.
Formas diferentes de fazer a mesma coisa
Para a mesma busca, "nomes que começam com Ana", e para o mesmo controle do índice, há várias escritas:
| Escrita | Usa índice? | Quando usar |
|---|---|---|
nome LIKE 'Ana%' com índice comum | Não | Tabela pequena |
nome LIKE 'Ana%' com índice COLLATE NOCASE | Sim | Busca por começo, sem diferenciar maiúsculas |
nome >= 'Ana' AND nome < 'Anb' | Sim, com índice comum | Quando maiúsculas importam |
substr(data, 1, 4) = '2025' | Não (só com índice de expressão) | Evite em tabela grande |
data >= '2025-01-01' AND data < '2026-01-01' | Sim | Sempre que der |
FROM t NOT INDEXED ou WHERE +col = … | Proíbe | Quando o índice atrapalha (seção 5) ou para medir |
FROM t INDEXED BY idx | Obriga a usar idx | Raramente; para testes. Se o índice sumir, a consulta passa a dar erro |
Quando dá errado
CREATE INDEX idx_c ON clientes(cidade); CREATE INDEX idx_c ON clientes(cidade);
A mensagem: index idx_c already exists, "o índice idx_c já existe". Acontece muito ao rodar o mesmo script duas vezes.
Como consertar: CREATE INDEX IF NOT EXISTS idx_c ON clientes(cidade); cria só se não existir, sem erro.
SELECT * FROM clientes INDEXED BY idx_cidade WHERE cidade = 'Picos';
A mensagem: no such index: idx_cidade, "não existe o índice idx_cidade". É o risco do INDEXED BY: se alguém apagar ou renomear o índice, a consulta, que antes funcionava, quebra. O mesmo erro aparece num DROP INDEX com nome errado.
Como consertar: deixe o SQLite escolher (tire o INDEXED BY), ou crie o índice antes.
CREATE UNIQUE INDEX idx_clientes_cidade ON clientes(cidade);
A mensagem: UNIQUE constraint failed: clientes.cidade, "a regra UNIQUE falhou". Um UNIQUE INDEX (aula UNIQUE) é um índice que também proíbe valores repetidos, e há 10 clientes de Teresina. Ele não foi criado.
Como consertar: se a ideia era só deixar a busca rápida, use CREATE INDEX, sem UNIQUE.
SCAN
Não é um erro com mensagem, e é o mais comum de todos. Confira, nesta ordem: o WHERE tem função ou conta em cima da coluna (seção 4.1 e 4.3)? Tem LIKE com % no começo, ou LIKE sem índice NOCASE (4.2)? É um índice composto e a consulta não usa a primeira coluna (4.4)? A consulta quer a maior parte da tabela (então o SCAN é o certo)?
As regras desta aula (coluna pura, ordem do índice composto, preço no INSERT) valem em todos os bancos. A escrita do índice de expressão muda. Os trechos são ilustrativos: não rodam no SQLite.
- PostgreSQL: aceita índice sobre expressão, como
CREATE INDEX … ON clientes (lower(cidade)). A documentação lembra que ele custa a cadaINSERTeUPDATE, porque a expressão é calculada para cada linha. - MySQL: chama isso de functional key part, e a expressão vai entre parênteses duplos:
CREATE INDEX idx1 ON t1 ((col1 + col2)).
-- PostgreSQL (ilustrativo) CREATE INDEX idx_clientes_cidade_min ON clientes (lower(cidade)); -- MySQL (ilustrativo) CREATE INDEX idx_clientes_cidade_min ON clientes ((lower(cidade)));
Referência rápida
| Escrita | O que faz |
|---|---|
.timer on / .timer off | No terminal: mostra o tempo de cada comando |
CREATE INDEX IF NOT EXISTS i ON t(col) | Cria o índice, se ainda não existir |
CREATE INDEX i ON t(a, b) | Índice composto: serve para a, e para a + b |
CREATE INDEX i ON t(col COLLATE NOCASE) | Índice que ignora maiúsculas: serve para LIKE 'x%' |
CREATE INDEX i ON t(substr(data, 1, 4)) | Índice de expressão |
DROP INDEX i | Apaga o índice (a tabela fica intacta) |
FROM t NOT INDEXED | Proíbe índices nesta consulta |
FROM t INDEXED BY i | Obriga a usar o índice i |
SELECT name, SUM(pgsize) FROM dbstat GROUP BY name | Espaço ocupado por tabela e índice |
Exercícios
1. Faixa no lugar da função
Complete para buscar os pedidos de 2024 sem função na coluna:
2. Proibir o índice
Complete para o SQLite ler a tabela direto, sem índice:
3. Em qual coluna da tabela vendas (um milhão de linhas) um índice ajuda mais?
4. Com um índice em nome, qual destas buscas pode usar o índice?
5. O índice é (estado, cidade). Qual consulta não consegue usá-lo para ir direto?
6. Na máquina da Ana, copiar um milhão de vendas para uma tabela com três índices levou 7,63 s, e para uma sem índices, 0,31 s. Por quê?
Missão final: o LIKE que usa índice
Missão: a busca de clientes por começo de nome (LIKE 'ana%') não usa o índice, e o plano mostra SCAN. Mude só o CREATE INDEX para o plano virar SEARCH, sem deixar de achar as duas Anas com o 'ana%' minúsculo.
CREATE INDEX idx_clientes_nome ON clientes(nome); EXPLAIN QUERY PLAN SELECT nome FROM clientes WHERE nome LIKE 'ana%'; SELECT nome FROM clientes WHERE nome LIKE 'ana%';
CREATE INDEX idx_clientes_nome ON clientes(nome COLLATE NOCASE); EXPLAIN QUERY PLAN SELECT nome FROM clientes WHERE nome LIKE 'ana%'; SELECT nome FROM clientes WHERE nome LIKE 'ana%';
Resumo
- Para medir, gere muitas linhas com uma CTE recursiva e use
.timer on. Compare os tempos entre si; o número exato depende da máquina. - Na máquina da Ana, achar 20 vendas em um milhão caiu de 0,046 s (
SCAN) para menos de 0,001 s (SEARCH) com o índice. Esse é o caso ideal: tabela grande, poucas linhas. - O índice custa espaço (33 MB → 44 MB) e deixa o
INSERTmais lento (0,31 s → 7,63 s com três índices). - O índice só é usado com a coluna pura: nada de função (
substr,lower), conta ouLIKE '%…'. Troque por faixas (data >= … AND data < …). LIKE 'x%'só usa índice criado comCOLLATE NOCASE. O índice composto vale a partir da primeira coluna.- Em coluna com poucos valores (como
pago), o índice pode deixar a consulta mais lenta;NOT INDEXEDe+coldesligam o índice.