SQL Índices

A Ana atende um cliente no balcão da Loja Aurora e quer ver os pedidos dele. O banco responde na hora, porque a loja tem só 90 pedidos. Mas e quando forem 90 mil? Ou 9 milhões? Sem ajuda, o banco precisa ler todos os pedidos, um por um, para achar os daquele cliente. A ajuda tem nome: índice. Nesta aula você vai criar índices com CREATE INDEX, entender por que a ordem das colunas importa num índice composto, usar o UNIQUE INDEX para proibir valores repetidos e apagar índices com DROP INDEX.

Você vai aprender
  • O que é um índice, pela comparação com o índice remissivo de um livro.
  • Criar um índice com CREATE INDEX e ver os índices que o banco já tem.
  • Que um índice nunca muda o resultado de uma consulta: só o caminho até ele.
  • O índice composto (várias colunas) e por que a ordem das colunas faz diferença.
  • O UNIQUE INDEX, que impede valores repetidos, e o que ele faz com NULL.
  • Apagar índices com DROP INDEX, os índices que o SQLite cria sozinho e o preço de cada índice.
  • As mensagens reais do SQLite quando um índice dá errado.

Os exemplos rodam de verdade no seu navegador, num SQLite com o banco da Loja Aurora. Cada exemplo parte do banco limpo: o índice que um exemplo cria não existe no seguinte. As sessões de terminal saíram do sqlite3 3.50.4 no computador fictício da Ana, com o arquivo ~/loja.db.


A ideia: o índice remissivo do livro

Pegue um livro de receitas grosso e procure "mandioca". Você tem dois jeitos:

  1. Folhear o livro inteiro, página por página, olhando cada receita. Funciona sempre, mas demora, e demora mais quanto mais grosso o livro.
  2. Ir ao índice remissivo, lá no fim: uma lista em ordem alfabética com cada ingrediente e as páginas onde ele aparece. Você acha "mandioca" em segundos (porque a lista está em ordem) e vai direto às páginas 37 e 112.
Macaxeira ......... veja Mandioca
Maionese .......... 15, 88
Mandioca .......... 37, 112
Manga ............. 140
Maracujá .......... 141, 150

Um índice de banco de dados é exatamente isso. É uma lista à parte, sempre em ordem, com os valores de uma coluna e, ao lado de cada valor, onde estão as linhas que o têm. Num índice da coluna cliente_id dos pedidos, o banco acha "cliente 3" depressa e pula direto para os pedidos 3, 29, 55 e 81, sem ler os outros 86.

No livroNo banco
As páginas do livroA tabela, com as linhas na ordem em que foram gravadas
Folhear o livro inteiroLer a tabela inteira (o SQLite chama isso de SCAN, "varredura")
O índice remissivo, em ordem alfabéticaO índice, em ordem pelos valores da coluna
"Mandioca ... 37, 112""cliente_id 3 → linhas 3, 29, 55, 81"
Ir direto à página certaBuscar no índice e ler só as linhas certas (SEARCH, "busca")
Quando entra receita nova, o índice tem de ser atualizadoCada INSERT, UPDATE ou DELETE também atualiza o índice

Repare na última linha: o índice não é de graça. Ele ocupa espaço e dá trabalho a cada mudança. Você vai ver isso na seção 7.


1. O primeiro índice: CREATE INDEX

CREATE INDEX quer dizer "crie um índice". Este cria um índice na coluna cliente_id da tabela pedidos:

CREATE INDEX idx_pedidos_cliente ON pedidos (cliente_id);
CREATE INDEX
"Crie um índice". O comando que monta a lista ordenada.
idx_pedidos_cliente
O nome do índice. É obrigatório e não pode repetir o nome de outro índice, tabela ou view do banco. O costume é idx_ (de index, "índice") + a tabela + a coluna: quem lê o nome já sabe o que o índice guarda.
ON pedidos
"Na tabela pedidos". ON = "em", "sobre".
(cliente_id)
A coluna que o índice vai pôr em ordem, entre parênteses. Pode ser mais de uma, separadas por vírgula (seção 4).

O resultado é só um "ok": criar um índice não devolve linhas. O banco leu os 90 pedidos uma vez, montou a lista ordenada por cliente_id e a guardou no arquivo, ao lado da tabela.

Onde o índice fica guardado

