Menu

Prepared Statements no SQLite: prepare, bind, step e finalize

Entenda o que são prepared statements no SQLite, por que eles existem e o ciclo prepare/bind/step/finalize que todo driver usa por baixo dos panos.

Esta página tem editores executáveis — edite, execute e veja a saída na hora.

O que é, de fato, um prepared statement

Quando você entrega uma string SQL para o SQLite, ele precisa fazer um bocado de trabalho antes de qualquer linha sair do lugar: tokenizar, fazer o parse, conferir se as tabelas e colunas existem, montar um plano de execução e compilar esse plano no bytecode da máquina virtual do SQLite. Só depois disso a consulta roda de verdade.

Um prepared statement é o que você obtém quando para no passo "compilado em bytecode" e guarda esse resultado. O programa compilado tem espaços vazios — os placeholders — onde os valores reais vão entrar mais tarde. Dá para executar o mesmo programa compilado várias vezes com valores diferentes, e ainda por cima de forma segura mesmo quando os valores vêm de uma fonte não confiável.

Pense na diferença entre entregar uma receita para alguém ler em voz alta toda vez que for cozinhar, e ensinar a receita uma vez e, no dia, só falar quais são os ingredientes.

O ciclo de vida: prepare, bind, step e finalize

Todo driver do SQLite, em qualquer linguagem, é um wrapper em cima das mesmas quatro chamadas da API C. Vale conhecer os nomes mesmo que você nunca vá escrever uma linha de C, porque as mensagens de erro e a documentação usam esse vocabulário:

  1. sqlite3_prepare_v2 — compila uma string SQL em um handle de statement.
  2. sqlite3_bind_* — preenche os valores dos placeholders (uma função para cada tipo).
  3. sqlite3_step — executa o programa. Para SELECT, você chama repetidamente para percorrer as linhas. Para INSERT/UPDATE/DELETE, uma única chamada já resolve.
  4. sqlite3_finalize — libera o programa compilado quando você terminar.

Entre uma execução e outra, sqlite3_reset rebobina um statement já finalizado, permitindo fazer novo bind e executar de novo sem precisar preparar tudo outra vez.

Placeholders no SQL

Dentro da string SQL, você marca cada ponto onde entra um valor com um placeholder, em vez de concatenar o valor direto na string. O SQLite aceita algumas formas diferentes:

-- Anônimo, posicional:
INSERT INTO users (name, email) VALUES (?, ?);

-- Numerado:
INSERT INTO users (name, email) VALUES (?1, ?2);

-- Nomeado:
INSERT INTO users (name, email) VALUES (:name, :email);
INSERT INTO users (name, email) VALUES (@name, @email);
INSERT INTO users (name, email) VALUES ($name, $email);

O ? é o mais comum em código de driver. Já os placeholders nomeados (:nome) ficam mais legíveis quando há vários parâmetros ou quando o mesmo valor aparece mais de uma vez. Escolha um estilo por projeto e mantenha a consistência.

O que você não deve fazer é montar a query concatenando strings:

-- NÃO FAÇA ISSO:
"INSERT INTO users (name) VALUES ('" + user_input + "')"

Esse é o caminho direto para SQL injection — e ainda joga fora todo o reaproveitamento de bytecode que você vai conhecer agora.

Um exemplo prático em SQL

Para entender a mecânica sem precisar de uma linguagem hospedeira, aqui vai o equivalente ao prepare/bind/step usando só os recursos de SQL que o próprio SQLite oferece. Vamos criar uma tabela e inserir uma linha usando um placeholder no estilo de parâmetro, preenchido por um literal:

Em uma aplicação real, você não escreveria os valores direto na string SQL — o certo é fazer o prepare do INSERT uma única vez usando placeholders ?, ?, e depois fazer o bind do par nome/email para cada usuário e chamar step. O bytecode compilado é exatamente o mesmo em todas as chamadas; o que muda são apenas os valores vinculados.

Reuso de prepared statement no SQLite (o ganho de performance)

Esse é o padrão que o seu driver permite escrever. O código abaixo é pseudocódigo — cada linguagem tem sua própria sintaxe —, mas a estrutura é sempre a mesma:

-- preparada uma vez:
INSERT INTO users (name, email) VALUES (?, ?);

-- então, em um loop:
--   bind(1, name)
--   bind(2, email)
--   step()
--   reset()

O prepare analisa e compila o SQL uma única vez. A cada iteração, só roda o bytecode e copia os valores para os slots. Em inserts em massa (imagine importar 100.000 linhas), isso é absurdamente mais rápido do que executar 100.000 comandos parseados individualmente — costuma ser uma ordem de magnitude, principalmente se você envolver tudo numa única transação.

Uma pegadinha comum: muita gente faz um loop chamando prepare dentro dele. Isso joga fora todo o ganho. O certo é preparar fora do loop e fazer o bind e o step lá dentro.

Por que essa é a forma segura

