Índices e transações

Escolha de índices, planos de consulta, propriedades ACID, commit, rollback e savepoints.

TLDR

Perguntar sobre esta página

ChatGPTClaudePerplexityGrokGeminiCopiar e abrirDeepSeekCopiar e abrir

A loja procura as encomendas de Ana num intervalo de datas. Um índice pode localizar essas linhas sem percorrer toda a tabela.

Ao transferir dinheiro entre contas, a preocupação é outra: debitar e creditar em conjunto. Uma transação define essa unidade de alteração, para a confirmar ou desfazer.

O que um índice guarda

Um índice é uma estrutura persistente que relaciona valores de uma chave de pesquisa com a localização das linhas. Não muda a resposta lógica de uma consulta.

O motor pode usá-lo para encontrar correspondências, percorrer intervalos ou obter uma ordem útil sem ordenar tudo.

Numa árvore equilibrada, a procura da primeira correspondência costuma crescer com o logaritmo do número de entradas; depois há o custo de percorrer e recuperar as correspondências. Uma tabela hash é adequada a igualdade, mas não fornece diretamente a mesma ordenação para intervalos. SQLite usa índices baseados em árvores, não uma opção USING HASH no comando CREATE INDEX.

Não confundas chave de pesquisa do índice com chave candidata. Podemos indexar stock apesar de vários produtos terem o mesmo stock.

CREATE UNIQUE INDEX acrescenta também uma regra de unicidade. Um índice comum só acrescenta um método de acesso.

Índices compostos

Para consultar encomendas de um cliente num intervalo de datas, um índice composto pode ser útil:

CREATE INDEX idx_encomenda_cliente_data
ON Encomenda(idCliente, data);

As entradas ficam ordenadas primeiro por cliente e, dentro dele, por data. A igualdade no primeiro atributo permite procurar o intervalo relevante no segundo. O índice também pode ajudar consultas apenas por cliente. Uma procura apenas por data não beneficia da mesma forma desse prefixo, embora certos planos e distribuições permitam outras estratégias do otimizador.

Com os dados da loja, as entradas de (idCliente, data) ficam assim ordenadas:

idClientedataencomenda
12026-01-05100
12026-01-06101
22026-01-06102

Fixar o cliente 1 isola as duas primeiras entradas; o intervalo de datas percorre só esse bloco.

A ordem (data, idCliente) tem comportamento diferente. Se procurarmos um intervalo amplo de datas, a igualdade do cliente já não restringe da mesma maneira um único intervalo ordenado. Escolhe a ordem pelo conjunto de consultas mais frequentes e pelas distribuições reais.

Índices também ajudam junções, por exemplo encontrar as encomendas que referenciam um cliente. SQLite não cria automaticamente um índice para cada estrangeira. As primárias e unicidades já têm mecanismos de acesso próprios, com a particularidade de INTEGER PRIMARY KEY corresponder ao rowid.

Ler um plano

Ficheiros do exemplo
Plano antes e depois de um índice composto
EXPLAIN QUERY PLAN
SELECT id, data FROM Encomenda
WHERE idCliente = 1 AND data >= '2026-01-01';
CREATE INDEX idx_encomenda_cliente_data ON Encomenda(idCliente, data);
EXPLAIN QUERY PLAN
SELECT id, data FROM Encomenda
WHERE idCliente = 1 AND data >= '2026-01-01';
PRAGMA foreign_keys = ON;
CREATE TABLE Cliente (
  id INTEGER PRIMARY KEY,
  nome TEXT NOT NULL,
  email TEXT UNIQUE
);
CREATE TABLE Produto (
  id INTEGER PRIMARY KEY,
  nome TEXT NOT NULL,
  precoCentimos INTEGER NOT NULL CHECK (precoCentimos >= 0),
  stock INTEGER NOT NULL CHECK (stock >= 0)
);
CREATE TABLE Encomenda (
  id INTEGER PRIMARY KEY,
  data TEXT NOT NULL,
  idCliente INTEGER NOT NULL REFERENCES Cliente(id)
);
CREATE TABLE Item (
  idEncomenda INTEGER NOT NULL REFERENCES Encomenda(id) ON DELETE CASCADE,
  idProduto INTEGER NOT NULL REFERENCES Produto(id),
  qtd INTEGER NOT NULL CHECK (qtd > 0),
  precoUnitario INTEGER NOT NULL CHECK (precoUnitario >= 0),
  PRIMARY KEY (idEncomenda, idProduto)
);
INSERT INTO Cliente VALUES
  (1, 'Ana', 'ana@example.test'),
  (2, 'Rui', NULL),
  (3, 'Mia', NULL);