Todo banco SQLite tem uma tabela especial, a sqlite_schema ("esquema do SQLite"), com uma linha para cada tabela, índice e view que existe nele. Dá para consultá-la como qualquer tabela:

CREATE INDEX idx_pedidos_cliente ON pedidos (cliente_id);

SELECT type, name, tbl_name, sql
FROM sqlite_schema
WHERE type = 'index';
type
O tipo do objeto: table, index, view ou trigger. O WHERE type = 'index' deixa só os índices.
name
O nome do índice.
tbl_name
A tabela a que ele pertence.
sql
O comando que o criou, guardado como texto.

Vieram dois índices, e você só criou um. O primeiro, sqlite_autoindex_itens_pedido_1, o SQLite criou sozinho, e a coluna sql dele é NULL porque nenhum CREATE INDEX o criou. Você vai entender de onde ele veio na seção 6.

Experimente: a Ana vive filtrando produtos por categoria. Na linha vazia, crie um índice chamado idx_produtos_categoria na coluna categoria_id da tabela produtos. Rode e veja o seu índice aparecer na lista, ao lado do que o SQLite criou sozinho.

-- crie o índice aqui:


SELECT name, tbl_name
FROM sqlite_schema
WHERE type = 'index';
-- crie o índice aqui:
CREATE INDEX idx_produtos_categoria ON produtos (categoria_id);

SELECT name, tbl_name
FROM sqlite_schema
WHERE type = 'index';

2. O índice não muda o resultado

Esta é a regra mais importante da aula: um índice nunca muda o que a consulta devolve, só como o banco chega lá. Você não precisa citar o índice no SELECT; o próprio banco decide se vale a pena usá-lo. Compare as duas consultas, uma sem e outra com o índice:

SELECT id, data, frete
FROM pedidos
WHERE cliente_id = 3;
CREATE INDEX idx_pedidos_cliente ON pedidos (cliente_id);

SELECT id, data, frete
FROM pedidos
WHERE cliente_id = 3;

As mesmas 4 linhas, na mesma ordem. Com 90 pedidos, a diferença de tempo nem dá para medir. Então como saber se o banco usou o índice? Existe um comando para perguntar ao SQLite qual caminho ele vai seguir: o EXPLAIN QUERY PLAN ("explique o plano da consulta"). Só uma espiada aqui; ele tem aula própria, EXPLAIN QUERY PLAN:

EXPLAIN QUERY PLAN
SELECT id, data, frete FROM pedidos WHERE frete = 0;

CREATE INDEX idx_pedidos_cliente ON pedidos (cliente_id);

EXPLAIN QUERY PLAN
SELECT id, data, frete FROM pedidos WHERE cliente_id = 3;

Olhe só a coluna detail ("detalhe"). Na primeira consulta, o frete não tem índice, e o plano diz SCAN pedidos: vou ler a tabela inteira (folhear o livro). Na segunda, diz SEARCH pedidos USING INDEX idx_pedidos_cliente (cliente_id=?): vou buscar usando o índice, pelo valor de cliente_id (ir ao índice remissivo). As outras colunas são números internos do SQLite e podem ser ignoradas.

No terminal, o programa sqlite3 desenha o mesmo plano em forma de árvore, sem os números:

$ sqlite3 loja.db
SQLite version 3.50.4 2025-07-30 19:33:53
Enter ".help" for usage hints.
sqlite> EXPLAIN QUERY PLAN SELECT id, data, frete FROM pedidos WHERE frete = 0;
QUERY PLAN
`--SCAN pedidos
sqlite> CREATE INDEX idx_pedidos_cliente ON pedidos (cliente_id);
sqlite> EXPLAIN QUERY PLAN SELECT id, data, frete FROM pedidos WHERE cliente_id = 3;
QUERY PLAN
`--SEARCH pedidos USING INDEX idx_pedidos_cliente (cliente_id=?)
sqlite> .quit

Medindo num banco grande

Para sentir a diferença, a Ana fez um teste no terminal, num arquivo à parte (teste.db, para não mexer na loja): uma tabela com um milhão de números. O comando .timer on do sqlite3 mostra quanto tempo cada consulta levou:

$ sqlite3 teste.db
SQLite version 3.50.4 2025-07-30 19:33:53
Enter ".help" for usage hints.
sqlite> CREATE TABLE numeros (n INTEGER, dobro INTEGER);
sqlite> WITH RECURSIVE c(x) AS (SELECT 1 UNION ALL SELECT x + 1 FROM c WHERE x < 1000000) INSERT INTO numeros SELECT x, x * 2 FROM c;
sqlite> .timer on
sqlite> SELECT dobro FROM numeros WHERE n = 777777;
1555554
Run Time: real 0.030 user 0.029716 sys 0.000144         « sem índice: leu 1 milhão de linhas
sqlite> CREATE INDEX idx_numeros_n ON numeros (n);
Run Time: real 0.236 user 0.189177 sys 0.027303         « montar o índice também leva tempo
sqlite> SELECT dobro FROM numeros WHERE n = 777777;
1555554
Run Time: real 0.000 user 0.000060 sys 0.000007         « com índice: menos de 1 milésimo
sqlite> .quit

Não se preocupe com o WITH RECURSIVE: é só um jeito de gerar um milhão de linhas, e ele tem aula própria (CTE Recursiva). O que importa são os tempos: a mesma consulta, com a mesma resposta (1555554), caiu de 30 milésimos de segundo para menos de 1. Parece pouco, mas um site faz essa consulta milhares de vezes por minuto, e numa tabela de 100 milhões de linhas os 30 milésimos virariam 3 segundos. Repare também que criar o índice levou 0,236 s: o banco precisou ler a tabela inteira e pôr tudo em ordem. A aula Quando o Índice Ajuda faz essas medições com calma e mostra quando o índice não ajuda.


3. Vendo os índices de uma tabela

Além da sqlite_schema, o SQLite tem dois comandos PRAGMA ("instrução especial") que respondem perguntas sobre índices. Eles funcionam no editor também:

CREATE INDEX idx_pedidos_cliente ON pedidos (cliente_id);

PRAGMA index_list('pedidos');
PRAGMA index_info('idx_pedidos_cliente');
ComandoPergunta que respondeColunas que importam
PRAGMA index_list('tabela')Quais índices esta tabela tem?name (o nome), unique (1 = não aceita repetidos), origin (c = criado por CREATE INDEX, u = por um UNIQUE, pk = pela chave primária)
PRAGMA index_info('índice')Quais colunas este índice guarda, e em que ordem?seqno (a posição no índice: 0, 1, 2…), name (a coluna)

Os PRAGMAs têm aula própria em PRAGMAs Úteis. No terminal do sqlite3, existe um atalho ainda mais curto, o comando de ponto .indexes (só do programa sqlite3, não do SQL; veja Comandos de Ponto). E o .schema de uma tabela mostra os índices dela embaixo do CREATE TABLE:

$ sqlite3 loja.db
SQLite version 3.50.4 2025-07-30 19:33:53
Enter ".help" for usage hints.
sqlite> .indexes
sqlite_autoindex_itens_pedido_1
sqlite> CREATE INDEX idx_pedidos_cliente ON pedidos (cliente_id);
sqlite> .indexes
idx_pedidos_cliente              sqlite_autoindex_itens_pedido_1
sqlite> .indexes pedidos                       « só os índices da tabela pedidos
idx_pedidos_cliente
sqlite> .schema pedidos
CREATE TABLE pedidos (
    id             INTEGER PRIMARY KEY,
    cliente_id     INTEGER NOT NULL REFERENCES clientes(id),
    funcionario_id INTEGER NOT NULL REFERENCES funcionarios(id),
    data           TEXT NOT NULL,
    entregue_em    TEXT,
    frete          REAL NOT NULL DEFAULT 0
);
CREATE INDEX idx_pedidos_cliente ON pedidos (cliente_id);

Diferente do editor, no terminal o índice fica gravado no arquivo loja.db: na próxima vez que a Ana abrir o banco, ele continua lá. Índice é parte do banco, como as tabelas.


4. Índice composto: várias colunas

A Ana quer os pedidos de um cliente a partir de uma data: "os pedidos da cliente 3 desde 2025". Um índice pode ter mais de uma coluna. Esse é o índice composto:

CREATE INDEX idx_pedidos_cliente_data ON pedidos (cliente_id, data);

SELECT id, cliente_id, data
FROM pedidos
WHERE cliente_id = 3 AND data >= '2025-01-01';
idx_pedidos_cliente_data
O nome, com as duas colunas, na ordem.
(cliente_id, data)
Primeiro o índice ordena por cliente_id. Entre os pedidos do mesmo cliente, ordena por data.

A ordem das colunas importa: a lista telefônica