Os parâmetros vinculados não são strings concatenadas no SQL. São valores entregues ao programa em bytecode por meio de slots tipados — slots de inteiro, de texto, de blob. O SQLite nunca os reinterpreta como SQL, então nenhum valor consegue alterar a estrutura da query.

Compare:

-- Vulnerável. Se user_input for:  '); DROP TABLE users;--
-- a consulta se torna destrutiva.
"SELECT * FROM users WHERE name = '" + user_input + "'"

-- Seguro. user_input é vinculado como um valor TEXT e sempre
-- comparado apenas como string, não importa o que contenha.
SELECT * FROM users WHERE name = ?;

A segunda forma é segura mesmo que user_input seja '); DROP TABLE users;--. O SQLite vai obedientemente procurar um usuário cujo nome seja exatamente essa string (esquisita), não vai encontrar nenhum e devolver zero linhas. Nada na estrutura da query muda com base no valor.

Vamos nos aprofundar em SQL injection num documento mais à frente, mas a ideia central é essa: prepared statements não são só uma defesa contra SQL injection — são a defesa.

Statements que retornam linhas

No caso de um SELECT, o step devolve uma linha por vez. O driver normalmente fica num loop até receber "done":

No código da aplicação, o driver faz o prepare desse SELECT com um ? no lugar de 2.00, faz o bind do valor de limite e chama step num loop, lendo uma linha por chamada. Quando passa da última linha, o step sinaliza que terminou, e aí o driver ou faz reset no statement (para rodar de novo com outro limite) ou chama finalize.

Não esqueça do finalize

Um prepared statement é uma pequena alocação dentro do SQLite. Vazar esses statements consome memória e, pior ainda, mantém um lock interno no banco que pode bloquear outros writers. Todo driver oferece alguma forma de limpar isso automaticamente — context managers no Python, blocos using em C#, RAII em C++ — e vale a pena usar:

  • O sqlite3 do Python faz finalize quando o cursor é coletado pelo garbage collector, mas chamar cursor.close() explicitamente é mais limpo.
  • O better-sqlite3 (Node) finaliza quando o objeto Statement é coletado; prepared statements de vida longa não são problema.
  • Em C puro, você mesmo chama sqlite3_finalize. Esquecer disso é um bug de verdade.

A regra de bolso: se você fez o prepare, alguém precisa fazer o finalize.

Quando talvez você nem precise chamar isso

Na prática, você raramente vai chamar sqlite3_prepare_v2 na mão. Drivers de alto nível transformam connection.execute("SELECT ... WHERE id = ?", (42,)) em prepare/bind/step/finalize por você. Entender o ciclo de vida vale a pena por outros motivos:

  • Você vai reconhecer o que está acontecendo quando aparecerem erros do tipo "statement is busy" ou "cannot operate on a finalized statement".
  • Você vai saber que precisa cachear prepared statements de longa duração quando estiver fazendo inserts num loop apertado — questão de performance.
  • Você vai escrever parameterized queries por instinto, mesmo quando uma concatenação de string parecer tentadora (e segura contra SQL injection).

ORMs e query builders levam isso mais longe ainda. Eles montam o SQL, cuidam dos prepared statements e te devolvem resultados tipados. Por baixo dos panos, são as mesmas quatro chamadas.

A seguir: bind de parâmetros

Até aqui falamos de placeholders de forma abstrata. No próximo tópico vamos ver o lado do bind em detalhes — parâmetros posicionais vs nomeados, tratamento de tipos, NULL e algumas pegadinhas que aparecem quando você começa a passar dados reais da aplicação para as queries.

Perguntas frequentes

O que é um prepared statement no SQLite?

É uma query SQL que já foi parseada, compilada e transformada num bytecode pronto pra rodar — só que com placeholders (? ou :nome) no lugar dos valores. Você passa os valores separadamente na hora de executar. No SQLite isso é feito pelas funções sqlite3_prepare_v2, sqlite3_bind_*, sqlite3_step e sqlite3_finalize.

Por que usar prepared statements no SQLite?

Por dois motivos: segurança e velocidade. Como os parâmetros vão ligados (bind) e nunca são interpretados como SQL, SQL injection deixa de ser possível. E quando você roda a mesma query várias vezes — tipo um insert de 10.000 linhas — preparar uma vez e só re-bindar os valores evita o parser a cada iteração, e isso aparece no tempo de execução.

Qual a diferença entre prepared statement e uma query normal?

Uma chamada comum de sqlite3_exec parseia e executa o SQL de uma vez só, com os valores embutidos como texto. O prepared statement separa compilação de execução: você faz prepare do SQL uma vez, bind dos valores tipados nos placeholders, step pra percorrer os resultados e reset quando quer rodar de novo. Todo driver de alto nível (o sqlite3 do Python, better-sqlite3 e companhia) usa prepared statements por baixo dos panos.

Coddy programming languages illustration

Aprenda a programar com o Coddy

COMEÇAR