INSERT INTO Produto VALUES
  (10, 'Teclado', 4500, 20),
  (11, 'Rato', 2500, 50),
  (12, 'Monitor', 18000, 5);
INSERT INTO Encomenda VALUES
  (100, '2026-01-05', 1),
  (101, '2026-01-06', 1),
  (102, '2026-01-06', 2);
INSERT INTO Item VALUES
  (100, 10, 2, 4500),
  (100, 11, 1, 2500),
  (101, 12, 1, 18000),
  (102, 11, 3, 2500);
Resultado

Executa a consulta para ver as tabelas.

O primeiro plano percorre Encomenda e o segundo pode procurar usando o índice, por isso confere sobretudo SCAN, SEARCH e o nome do índice.

Um índice cobridor contém os valores de que a consulta precisa, permitindo evitar acessos adicionais à tabela em certos planos. Nos índices comuns de uma tabela SQLite com rowid, esse identificador também está nas entradas; isso ajuda a devolver o id de uma INTEGER PRIMARY KEY.

O custo dos índices

Cada inserção, eliminação e alteração dos atributos indexados precisa de manter os índices. Eles ocupam espaço e têm custo de criação. Se uma condição devolve quase toda a tabela, um percurso completo pode ser mais barato que muitas visitas por índice.

A seletividade indica quanto a condição restringe os dados. Uma coluna com poucos valores pode ser útil ou inútil conforme a frequência dos valores procurados e o plano. “Há dois valores distintos” não chega para decidir: um deles pode corresponder a uma fração muito pequena das linhas.

O otimizador usa estatísticas para estimar custos. ANALYZE recolhe estatísticas em SQLite. A escolha final depende do tamanho, da distribuição, dos índices e da carga de leitura e escrita. Criar um índice em todas as colunas não é uma estratégia de desenho.

Uma transação e as propriedades ACID

Uma transação agrupa operações que têm de formar uma unidade. Transferir 25 euros exige debitar uma conta e creditar outra. Uma falha entre as duas operações não pode deixar uma transferência a meio.

  • Atomicidade: as alterações da transação são confirmadas em conjunto ou desfeitas.
  • Consistência: a transação leva um estado válido a outro válido segundo as regras do domínio.
  • Isolamento: define o que transações concorrentes podem observar e como interferem.
  • Durabilidade: as alterações confirmadas sobrevivem às falhas abrangidas pelas garantias e configuração do sistema.

O SGBD verifica as restrições declaradas, mas não adivinha todas as regras. Uma transação que debita duas contas sem creditar nenhuma pode ser atómica e ainda estar errada. Consistência exige código correto e restrições adequadas.

BEGIN, COMMIT e ROLLBACK

Em SQLite, fora de uma transação explícita, as instruções decorrem em transações implícitas. BEGIN inicia uma transação explícita, COMMIT confirma e ROLLBACK desfaz as alterações dessa transação.

Transferência desfeita e depois confirmada
CREATE TABLE Conta(id INTEGER PRIMARY KEY,
saldo INTEGER NOT NULL CHECK (saldo >= 0));
INSERT INTO Conta VALUES (1, 10000), (2, 5000);
BEGIN;
UPDATE Conta SET saldo = saldo - 2500 WHERE id = 1;
UPDATE Conta SET saldo = saldo + 2500 WHERE id = 2;
ROLLBACK;
SELECT id, saldo FROM Conta ORDER BY id;
BEGIN;
UPDATE Conta SET saldo = saldo - 2500 WHERE id = 1;
UPDATE Conta SET saldo = saldo + 2500 WHERE id = 2;
COMMIT;
SELECT id, saldo FROM Conta ORDER BY id;
Resultado

Executa a consulta para ver as tabelas.

Os estados das contas na transferência de 2500 cêntimos:

MomentoConta 1Conta 2Confirmado
início100005000nenhuma
dentro da transação75007500ainda não
após ROLLBACK100005000não
após COMMIT (nova tentativa)75007500sim

