IN: 0cm 0cm 0pt"> 

Explorando as técnicas de mais baixo nível da JDBC – para desenvolvedores dispostos a ganhar desempenho a (quase) qualquer preço.

 

 

Na edição anterior, apresentamos as técnicas de programação JDBC, estendendo o padrão de projeto DAO de forma a obter os mesmos benefícios de desempenho de ferramentas O/R – mas mantendo as vantagens de um estilo de programação de “baixo nível”, acessando o bando de dados diretamente.

Este artigo dá seqüência a nossa série sobre JDBC e persistência em geral, com uma variedade de técnicas e dicas mais curtas que não renderiam sozinhas um artigo inteiro. Um ponto em comum dessas dicas é que vamos explorar “truques sujos” como extensões de JDBC e de SQL, que muitas vezes são inevitáveis em nome do desempenho máximo.

Uma observação; muitas dicas deste artigo não são garantidamente independentes de banco de dados. Minha maior experiência é com o Oracle, mais, em muitos casos, diversos bancos suportam as mesmas técnicas apenas com diferenças nos detalhes (como sintaxes SQL diferentes).

Neste artigo, usamos “banco de dados” (ou “BD” ou “banco”) como termo mais simples para Sistema de Gerenciamento de Banco de Dados (SGBD).

 

Statements ou PreparedStatments?

É comum a dúvida de que tipo de statement JDBC utilizar: o simples Statement ou sua extensão, o PreparedStatement. Revisando, o PreparedStatement estende Statement de forma a resolver três problemas.

  • Simplifica a representação dos parâmetros: não é necessário envolver strings com ‘’ (aspas simples) nem converter datas na sintaxe aceita pelo banco de dados, não é preciso converter booleanos para ‘Y’/‘N’ ou 1/0, e assim por diante.
  • Aumenta o desempenho: a execução é mais eficiente porque o driver e/ ou banco de dados processam o texto SQL uma só vez, fazendo um cache da sua representação compilada. Além disso, quando uma consulta é executada, antes de ler os dados do resultado o driver também precisa receber o banco de dados os metadados sobre o resultset (nome, tipo e outras informações sobre cada coluna do cursor de resultados1). Com PreparedStatement, esses metadados só são transferidos na primeira execução; depois também ficam num cache do driver.

Suporta updates em batch2: o PreparedStatement permite executar updates repetitivos, que variam somente pelos parâmetros, com os métodos addBatch() e executeBatch(). (Note que estamos usando o termo “update” em minúsculas para significar qualquer alteração, seja via INSERT, UPDATE ou DELETE, no banco.)

Num capitulo disponível online do livro “Java Programming with Oracle JDBC”,

(oreilly. com/catalog /jorajdbc/chapter/ch 19.html), o autor Donald Bales apresenta um

benchmark comparando ambos os tipos de statements. Ele mostra que Statement é mais

rápido para um número pequeno de execuções de um INSERT (entre ~60 e ~120, variando conforme o driver). Para quantidades maiores de operações, PreparedStatement é até duas vezes mais rápido. Bales adverte contra o uso de PreparedStatement para um número pequeno de operações, mas eu discordo, pois os caches do banco de dados e do driver são de longo prazo: mesmo que você não execute um determinado INSERT cem vezes num loop, certamente irá executá-lo muito mais vezes ao longo de horas ou dias3.Prefiro usar PreparedStatement por default, sempre que não houver evidência de não ser a melhor opção - inclusive em casos onde a funcionalidade de Statement seja suficiente. Esta recomendação é especialmente

forte para bancos de dados de maior porte, que possuem otimizadores de consultas

mais poderosos. Nestes casos o custo de compilação das consultas é maior, pois envolve a análise de estatísticas das tabelas, geração de planos de execução complexos, paralelização de consultas etc.

A documentação da JDBC recomenda o  uso de Statement para consultas dinâmicas.

Um exemplo simples de consulta dinâmica é uma aplicação “cliente SQL” que

permite ao usuário digitar comandos SQL arbitrários. Esse caso extremo é OK para

Statement, mas e um caso raro. Para aplicações comuns, o mais usual é ter consultas