Pense numa lista telefônica antiga, ordenada por sobrenome e, dentro do mesmo sobrenome, por nome. Ela é ótima para achar "Sousa, Ana" e ótima para achar todos os "Sousa". Mas é inútil para achar todas as pessoas chamadas "Ana": as Anas estão espalhadas pela lista inteira, uma em cada sobrenome. Você teria de ler a lista toda.

Um índice composto funciona igual. O (cliente_id, data) ajuda quando a consulta filtra pela primeira coluna (com ou sem a segunda), mas não ajuda quem filtra só pela segunda. O plano confirma:

CREATE INDEX idx_pedidos_cliente_data ON pedidos (cliente_id, data);

EXPLAIN QUERY PLAN
SELECT id, data, frete FROM pedidos WHERE cliente_id = 3 AND data >= '2025-01-01';

EXPLAIN QUERY PLAN
SELECT id, data, frete FROM pedidos WHERE cliente_id = 3;

EXPLAIN QUERY PLAN
SELECT id, data, frete FROM pedidos WHERE data >= '2025-01-01';
A consulta filtra por…Na lista telefônicaO plano
cliente_id e data"Sousa, Ana"SEARCH … (cliente_id=? AND data>?): usa as duas colunas
só cliente_idtodos os "Sousa"SEARCH … (cliente_id=?): usa a primeira coluna
só datatodas as "Ana"SCAN pedidos: lê a tabela inteira

A regra prática: no índice composto, ponha primeiro a coluna que aparece sempre no WHERE, comparada com =. Um índice (cliente_id, data) não é a mesma coisa que um (data, cliente_id). Por outro lado, ele já faz o trabalho de um índice só de cliente_id, então ter os dois seria desperdício.


5. UNIQUE INDEX: proibido repetir

Às vezes o índice tem uma segunda função: garantir que um valor não se repita. Dois clientes com o mesmo email são um problema (para qual dos dois vai a nota fiscal?). Com CREATE UNIQUE INDEX ("crie um índice único"), o banco passa a recusar qualquer email repetido:

CREATE UNIQUE INDEX idx_clientes_email ON clientes (email);

INSERT INTO clientes (nome, email)
VALUES ('Beatriz Sousa', 'ana.beatriz@exemplo.com.br');
CREATE UNIQUE INDEX
Igual ao CREATE INDEX, com uma promessa a mais: nunca vão existir duas linhas com o mesmo valor nessa coluna.
idx_clientes_email
O nome do índice.
ON clientes (email)
A tabela e a coluna que não podem repetir.

O índice foi criado sem problema, porque hoje nenhum email se repete. Mas o INSERT tentou cadastrar o email ana.beatriz@exemplo.com.br, que já é da cliente 3, e parou com o erro UNIQUE constraint failed: clientes.email: "a restrição de único falhou em clientes.email". Nada foi gravado. O UNIQUE também pode ser escrito direto no CREATE TABLE, como restrição da coluna; isso tem aula própria, UNIQUE. Por baixo, as duas formas dão no mesmo: um índice único.

E o NULL?

Três clientes da loja estão sem email (NULL): os clientes 7, 15 e 23. Mesmo assim, o índice único foi criado. Para o SQLite, um NULL não é igual a outro NULL ("valor desconhecido" não é igual a "valor desconhecido", veja O Valor NULL), então vários NULL convivem num índice único. Dá até para cadastrar mais um cliente sem email:

CREATE UNIQUE INDEX idx_clientes_email ON clientes (email);

INSERT INTO clientes (nome, email) VALUES ('Beatriz Sousa', NULL);

SELECT COUNT(*) AS sem_email
FROM clientes
WHERE email IS NULL;

Agora são 4 clientes sem email, e o índice único não reclamou. PostgreSQL e MySQL fazem igual; o SQL Server é diferente (veja a nota "Nos outros bancos", mais abaixo).

Missão: o estoquista cadastrou de novo o Lápis Grafite HB, que já existe (é o produto 3). Hoje o banco aceita, e a loja fica com dois produtos iguais. Antes do INSERT, crie um índice único chamado idx_produtos_nome na coluna nome de produtos, para que o banco recuse o cadastro repetido. A missão passa quando o INSERT der o erro de UNIQUE.

-- o índice único vem aqui:


INSERT INTO produtos (nome, categoria_id, fornecedor_id, preco)
VALUES ('Lápis Grafite HB', 1, 6, 1.20);
-- o índice único vem aqui:
CREATE UNIQUE INDEX idx_produtos_nome ON produtos (nome);