Depois do rollback, os saldos voltam ao início e depois do commit refletem a transferência, porque a primeira transferência não deixou efeitos guardados.

O código pressupõe que as duas contas existem e que há saldo suficiente. Um UPDATE de uma conta inexistente altera zero linhas e não dá automaticamente erro. Uma aplicação deve conferir o número de linhas afetadas e só confirmar quando todas as condições da transferência estiverem cumpridas.

Um erro não faz sempre rollback de tudo

Em SQLite, uma violação de CHECK com a política por omissão costuma abortar a instrução, mantendo anteriores instruções da transação. Se o primeiro débito ocorreu e a segunda operação falhou, não assumes que o débito já foi desfeito. Trata o erro e decide fazer rollback da transação inteira.

PostgreSQL tem outro comportamento: um erro deixa normalmente a transação em estado abortado até rollback, ou recuperação para um savepoint. A distinção importa ao ler código de aplicação e ao prever resultados.

Savepoints

Um savepoint marca um ponto de recuperação dentro de uma transação:

Desfazer apenas a segunda alteração
CREATE TABLE Valor(id INTEGER PRIMARY KEY, n INTEGER NOT NULL);
INSERT INTO Valor VALUES (1, 10);
BEGIN;
UPDATE Valor SET n = n + 5 WHERE id = 1;
SAVEPOINT tentativa;
UPDATE Valor SET n = n * 2 WHERE id = 1;
ROLLBACK TO tentativa;
RELEASE tentativa;
COMMIT;
SELECT n FROM Valor;
Resultado

Executa a consulta para ver as tabelas.

A primeira atualização dá 15. A segunda dá 30, mas ROLLBACK TO regressa a 15. RELEASE retira o marcador; o COMMIT exterior confirma 15. Libertar um savepoint interior não equivale a tornar as suas alterações definitivamente duráveis antes do commit exterior.

Falhas e duração

Um SGBD pode usar registos de escrita, journaling ou WAL para recuperar estados após falhas. Atomicidade desfaz trabalho não confirmado; durabilidade conserva trabalho confirmado dentro das condições assumidas. A recuperação não substitui cópias de segurança contra eliminação deliberada, corrupção fora dessas garantias ou perda de todos os suportes.

Mantém transações curtas. Pedir confirmação humana enquanto a transação está aberta pode conservar bloqueios por muito tempo. A página de concorrência explica por que atomicidade, por si só, não impede leituras inconsistentes nem decisões simultâneas erradas.

Exercícios

Escolher a ordem de um índice

A consulta frequente é WHERE idCliente=7 AND data BETWEEN duas datas. Compara os índices (idCliente,data) e (data,idCliente), sem assumir distribuições adicionais. Justifica o efeito da ordem das colunas.

Pista

Um índice composto ordena primeiro pela primeira coluna.

Solução

(idCliente,data) é a escolha natural para este padrão: a igualdade fixa o prefixo e o intervalo atua nas datas desse cliente. (data,idCliente) organiza primeiro todas as encomendas por data, podendo exigir percorrer clientes diferentes no intervalo. Não há uma regra universal de que o primeiro será sempre mais rápido: tamanho, distribuição, cobertura e outras consultas influenciam o plano. Confere EXPLAIN QUERY PLAN e a carga real.

Confere a tua resposta:

  • Justifiquei igualdade no prefixo e intervalo seguinte.
  • Indiquei as condições que ainda influenciam a escolha.

Erros frequentes

Não escolhas por ordem das condições no WHERE; o otimizador não depende dessa ordem textual. Um índice também tem custo de manutenção.

Voltar à explicação
Rollback parcial

Um valor começa em 10. Dentro de BEGIN, somamos 5, criamos SAVEPOINT s, multiplicamos por 2, fazemos ROLLBACK TO s, RELEASE s e COMMIT. Qual o valor final?

Pista
O savepoint foi criado depois da soma.
Solução

O valor final é 15. A soma alterou 10 para 15 antes do marcador. A multiplicação deu 30, mas foi desfeita ao regressar ao savepoint. RELEASE retirou o marcador e o COMMIT exterior confirmou 15. Um ROLLBACK da transação inteira teria reposto 10.

Erros frequentes

Não confundas RELEASE de um savepoint interior com confirmação durável da transação exterior.

Voltar à explicação
Um erro desfez quanto trabalho?