parcialmente dinâmicas: comandos SQL complexos, montados segundo condições que mudam a cada invocação, como no método a seguir:

 

Venda[] findVendas (long codltem, Data datalni,

         Date dataFim, int status) {

StringBuffer sql = new StringBuffer(

         “SELECT FROM VENDAS WHERE item=?”);

if(datalni!= null) sql.append(“ AND data >= ?”);

if(dataFim!= null) sql.append(“ AND data <= ?”);

if (status!= -1) sql.append(“ ANDstatus = 1”);

PreparedStatement stmt =

getConn().prepareStatement(sql.toString());

...

}

 

Com três trechos opcionais, o número de possíveis consultas distintas de findVendas()

sera 23 = 8. Isso ainda é muito pouco para nos preocuparmos; mas e se fossem 16

parâmetros opcionais, gerando até 65.536 consultas diferentes? O senso comum (e o

javadoc) nos diria que não é boa idéia usar PreparedStatement - para não encher o cache nem pagar os custos maiores de inicialização com consultas raramente repetidas.

Mas na prática, métodos desse tipo tendem a ter algumas combinações de parâmetros

muito mais freqüentes que outras. Além disso, os bancos de dados costumam ser

inteligentes para manter no cache as consultas executadas com mais freqüência Statement era bem mais útil nos primeiros tempos da JDBC, quando havia drivers que

nem implementavam PreparedStatement.

Por outro lado, se a sua aplicação tiver muitas consultas “extremamente dinâmicas”,

isso e mau sinal. Pode ser melhor mudar sua implementação de forma a ter

consultas mais fixas. Os maiores problemas de consultas dinâmicas estão na depuração

e no tuning. Quando alguma coisa funciona de forma errada ou muito lentamente, a

correção será mais difícil se cada execução gerar um comando SQL diferente. Uma alteração que corrige um caso pode estragar outro, sendo difícil testar todas as consultas

resultantes de cada permutação de muitos parâmetros opcionais.

Alguns desenvolvedores já repararam que certas consultas SQL executam com

eficiência maior com um Statement. Isso aconteceu comigo, em operações como:

 

  1. DELETE FROM WHERE data
  2. DELETE FROM VENDAS WHERE data < TO_DATE('01/03/2005')

O DELETE 1 era usado para limpar registros desnecessários de uma tabela extremamente grande (com dezenas de milhões de registros). O problema é que o 1 demorava no mínimo um minuto para executar, mesmo que nenhum registro fosse apagado. Isto ocorria num banco de dados Oracle 9i, sendo que a coluna data era indexada.

Investigando o problema, descobri que ao usar um Statement com a versão 2 (ou um cliente SQL como Squirrel, TOAD, sqlplus etc.) a execução era instantânea! Mas o

motivo da diferença de desempenho não tem nada a ver com a JDBC. Acontece que

os compiladores de qualquer linguagem costumam tirar vantagem de constantes - e o SQL não é uma exceção.

Como as consultas 1 e 2 não são exatamente iguais elas não serão compiladas da mesma forma. Com o parâmetro literal em 2, o otimizador do Oracle gerava uma versão compilada da consulta que deveria verificar se o valor mínimo da coluna já é menor do que a data do WHERE. Neste caso, o DELETE não precisa ser feito. Com um argumento variável, esta otimização talvez seja mais difícil, ou o que é mais provável,

talvez o otimizador do Oracle considere que não vale a pena fazê-la. Como as consultas parametrizadas costumam ser executadas repetitivamente, o otimizador não vai querer adicionar o trabalho de conferir o MIN(date) da tabela a cada execução, aumentando o custo de todas as execuções da consulta. Isso para beneficiar um caso raro - o de não haver nada a apagar.

A necessidade da minha aplicação era um desses casos raros, e eu não queria ter que converter o argumento data para literal, nem pagar o custo de compilação da consulta a cada execução. Assim resolvi o problema fazendo a otimização equivalente à mão, executando primeiro um SELECTMAX(date) para ver se o DELETE era necessário4.

Conclusão: não coloque a culpa no PreparedStatement prematuramente se por acaso

ele parecer mais lento!

 

...

Quer ler esse conteúdo completo? Tenha acesso completo