INSERT INTO produtos (nome, categoria_id, fornecedor_id, preco)
VALUES ('Lápis Grafite HB', 1, 6, 1.20);

Índice único composto

O UNIQUE também vale para várias colunas juntas: aí o que não pode repetir é a combinação. Um índice único em (cidade, nome) de clientes deixaria existir duas "Ana Lima", desde que em cidades diferentes. É o caso da tabela itens_pedido: o mesmo produto aparece em vários pedidos, e cada pedido tem vários produtos, mas o par (pedido, produto) nunca se repete. Como o SQLite garante isso? Com um índice que ele criou sozinho, o assunto da próxima seção.


6. Apagando índices e os índices automáticos

DROP INDEX

Um índice que não ajuda só dá trabalho. Para apagá-lo, use DROP INDEX ("descarte o índice") com o nome dele:

CREATE INDEX idx_pedidos_cliente ON pedidos (cliente_id);

DROP INDEX idx_pedidos_cliente;

SELECT name FROM sqlite_schema WHERE type = 'index';
DROP INDEX
Apaga o índice. Só o índice: a tabela e os dados continuam intactos. As consultas também continuam funcionando e dando o mesmo resultado, só que sem o atalho.
idx_pedidos_cliente
O nome do índice. No SQLite, não precisa dizer de que tabela ele é, porque o nome é único no banco inteiro.

Apagar um índice é uma operação segura: se você se arrepender, basta criar de novo. Bem diferente de DROP TABLE, que leva os dados junto (aula DROP TABLE).

IF NOT EXISTS e IF EXISTS

Criar um índice que já existe é erro; apagar um que não existe também (você vai ver as mensagens em Quando dá errado). Em scripts que rodam várias vezes, use as formas "se":

CREATE INDEX IF NOT EXISTS idx_pedidos_cliente ON pedidos (cliente_id);
CREATE INDEX IF NOT EXISTS idx_pedidos_cliente ON pedidos (cliente_id);

DROP INDEX IF EXISTS idx_que_nunca_existiu;

Os três comandos deram "ok". O segundo CREATE viu que o índice já existia e não fez nada; o DROP viu que o índice não existia e também não fez nada. Nenhum erro.

Os índices que o SQLite cria sozinho

Lembra do sqlite_autoindex_itens_pedido_1? A tabela itens_pedido foi criada com PRIMARY KEY (pedido_id, produto_id), uma chave primária de duas colunas. Para garantir que o par não se repita (e achar um item depressa), o SQLite montou um índice único automático. O mesmo acontece com toda coluna marcada como UNIQUE no CREATE TABLE.

E as outras tabelas, que têm id INTEGER PRIMARY KEY? Elas não ganharam índice nenhum, e nem precisam: no SQLite, a coluna INTEGER PRIMARY KEY é a própria ordem em que a tabela é guardada. Buscar WHERE id = 42 já é rápido sem índice (veja PRIMARY KEY).

Os índices automáticos não podem ser apagados com DROP INDEX, porque a chave primária depende deles:

DROP INDEX sqlite_autoindex_itens_pedido_1;

"Um índice ligado a uma restrição UNIQUE ou PRIMARY KEY não pode ser apagado." Para tirar esse índice, seria preciso tirar a restrição da tabela. Pela mesma razão, você não pode dar a um índice seu um nome começado por sqlite_: esses nomes são reservados para o próprio SQLite.


7. O preço de um índice: quando criar

Se o índice deixa as buscas tão rápidas, por que não criar um índice em cada coluna? Porque ele custa:

  • Espaço: o índice é uma cópia ordenada da coluna (mais o "endereço" de cada linha). Num banco grande, os índices podem ocupar tanto quanto os dados.
  • Escritas mais lentas: cada INSERT, UPDATE ou DELETE precisa atualizar a tabela e cada índice dela. É como um livro em que cada receita nova obriga a corrigir o índice remissivo. Uma tabela com dez índices recebe dados bem mais devagar que uma com um.
  • Tempo de criação: criar o índice lê a tabela inteira (você viu os 0,236 s no teste do milhão).