Em SQLite, Conta(id,saldo) tem saldo NOT NULL CHECK(saldo >= 0). A conta 1 começa com 100. Executamos BEGIN, um UPDATE que põe o saldo a 80 e outro UPDATE que o põe a -1. A aplicação apanha a violação de CHECK, usando a política ABORT por omissão. Qual o saldo dentro da transação? E depois de ROLLBACK? A transação terminou automaticamente com o erro?

Pista

ABORT desfaz a instrução, não as anteriores da mesma transação.

Solução

Após o erro, o saldo continua 80 dentro da transação, que permanece aberta. A escrita de -1 foi desfeita. ROLLBACK repõe 100 e termina a transação. Se a aplicação fizer COMMIT após apanhar o erro, pode confirmar 80. Numa operação que exige sucesso de todos os passos, deve fazer rollback da unidade completa em vez de assumir que o motor já o fez.

Confere a tua resposta:

  • Distingui o estado após a instrução falhada do estado após ROLLBACK.
  • Expliquei o tratamento necessário antes de confirmar.

Erros frequentes

Não generalizes a política ABORT a ROLLBACK, FAIL ou a todos os tipos de erro. PostgreSQL normalmente deixa a transação em estado abortado após erro.

Voltar à explicação
Interpretar um plano observado

Para SELECT id,data FROM Encomenda WHERE idCliente=1 AND data>='2026-01-01', foram observados estes planos SQLite:

Antes:  SCAN Encomenda
Depois: SEARCH Encomenda USING COVERING INDEX idx_encomenda_cliente_data
        (idCliente=? AND data>?)

O índice foi criado em (idCliente,data); id é INTEGER PRIMARY KEY. Explica a procura e a palavra COVERING. Estes planos provam que a segunda consulta é mais rápida para qualquer volume de dados?

Pista
O plano diz onde procurar e se é necessário recuperar colunas na tabela.
Mais uma pista
Nos índices desta tabela com rowid, o id também está disponível como identificador da linha.
Solução

Antes, o motor percorre Encomenda e testa o filtro. Depois, fixa idCliente no prefixo do índice e percorre o intervalo de data. COVERING indica que os valores pedidos podem ser obtidos no índice: data é parte da chave de pesquisa e id é o rowid desta tabela.

Os planos não medem tempo. A escolha depende de dados, estatísticas, versão e consultas concorrentes. É necessário medir com uma carga representativa para comparar desempenho; um índice também custa espaço e manutenção nas escritas.

Confere a tua resposta:

  • Identifiquei o percurso completo e a procura pelo índice.
  • Expliquei o prefixo e a cobertura.
  • Distingui plano escolhido de medição de tempo.

Erros frequentes

SCAN não é sempre um erro. Uma consulta que devolve quase toda a tabela pode justificar um percurso completo.
Voltar à explicação

À tua maneira

Aparência

Modo de cor
Tema
Cor de destaque

Leitura

Fonte de leitura

Álgebra, lógica e uma ideia de cada vez.

Código

Fonte do código

Navegação

Cursor de leitura, sem edição. h, j, k, l movem; / pesquisa nesta página. v seleciona caracteres; V seleciona linhas; y copia. Tab permite destacar ou comentar. Espaço ou Enter abre blocos recolhíveis. Nos cartões, h, j, k, l mudam o foco.

CSS personalizado

Ativa uma sugestão ou guarda o teu CSS. Só muda este navegador.

Usa --page, --text e --accent para acompanhar o tema.

Seletores e recuperação

.site-header, .prose, .course-card, [data-reading-history], [data-playground] e .page-actions.

Abre a aparência com a tecla vírgula ou no rodapé. Se o CSS esconder os controlos, abre esta página sem CSS personalizado.

Pesquisar

Escreve para pesquisar em todo o site.

para escolher · Enter para abrir · Esc para fechar

Copiar fórmula

A cópia automática não está disponível. Copia o LaTeX abaixo.

Pesquisar nesta página

Enter procura · n / N repetem · Esc cancela. Ctrl / Cmd K pesquisa todo o site.

Atalhos de teclado

Clica numa tecla para a mudar. Esc cancela. Backspace desativa.

PesquisarCtrl / Cmd K

Os atalhos não interferem enquanto escreves. Tab e Enter funcionam sempre.