Por que usar queries parametrizadas?
Um erro comum entre programadores é achar que parâmetros dentro de uma query são como macro-substituições, uma espécie de "Search/Replace" dentro de queries. Isso não é verdade.
Parâmetros são formas de identificar para o servidor SGBD os valores e seus tipos de forma organizada, segura e não sujeita a erros. Além disso, permitem a reutilização de uma mesma sentença SQL de forma mais performática.
O problema das queries literais (concatenação de strings)
Muitos programadores preferem usar queries literais, isto é, concatenando valores diretamente na string SQL. Isso pode parecer mais simples à primeira vista, mas traz sérios problemas de segurança e performance.
Exemplo de query literal (ERRADO):
if Query1.Active then
Query1.Close;
Query1.SQL.Text :=
'UPDATE CLIENTES SET ' +
' RAZAO_SOCIAL = ''' + Trim(edtRazaoSocial.Text) + '''' +
' WHERE ID_CLIENTE = ' + edtID_CLIENTE.Text;
Query1.ExecSQL;
Se a razão social for INTELIG S/A e o ID_CLIENTE for 666, a query resultante seria:
UPDATE CLIENTES SET RAZAO_SOCIAL='INTELIG S/A' WHERE ID_CLIENTE=666
Até aqui, parece inofensivo. Mas e se a razão social digitada for ''--INTELIG S/A (duas aspas simples propositais)? A query resultante seria:
UPDATE CLIENTES SET RAZAO_SOCIAL=''--INTELIG S/A' WHERE ID_CLIENTE=666
--) em SQL indicam comentário. Isso significa que tudo após os traços será ignorado pelo SGBD. No exemplo acima, a cláusula WHERE foi comentada, fazendo com que TODOS os registros da tabela CLIENTES sejam atualizados, não apenas o registro com ID 666!
Por que QuotedStr() não é suficiente?
Alguns programadores podem pensar: "Vou usar QuotedStr() para resolver o problema das aspas". Embora essa função resolva parcialmente a questão de aspas simples, ela tem limitações importantes:
- Performance: É anti-performática, especialmente com textos longos. Imagine processar um código HTML, RTF ou um campo MEMO grande - a função precisa contar todas as posições das aspas e duplicá-las.
- Segurança incompleta:
QuotedStr()não resolve questões envolvendo códigos Unicode, caracteres de escape e outros métodos que podem sabotar sua query. - Manutenção: Código mais difícil de manter e propenso a erros.
A solução: Queries parametrizadas
A solução correta é usar parâmetros e sinalizar o final da query. Veja o exemplo correto:
if Query1.Active then
Query1.Close;
Query1.SQL.Clear;
Query1.SQL.Add('UPDATE CLIENTES SET ');
Query1.SQL.Add(' RAZAO_SOCIAL = :P_RAZAO_SOCIAL');
Query1.SQL.Add('WHERE ID_CLIENTE = :P_ID_CLIENTE');
Query1.SQL.Add(';'); // sinalizando o fim da query
Query1.ParamByName('P_RAZAO_SOCIAL').AsString := Trim(edtRazaoSocial.Text);
Query1.ParamByName('P_ID_CLIENTE').AsInteger := StrToInt(edtID_CLIENTE.Text);
Query1.ExecSQL;
Por que usar SQL.Add() ao invés de SQL.Text? Se você viu artigos anteriores sobre produtividade na IDE Delphi e Lazarus, deve ter notado o uso de MultiPaste ou Column Mode. Usando esses recursos, você escreve a query dentro do editor SQL (IBExpert, FlameRobin, etc.), testa a query, e depois cola na IDE de programação. Usando MultiPaste ou Column Mode, você gera o código de forma rápida e organizada.
Com queries parametrizadas, sob nenhuma hipótese sua query poderá ser sabotada pelo tipo de dados informado num formulário. O SGBD trata os parâmetros de forma segura, separando claramente a estrutura da query dos valores que serão utilizados.
Benefícios adicionais: Preparação de queries
Queries parametrizadas têm ainda outra vantagem importante: caso sejam muito repetitivas em sua aplicação, você pode prepará-las uma única vez e reutilizá-las muitas vezes, otimizando o uso em rede.
Como funciona a execução de uma query?
Quando você executa uma query, o SGBD passa por várias etapas:
- Análise sintática: Verifica se a sintaxe SQL está correta
- Otimização: Analisa a melhor forma de executar a query
- Cálculo de custo: Avalia o custo de operação com índices disponíveis
- Compilação: Converte para o formato BLR (Binary Language Representation)
- Execução: Executa a query e retorna o resultado (se aplicável)
De todas essas etapas, a mais custosa é o PLANO (plan) para escolher o índice adequado. Quanto mais índices existirem, mais tempo levará a análise. Isso contradiz um pouco a ideia de que "quanto mais índices, melhor", não é mesmo?
Exemplo sem preparação:
Imagine um sistema de atendimento com mais de 400 pessoas realizando a mesma consulta, apenas com clientes diferentes:
if Query1.Active then
Query1.Close;
Query1.SQL.Clear;
Query1.SQL.Add('SELECT status, razao_social, id_cliente FROM CLIENTES');
Query1.SQL.Add('WHERE CNPJ = :P_CNPJ');
Query1.ParamByName('P_CNPJ').AsString := Trim(Pesquisa.Text);
Query1.Open();
Essa query será executada inúmeras vezes, passando por todo o processo (análise, otimização, cálculo de custo, compilação) a cada consulta. Isso é muito ineficiente!
Exemplo com preparação (CORRETO):
Há uma forma de otimizar isso usando a preparação da query:
if Query1.Active then
Query1.Close;
Query1.SQL.Clear;
Query1.SQL.Add('SELECT status, razao_social, cnpj FROM CLIENTES');
Query1.SQL.Add('WHERE ID_CLIENTE = :P_ID_CLIENTE');
if not Query1.Prepared then
Query1.Prepare;
O método Prepare submete a query ao SGBD para "preparação". A query vai até a última etapa (compilação para BLR e criação do plano de execução), mas não executa. O SGBD mantém o BLR e o PLANO em memória, e nas próximas execuções repetitivas não será necessário passar por todo o processo novamente.
E se eu não usar o método Prepare? Ferramentas de programação como Delphi e Lazarus fazem inferência, isto é, acabam preparando do mesmo jeito, mas isso pode acontecer num momento inconveniente do programa e mais de uma vez. É melhor que você faça a preparação num momento bem antes da execução da query, para que o tempo de preparação não se some ao tempo da execução.
Como o SGBD identifica queries preparadas?
O servidor mantém na memória todas as queries já preparadas em forma de assinatura:
SELECT status, razao_social, cnpj FROM CLIENTES WHERE ID_CLIENTE = ?
Esta é a assinatura. Em alguns SGBDs, os nomes dos campos são irrelevantes. Qualquer query similar à assinatura será reconhecida imediatamente como uma query preparada.
Não importa de qual estação, programa ou parte do programa venha a query - o que importa é a assinatura. Se ela for reconhecida, o SGBD usará o que já foi preparado. Por isso podemos dizer que queries preparadas não beneficiam apenas uma conexão/programa, mas muitas conexões e muitos programas diferentes onde estatisticamente a mesma consulta acontece.
Verificando se a query já está preparada:
Note que fazemos um teste antes de prepará-las:
if not Query1.Prepared then
Query1.Prepare;
Por que fazemos isso? Parece óbvio, mas se prepararmos todas as vezes, não obteremos a performance que desejamos. A preparação deve ser feita uma única vez e nunca mais repetida. Mesmo que você feche o programa e abra de novo, a preparação pode estar lá em alguma parte da memória do SGBD (dependendo de como o SGBD gerencia o cache de queries preparadas).
Exemplo prático: Transferências em lote
A preparação também é excelente em transferências de dados em lote. Veja o exemplo:
// Primeira query: busca clientes inativos há mais de 5 anos
if Query1.Active then
Query1.Close;
Query1.SQL.Clear;
Query1.SQL.Add('SELECT status, razao_social, cnpj, id_cliente FROM CLIENTES');
Query1.SQL.Add('WHERE ULTIMO_PEDIDO < DATEADD(-5 YEAR TO CURRENT_DATE)');
Query1.Open();
// Segunda query: atualiza status (será executada múltiplas vezes)
if Query2.Active then
Query2.Close;
Query2.SQL.Clear;
Query2.SQL.Add('UPDATE CLIENTES SET STATUS = ''C'' ');
Query2.SQL.Add('WHERE ID_CLIENTE = :P_ID_CLIENTE');
if not Query2.Prepared then
Query2.Prepare;
// Loop processando cada registro
while not Query1.EOF do
begin
// ... lógica de negócio ...
if isPrecisaCancelar then
begin
Query2.ParamByName('P_ID_CLIENTE').AsInteger :=
Query1.FieldByName('ID_CLIENTE').AsInteger;
Query2.ExecSQL;
end;
Query1.Next;
end;
// Libera recursos quando não for mais usar
if Query2.Prepared then
Query2.UnPrepare;
No exemplo acima, a repetição da mesma query é óbvia, então realizamos a preparação e economizamos tempo significativo. Ao término do processo, quando não vamos mais usar a query preparada, executamos UnPrepare para liberar recursos.
Quando usar UnPrepare?
O UnPrepare faz com que a assinatura da preparação no SGBD seja eliminada, liberando recursos da memória do SGBD.
Devo fazer sempre o UnPrepare no final? Quase sempre a resposta é "nunca". Geralmente só usamos o UnPrepare em situações de transferência de lote de dados, um cenário onde não há outros na rede que poderiam se beneficiar de uma query preparada. Um UnPrepare quando outros na rede estão se beneficiando de queries repetitivas mataria a performance.
Regra simples: Quando a chance é zero de outros repetirem o mesmo procedimento depois que o mesmo já foi executado, então podemos com tranquilidade chamar o método UnPrepare. Nos outros cenários, você nunca irá usá-lo.
Quando NÃO usar preparação
Sentenças SQL são muito flexíveis e cada SGBD tem suas peculiaridades. Não é possível descrever todas as situações, mas quando uma sentença é ambígua para tomada de decisão, você deve evitar uma preparação.
Exemplo problemático: LIKE com parâmetros
Com exemplo podemos citar o LIKE. Quando usamos:
SELECT * FROM CLIENTES
WHERE RAZAO_SOCIAL LIKE 'INDUSTRIA%'
No exemplo acima, está bem claro que se houver um índice associado à razão social, o SGBD deverá usá-lo. Mas e se a query fosse:
SELECT * FROM CLIENTES
WHERE RAZAO_SOCIAL LIKE '%INDUSTRIA%'
Neste novo exemplo, o SGBD irá ignorar qualquer índice, pois não faria sentido usá-los já que todos os registros precisarão ser lidos de qualquer forma.
Agora imagine o que aconteceria se preparasse uma query assim:
if Query1.Active then
Query1.Close;
Query1.SQL.Clear;
Query1.SQL.Add('SELECT * FROM CLIENTES');
Query1.SQL.Add('WHERE RAZAO_SOCIAL LIKE :PESQUISA');
if not Query1.Prepared then
Query1.Prepare;
Como você não consegue determinar o que a pessoa digitará na barra de pesquisa, vamos imaginar que a primeira pessoa tenha pesquisado por:
'INDUSTRIA%'
Como ela é a primeira pessoa a fazer a preparação, nosso PLANO seria usar um índice por Razão Social, certo? Sim, isto estaria certo. Mas note que a query já foi preparada e há no SGBD a assinatura que já indicará o PLANO a ser executado.
Daí vem a segunda pessoa e no campo de pesquisa faz a seguinte busca:
'%INDUSTRIA%'
Como a query já foi preparada, as pesquisas seguintes usarão o mesmo PLANO, ou seja, você forçou o uso de um índice imprestável ao LIKE que só o fará perder tempo.
Solução para casos ambíguos:
Em situações ambíguas, evite usar parâmetro e prefira o QuotedStr() em seu lugar:
if Query1.Active then
Query1.Close;
Query1.SQL.Clear;
Query1.SQL.Add('SELECT * FROM CLIENTES');
Query1.SQL.Add('WHERE RAZAO_SOCIAL LIKE ' + QuotedStr(edtPesquisa.Text));
Neste caso específico, como a query muda significativamente dependendo do padrão LIKE (com ou sem % no início), não faz sentido prepará-la. Cada execução terá um plano de execução diferente.
Resumo das boas práticas
Sempre use queries parametrizadas quando:
- Os valores vêm de entrada do usuário (formulários, campos de texto)
- A estrutura da query é fixa e apenas os valores mudam
- Você precisa de segurança contra SQL Injection
- A query será executada múltiplas vezes com valores diferentes
Use Prepare quando:
- A query será executada repetidamente na mesma sessão
- Você quer otimizar performance em operações em lote
- A estrutura da query é estável e não muda
❌ Evite parâmetros quando:
- A query tem estruturas que mudam significativamente (ex: LIKE com padrões variáveis)
- O plano de execução precisa ser recalculado a cada execução
- A query é executada apenas uma vez e não será reutilizada
❌ Nunca use UnPrepare quando:
- Outros programas/usuários na rede podem se beneficiar da query preparada
- A query será reutilizada frequentemente
- Você está em um ambiente multi-usuário
Conclusão
Tanto os parâmetros quanto a preparação de uma query não dependem da linguagem de programação, pois este recurso está disponível em todas elas. Estes recursos servem a um propósito bem definido: melhorar segurança e agilizar pesquisas.
Se você notar que programadores de sua equipe não estão usando tais recursos, compartilhe este artigo com eles.
Cada SGBD é diferente. Alguns deles subvertem queries mal feitas, outros usam algoritmos para melhorar uma query pouco performática, mas não se engane: se programadores forem eficientes na origem da query, o banco será muito mais performático. Do contrário, o banco vai ser uma tartaruga obesa com 3 patas.
Principais benefícios das queries parametrizadas:
- Segurança: Proteção contra SQL Injection
- Performance: Possibilidade de preparação e reutilização
- Manutenibilidade: Código mais limpo e fácil de manter
- Tipagem: Validação automática de tipos de dados
- Eficiência: Menor uso de recursos do servidor