Vale a pena um índice na coluna que…Exemplo na Loja Aurora
aparece muito no WHERE para achar poucas linhasWHERE cliente_id = 3, WHERE email = '…'
liga tabelas num JOIN (a chave estrangeira)pedidos.cliente_id, itens_pedido.produto_id (veja INNER JOIN)
aparece muito no ORDER BY de tabelas grandesORDER BY data num histórico de milhões de pedidos
não pode repetiremail do cliente, nome do produto (UNIQUE INDEX)
Não vale a pena quando…Por quê
a tabela é pequena (dezenas ou centenas de linhas)Ler tudo já é instantâneo; as 8 categorias da loja nunca vão precisar de índice.
a coluna tem poucos valores diferentesvip é 0 ou 1: o índice "acharia" metade da tabela, e ler tudo sai quase igual.
a tabela recebe muito mais escritas que leiturasUm registro de log, por exemplo: cada índice atrasa cada gravação.
a coluna já é a primeira de outro índice(cliente_id) sobra se já existe (cliente_id, data).

Não decore essas tabelas: na dúvida, meça. A aula Quando o Índice Ajuda ensina a usar o EXPLAIN QUERY PLAN e o .timer para decidir com números.


8. Formas diferentes de fazer

Há mais de um jeito de conseguir um índice, e o SQLite tem duas variações que valem conhecer de nome:

-- 1. no CREATE TABLE, com UNIQUE (o índice vem junto, automático)
CREATE TABLE cupons (codigo TEXT UNIQUE, desconto REAL);

-- 2. índice parcial: só as linhas que passam no WHERE
CREATE INDEX idx_pedidos_pendentes ON pedidos (data)
WHERE entregue_em IS NULL;

-- 3. índice numa expressão: a busca por lower(email) passa a usar o índice
CREATE INDEX idx_clientes_email_min ON clientes (lower(email));

SELECT name, tbl_name FROM sqlite_schema WHERE type = 'index';

O cupons ganhou o índice sqlite_autoindex_cupons_1 sem nenhum CREATE INDEX. O índice parcial guarda só os 5 pedidos não entregues: pequeno, e perfeito para a tela "pedidos pendentes". O índice em expressão guarda o resultado de lower(email) (o email em minúsculas; veja Funções de Texto), para buscas que ignoram maiúsculas.

FormaQuando usar
CREATE INDEX nome ON t (col)O caso comum: acelerar buscas por uma coluna.
CREATE INDEX nome ON t (col1, col2)Consultas que filtram sempre pela col1 e, às vezes, também pela col2.
CREATE UNIQUE INDEX nome ON t (col)Proibir valores repetidos numa tabela que já existe.
UNIQUE no CREATE TABLEProibir repetidos desde o começo, ao criar a tabela (aula UNIQUE).
CREATE INDEX … WHERE … (parcial)Quando você só busca um pedaço pequeno da tabela, como os pedidos pendentes.
CREATE INDEX … (lower(col)) (expressão)Quando o WHERE sempre aplica a mesma função à coluna.

Quando dá errado

Os erros de índice são poucos e as mensagens são claras. Em cada quadro abaixo, o SQLite do seu navegador roda o SQL errado e mostra a mensagem real.

Criou o mesmo índice duas vezes
CREATE INDEX idx_pedidos_cliente ON pedidos (cliente_id);
CREATE INDEX idx_pedidos_cliente ON pedidos (cliente_id);

O que aconteceu: o primeiro deu "ok"; o segundo parou com index idx_pedidos_cliente already exists, "o índice idx_pedidos_cliente já existe". No terminal, a mesma mensagem vem com o prefixo Parse error::

$ sqlite3 loja.db
SQLite version 3.50.4 2025-07-30 19:33:53
Enter ".help" for usage hints.
sqlite> CREATE INDEX idx_pedidos_cliente ON pedidos (cliente_id);
sqlite> CREATE INDEX idx_pedidos_cliente ON pedidos (cliente_id);
Parse error: index idx_pedidos_cliente already exists

Como consertar: se o índice já está lá, não há nada a fazer. Num script que roda mais de uma vez, use CREATE INDEX IF NOT EXISTS.

Nome de tabela errado
CREATE INDEX idx_pedidos_cliente ON pedido (cliente_id);

O que aconteceu: no such table: main.pedido, "não existe a tabela main.pedido". A tabela se chama pedidos, no plural. O main. é o nome do banco principal: o SQLite diz em qual banco procurou.

Como consertar: confira o nome da tabela (no terminal, .tables).

Nome de coluna errado
CREATE INDEX idx_pedidos_cliente ON pedidos (cliente);

