EXPLAIN QUERY PLAN mostra como sua consulta vai rodar
Antes de sair otimizando uma query lenta, você precisa entender o que o SQLite está realmente fazendo por baixo dos panos. O EXPLAIN QUERY PLAN exibe um resumo curto da estratégia que o planejador escolheu: quais tabelas serão acessadas, em que ordem e quais índices (se houver) vão entrar em ação. A consulta em si não chega a ser executada — você recebe apenas o plano de execução.
Basta colocar essas palavras-chave na frente de qualquer comando:
O resultado fica mais ou menos assim:
QUERY PLAN
`--SEARCH users USING INDEX sqlite_autoindex_users_1 (email=?)
Essa única linha já diz muita coisa: o SQLite está fazendo um SEARCH (e não um scan) na tabela users, usando o índice único criado automaticamente para email, com email como chave de busca. Exatamente o que a gente espera ver.
SCAN vs SEARCH no SQLite: o primeiro sinal a observar
Toda linha do plano começa com SCAN ou SEARCH. Essa diferença é o sinal mais importante de toda a saída do EXPLAIN QUERY PLAN.
SCAN <tabela>— o SQLite percorre todas as linhas da tabela (ou todas as entradas de um índice). O custo cresce junto com o tamanho da tabela.SEARCH <tabela> USING ...— o SQLite vai direto às linhas que interessam, usando um índice ou a chave primária. O custo cresce com o tamanho do resultado, não da tabela.
Veja a comparação lado a lado. Uma coluna tem índice, a outra não:
O primeiro plano mostra SEARCH orders USING INDEX idx_orders_customer. Já o segundo traz SCAN orders — como não existe índice em status, o SQLite acaba lendo linha por linha. Numa tabela pequena isso passa batido; numa tabela com um milhão de registros, é a diferença entre milissegundos e segundos.
Mas atenção: nem todo SCAN é problema. Em tabelas de apoio bem pequenas, ou em consultas que de fato precisam retornar a maior parte das linhas, varrer tudo é o plano certo. O sinal de alerta aparece quando você tem uma tabela grande com um filtro seletivo — aí sim, SCAN é o aviso de que falta um índice.
Como confirmar se o índice está sendo usado no SQLite
A expressão que você quer encontrar no plano é USING INDEX <nome> (ou USING COVERING INDEX <nome>, que veremos daqui a pouco). Se você criou um índice na esperança de que o planejador fosse aproveitá-lo, é assim que se confere:
Você deve ver algo como SEARCH events USING INDEX idx_events_user (user_id=?). Se em vez disso aparecer SCAN events, é porque alguma coisa está impedindo o planejador de usar o índice — os culpados mais comuns são envolver a coluna em uma função (WHERE lower(user_id) = ...), comparar tipos diferentes ou usar LIKE '%foo%' com curinga no início.
Um teste rápido disso:
Esse + 0 mata o índice — o plano volta para SCAN events. Qualquer expressão aplicada sobre a coluna indexada provoca o mesmo efeito.
Índices de cobertura aparecem de outro jeito
Quando o índice já contém todas as colunas que a consulta precisa, o SQLite consegue responder direto pelo índice, sem nem encostar na tabela. Nesses casos, o plano mostra USING COVERING INDEX:
O plano: SEARCH products USING COVERING INDEX idx_products_sku_price (sku=?). A query pede price, e o índice já guarda sku e price, então o SQLite nem chega a tocar na tabela original. Índice coberto (covering index) é o plano mais rápido que você consegue numa busca pontual — vale ter isso em mente na hora de decidir quais colunas indexar juntas.
Como ler o plano de execução de JOINs no SQLite
É nos JOINs que o plano fica realmente interessante. Cada linha do plano representa uma tabela do JOIN, e a ordem das linhas é a ordem em que o SQLite percorre as tabelas. A primeira tabela é a externa (outer); as seguintes são consultadas uma vez para cada linha dessa tabela externa.
Um plano típico tem essa cara:
QUERY PLAN
|--SEARCH c USING INTEGER PRIMARY KEY (rowid=?)
`--SEARCH o USING INDEX idx_orders_customer (customer_id=?)
Leia de cima para baixo: o SQLite localiza o cliente pela chave primária e, para esse cliente, busca os pedidos correspondentes via índice em customer_id. As duas linhas são SEARCH — sem full scan — que é exatamente o que queremos.
Se na segunda linha aparecesse SCAN o, cada busca de cliente dispararia uma varredura completa em orders. Numa tabela grande, isso é desastroso. A solução, quase sempre, é criar um índice na coluna do join.
Consultas compostas e subconsultas
Planos de UNION, EXCEPT e subconsultas são aninhados. Cada ramo aparece indentado sob seu pai:
Você verá duas linhas filhas sob o título COMPOUND QUERY, uma para cada ramo. Subqueries e CTEs seguem a mesma lógica — cada uma ganha seu próprio nó de plano indentado, e você lê todos com a mesma ótica de SCAN vs SEARCH.
A subconsulta vira um nó separado no plano (algo como "LIST SUBQUERY"), com sua própria estratégia de acesso. Vale aplicar a mesma análise em cada nível.
EXPLAIN vs EXPLAIN QUERY PLAN
São duas coisas diferentes, e tem muita gente que confunde.
EXPLAIN (sem o QUERY PLAN) despeja o bytecode que a máquina virtual do SQLite vai executar — dezenas de opcodes de baixo nível como OpenRead, SeekRowid, Column, ResultRow. Útil se você estiver depurando a própria engine. Quase nunca útil para tunar consulta.
Já o EXPLAIN QUERY PLAN é o resumo legível que você realmente quer ver. Na dúvida, vá direto de EXPLAIN QUERY PLAN.
Um fluxo para consultas lentas no SQLite
Quando uma query está lenta, o ciclo de trabalho é mais ou menos assim:
- Rode
EXPLAIN QUERY PLANem cima dela. - Para cada linha de tabela, pergunte: é
SCANouSEARCH? Em tabela grande,SCANé o principal suspeito. - Se um
SCANestiver filtrando por alguma coluna, pense em criar um índice nessa coluna. - Em joins, confirme que as tabelas do laço interno usam
SEARCH USING INDEXna coluna de junção. - Rode o
EXPLAIN QUERY PLANde novo depois de criar o índice. O plano deve mudar. Se não mudou, o planejador decidiu que seu índice não valia a pena — normalmente porque a tabela é pequena ou o filtro não é seletivo o bastante.
Veja um exemplo prático do passo 5:
O plano deixou de ser SCAN e virou SEARCH. Esse é o sinal de que o índice está cumprindo o papel dele. (Numa tabela recém-criada e quase vazia, o planejador ainda pode optar por scan porque não há dados suficientes para valer a pena usar o índice — popule a tabela ou rode ANALYZE que, na maioria das vezes, a escolha muda.)
O que o plano de execução não te conta
O EXPLAIN QUERY PLAN mostra a estratégia, não o custo. Ele não vai te dizer que a consulta levou 800 ms nem que retornou 50.000 linhas. Para isso, você precisa de tempo de execução (.timer on no CLI) e contagem de linhas. Plano e tempo se completam — o plano te diz por que a consulta está lenta, o timer te diz se ela está.
Mais duas limitações que vale ter em mente:
- O plano pode mudar conforme os dados crescem. Uma query que fazia scan tranquilo numa tabela de 100 linhas vai precisar de índice quando ela chegar a um milhão. Revise os planos com dados em volume de produção, não com as fixtures do seu ambiente de dev.
- O planejador usa estatísticas coletadas pelo
ANALYZE. Sem elas, ele cai em valores padrão que nem sempre são bons. Estatísticas desatualizadas ou ausentes são uma causa comum de planos surpreendentes.
A seguir: ANALYZE e VACUUM
O query planner decide com base nas estatísticas das suas tabelas e índices. Se essas estatísticas estiverem ausentes ou velhas, até um schema com índices perfeitos pode gerar um plano ruim. O ANALYZE é o comando que mantém isso em dia — e o VACUUM é o complemento, usado para liberar espaço e desfragmentar o arquivo do banco. É o que vem na sequência.
Perguntas frequentes
O que o EXPLAIN QUERY PLAN faz no SQLite?
Ele pede ao SQLite para descrever como executaria a consulta, sem rodá-la de fato. A saída mostra quais tabelas serão percorridas, quais índices serão usados e em que ordem os joins acontecem. Basta colocar EXPLAIN QUERY PLAN na frente de qualquer SELECT, INSERT, UPDATE ou DELETE para ver o plano.
Qual a diferença entre SCAN e SEARCH na saída?
SCAN significa que o SQLite vai ler linha por linha da tabela ou do índice — tudo bem em tabelas pequenas, mas caro em tabelas grandes. Já SEARCH indica que ele vai direto nas linhas que interessam, usando um índice ou a chave primária. Em tabelas grandes, você quase sempre quer ver SEARCH nas colunas usadas no filtro.
Como verifico se minha consulta está usando índice?
Rode EXPLAIN QUERY PLAN na consulta e procure por USING INDEX <nome> ou USING COVERING INDEX <nome> na saída. Se aparecer só SCAN <tabela> sem nenhum índice mencionado, é sinal de que a consulta está fazendo varredura completa — e provavelmente um índice resolveria.
Qual a diferença entre EXPLAIN e EXPLAIN QUERY PLAN?
O EXPLAIN mostra o bytecode de baixo nível da máquina virtual do SQLite — útil para entender o motor por dentro, mas raramente ajuda a tunar consultas. Já o EXPLAIN QUERY PLAN traz um resumo legível de como as tabelas são acessadas e quais índices são usados. Para trabalho de performance, é praticamente sempre o EXPLAIN QUERY PLAN que você quer.