O que aconteceu: no such column: cliente, "não existe a coluna cliente". A coluna é cliente_id.

Como consertar: confira as colunas com .schema pedidos no terminal, ou com SELECT * FROM pedidos LIMIT 1 no editor.

Índice único numa coluna que já tem repetidos
CREATE UNIQUE INDEX idx_clientes_cidade ON clientes (cidade);

O que aconteceu: UNIQUE constraint failed: clientes.cidade. A loja tem 10 clientes de Teresina; um índice que promete "cidade nunca repete" não pode nascer numa tabela onde ela já repete. O índice não foi criado.

Como consertar: se a coluna pode repetir (como cidade), use um CREATE INDEX comum. Se não pode (como email), primeiro ache os repetidos com GROUP BY e HAVING COUNT(*) > 1 (aula HAVING), corrija os dados e só então crie o índice único.

Esqueceu o nome do índice
CREATE INDEX ON pedidos (cliente_id);

O que aconteceu: near "ON": syntax error, "erro de sintaxe perto de ON". O SQLite esperava o nome do índice e encontrou o ON. O PostgreSQL aceita índice sem nome (ele inventa um); o SQLite, não.

Como consertar: dê um nome: CREATE INDEX idx_pedidos_cliente ON pedidos (cliente_id);.

Esqueceu os parênteses
CREATE INDEX idx_pedidos_cliente ON pedidos cliente_id;

O que aconteceu: near "cliente_id": syntax error. Depois do nome da tabela, o SQLite espera um ( com a lista de colunas.

Como consertar: as colunas vão sempre entre parênteses, mesmo quando é uma só: ON pedidos (cliente_id).

Deu ao índice o nome de uma tabela
CREATE INDEX pedidos ON pedidos (cliente_id);

O que aconteceu: there is already a table named pedidos, "já existe uma tabela chamada pedidos". Tabelas, índices e views dividem a mesma lista de nomes no banco.

Como consertar: use o prefixo idx_: além de evitar o choque, deixa claro o que é índice.

Apagou um índice que não existe
DROP INDEX idx_pedidos_cliente;

O que aconteceu: no such index: idx_pedidos_cliente, "não existe o índice". Aqui o banco está limpo e o índice nunca foi criado. Acontece também quando o nome foi digitado errado.

Como consertar: confira o nome com .indexes (terminal) ou na sqlite_schema. Em scripts, DROP INDEX IF EXISTS.

Missão: este script tem dois erros e para no primeiro. Conserte o nome da tabela e o nome da coluna (a tabela é pedidos, a coluna é cliente_id) para o índice ser criado e aparecer na lista, junto do automático.

CREATE INDEX idx_pedidos_cliente ON pedido (cliente);

SELECT name FROM sqlite_schema WHERE type = 'index';
CREATE INDEX idx_pedidos_cliente ON pedidos (cliente_id);

SELECT name FROM sqlite_schema WHERE type = 'index';
Nos outros bancos

O CREATE INDEX nome ON tabela (colunas) e o CREATE UNIQUE INDEX são escritos do mesmo jeito no MySQL, no PostgreSQL e no SQL Server. A diferença maior está no DROP INDEX: no MySQL e no SQL Server, o nome do índice só precisa ser único dentro da tabela, então é preciso dizer de qual tabela ele é. Trechos ilustrativos, que não rodam no SQLite:

-- PostgreSQL: igual ao SQLite
DROP INDEX idx_pedidos_cliente;

-- MySQL e SQL Server: com ON tabela
DROP INDEX idx_pedidos_cliente ON pedidos;

No SQL Server, um índice único deixa entrar só um NULL na coluna; para aceitar vários, como o SQLite, é preciso um índice filtrado (WHERE email IS NOT NULL). E os bancos com servidor têm tipos de índice que o SQLite não tem (como os índices clustered do SQL Server e os vários métodos do PostgreSQL). O básico desta aula vale em todos.


Referência rápida

ComandoO que faz
CREATE INDEX nome ON tabela (coluna);Cria um índice numa coluna
CREATE INDEX nome ON tabela (col1, col2);Índice composto: ordena por col1 e, empatando, por col2
CREATE UNIQUE INDEX nome ON tabela (coluna);Índice que também proíbe valores repetidos (vários NULL são aceitos)
CREATE INDEX IF NOT EXISTS …Cria só se ainda não existir; sem erro
CREATE INDEX nome ON tabela (col) WHERE …;Índice parcial: só as linhas do WHERE
DROP INDEX nome;Apaga o índice (os dados ficam)
DROP INDEX IF EXISTS nome;Apaga só se existir; sem erro
SELECT name, tbl_name FROM sqlite_schema WHERE type = 'index';Lista os índices do banco
PRAGMA index_list('tabela');Os índices de uma tabela
PRAGMA index_info('índice');As colunas de um índice, na ordem
.indexes / .indexes tabelaNo terminal do sqlite3: lista os índices
EXPLAIN QUERY PLAN SELECT …Mostra se a consulta usa índice (SEARCH) ou lê tudo (SCAN)
Mensagem do SQLiteQuer dizer
index X already existsJá existe um índice com esse nome
no such table: main.XA tabela não existe (nome errado?)
no such column: XA coluna não existe nessa tabela
UNIQUE constraint failed: tabela.colunaValor repetido numa coluna com índice único (ou ao criar o índice numa coluna que já repete)
no such index: XO DROP INDEX não achou o índice
there is already a table named XO nome do índice é o de uma tabela
index associated with UNIQUE or PRIMARY KEY constraint cannot be droppedTentou apagar um índice automático
object name reserved for internal use: sqlite_…Nomes começados por sqlite_ são do próprio SQLite

Exercícios

1. Complete o comando

Crie um índice chamado idx_clientes_cidade na coluna cidade da tabela clientes:

idx_clientes_cidade clientes (cidade);

2. Apague o índice

Complete para apagar o índice idx_clientes_cidade:

idx_clientes_cidade;

3. A Ana criou um índice em pedidos (cliente_id). O que acontece com o resultado de SELECT * FROM pedidos WHERE cliente_id = 3?

4. Existe um índice em pedidos (cliente_id, data). Qual destas consultas não consegue usá-lo para buscar?

5. A Ana rodou CREATE UNIQUE INDEX idx_clientes_cidade ON clientes (cidade);. O que acontece?

6. Em qual destas colunas um índice menos ajudaria?

Missão final: o índice dos vendedores

Missão: todo fim de mês, cada vendedor confere os próprios pedidos do período, com WHERE funcionario_id = ? AND data BETWEEN ? AND ?. Crie o índice composto idx_pedidos_func_data na tabela pedidos, com as colunas certas na ordem certa: primeiro a que é comparada com =, depois a do intervalo. O PRAGMA no fim mostra as colunas do índice, na ordem; enquanto o índice não existir, ele não mostra nada.

-- crie o índice aqui:


SELECT name FROM pragma_index_info('idx_pedidos_func_data');
-- crie o índice aqui:
CREATE INDEX idx_pedidos_func_data ON pedidos (funcionario_id, data);

SELECT name FROM pragma_index_info('idx_pedidos_func_data');

Repare na última linha: pragma_index_info(…) é o mesmo PRAGMA index_info, escrito como se fosse uma tabela. Assim dá para escolher só a coluna name. Se você inverteu a ordem das colunas, a saída mostra data em cima, e a missão não passa: apague com DROP INDEX (ou só corrija o CREATE, já que cada Rodar parte do banco limpo).


Resumo

  • Um índice é uma lista à parte, sempre em ordem, com os valores de uma ou mais colunas e onde estão as linhas: o índice remissivo do livro. Sem ele, o banco lê a tabela inteira (SCAN); com ele, vai direto às linhas certas (SEARCH).
  • CREATE INDEX nome ON tabela (coluna); cria; DROP INDEX nome; apaga (só o índice, nunca os dados). Use IF NOT EXISTS e IF EXISTS em scripts.
  • O índice nunca muda o resultado, só a velocidade. O próprio banco decide quando usá-lo.
  • No índice composto, a ordem importa: (cliente_id, data) ajuda quem filtra por cliente_id, mas não quem filtra só por data, como uma lista telefônica ordenada por sobrenome.
  • CREATE UNIQUE INDEX proíbe valores repetidos (UNIQUE constraint failed), mas aceita vários NULL.
  • O SQLite cria sozinho índices para UNIQUE e chaves primárias compostas (sqlite_autoindex_…), e não deixa apagá-los.
  • Índice custa espaço e deixa as escritas mais lentas. Crie nas colunas muito buscadas e nas de JOIN, não em todas. Na dúvida, meça: Quando o Índice Ajuda.