Qual é o sentido e o benefício de usar SqlCommand.Prepare ()?

14

Me deparei com o código do desenvolvedor em que o método SqlCommand.Prepare () (consulte MSDN) é amplamente utilizado antes da execução de consultas SQL. E eu me pergunto qual é o benefício disso?

Amostra:

command.Prepare();
command.ExecuteNonQuery();
//...
command.Parameters[0].Value = 20;
command.ExecuteNonQuery();

Eu brinquei um pouco e segui. A execução do comando após chamar o Prepare()método faz com que o Sql Server execute a seguinte instrução:

declare @p1 int
set @p1=1
exec sp_prepexec @p1 output,N'@id int,@desc text',N'INSERT INTO dbo.testtable (id) VALUES (@id)',@id=20'
select @p1

Depois disso, quando o parâmetro obtém seu valor e SqlCommand.ExecuteNonQuery()é chamado, o seguinte é executado no Sql-Server:

exec sp_execute 1,@id=20

Para mim, parece que a instrução é compilada assim que Prepare()é executada. Gostaria de saber qual é o benefício disso? Isso significa que ele é colocado no cache do plano e pode ser reutilizado assim que a consulta final for executada com os valores de parâmetro desejados?

Eu descobri (e o documentei em outra pergunta ) que SqlCommands que são executados com SqlParameters sempre são agrupados em sp_executesqlchamadas de procedimento. Isso torna o Sql Server capaz de armazenar e reutilizar os planos independentemente dos valores dos parâmetros.

Com relação a isso, pergunto-me se o prepare()método é meio inútil ou obsoleto ou se estou perdendo alguma coisa aqui.

Magier
fonte
Eu sempre pensei que tinha algo a ver com a prevenção da injeção de SQL, então você prepararia a consulta para aceitar determinadas variáveis ​​de certos tipos. Dessa forma, se você não passar em variáveis desses tipos, a consulta falha
Mark Sinkinson
Embora em dizer que, esta explicação PHP é provavelmente tão válido para .NET php.net/manual/en/pdo.prepared-statements.php
Mark Sinkinson
"Sim, senhor. Motorista, prepare-se para sair." "O que você está preparando ?! Você está sempre se preparando! Apenas vá!"
Jon of All Trades

Respostas:

19

Preparar um lote SQL separadamente da execução do lote SQL preparado é uma construção efetivamente **inútil para o SQL Server, considerando como os planos de execução são armazenados em cache. Separar as etapas de preparação (análise, vinculação de quaisquer parâmetros e compilação) e execução somente faz sentido quando não houver armazenamento em cache. O objetivo é economizar o tempo gasto na análise e compilação reutilizando um plano existente, e é isso que o cache do plano interno faz. Mas nem todos os RDBMSs fazem esse nível de armazenamento em cache (claramente a partir da existência dessas funções) e, portanto, fica a cargo do código do cliente solicitar que um plano seja "armazenado em cache", salve o ID do cache e reutilize-o isto. Isso representa um ônus adicional para o código do cliente (e para o programador se lembrar de fazê-lo e corrigi-lo) que é desnecessário. De fato, a página do MSDN para o método IDbCommand.Prepare () declara:

O servidor armazena em cache automaticamente os planos de reutilização, conforme necessário; portanto, não há necessidade de chamar esse método diretamente no seu aplicativo cliente.

Deve-se observar que, na medida em que meus testes mostram (que corresponde ao que você mostra na pergunta), chamar SqlCommand.Prepare () , não executa uma operação "preparar" apenas: chama sp_prepexec que prepara e executa o SQL ; ele não chama sp_prepare, que é apenas analisado e compilado (e não recebe nenhum valor de parâmetro, apenas seus nomes e tipos de dados). Portanto, não pode haver nenhum benefício de "pré-compilação" da chamada, SqlCommand.Preparepois ela executa uma execução imediata (supondo que o objetivo seja adiar a execução). No entanto , há um benefício potencial interessante menor de ligar SqlCommand.Prepare(), mesmo que ele ligue em sp_prepexecvez de sp_prepare. Por favor, veja a nota (** ) na parte inferior para obter detalhes.

Meus testes mostram que, mesmo quando SqlCommand.Prepare()é chamado (e observe que esta chamada não emite nenhum comando no SQL Server), o ExecuteNonQueryou ExecuteReadera seguir é totalmente executado e, se for ExecuteReader, ele retorna linhas. Ainda assim, o SQL Server Profiler mostra que a primeira SqlCommand.Execute______()chamada após a SqlCommand.Prepare()chamada é registrada como um evento "Prepare SQL" e as .Execute___()chamadas subseqüentes são registradas como eventos "Exec Prepared SQL".


Código de teste

Foi carregado no PasteBin em: http://pastebin.com/Yc2Tfvup . O código cria um aplicativo de console .NET / C # destinado a ser executado enquanto um rastreamento do SQL Server Profiler estiver em execução (ou uma sessão de Eventos Estendidos). Ele pausa após cada etapa, para que fique claro quais instruções têm um efeito específico.


ATUALIZAR

Encontrei mais informações e um pequeno motivo potencial para evitar chamadas SqlCommand.Prepare(). Uma coisa que notei nos meus testes e o que deve ser observado para qualquer pessoa que execute esse teste do aplicativo Console: nunca há uma chamada explícita sp_unprepare. Eu pesquisei e encontrei a seguinte postagem nos fóruns do MSDN:

SqlCommand - Preparar ou não preparar?

A resposta aceita contém várias informações, mas os pontos mais importantes são:

  • ** O que a preparação realmente poupa é principalmente o tempo necessário para transmitir a sequência de consultas pelo fio.
  • Uma consulta preparada não tem garantia de que um plano em cache seja salvo e não é mais rápido que uma consulta ad-hoc quando chega ao servidor.
  • Os RPCs (CommandType.StoredProcedure) não ganham nada com a preparação.
  • No servidor, um identificador preparado é basicamente um índice em um mapa na memória contendo TSQL para executar.
  • O mapa é armazenado por conexão, não é possível o uso de conexão cruzada.
  • A redefinição enviada quando o cliente reutiliza a conexão do pool limpa o mapa.

Também encontrei a seguinte postagem da equipe do SQLCAT, que parece estar relacionada, mas poderia ser simplesmente um problema específico do ODBC, enquanto o SqlClient é limpo corretamente ao descartar o SqlConnection. É difícil dizer, já que a postagem do SQLCAT não menciona nenhum teste adicional que ajudaria a provar isso como uma causa, como limpar o pool de conexões etc.

Cuidado com as instruções SQL preparadas


CONCLUSÃO

Dado que:

  • o único benefício real da chamada SqlCommand.Prepare()parece ser que você não precisa enviar o texto da consulta novamente pela rede,
  • chamando sp_preparee sp_prepexecarmazenando o texto da consulta como parte da memória da conexão (ou seja, memória do SQL Server)

Eu recomendaria não ligar, SqlCommand.Prepare()porque o único benefício potencial é economizar pacotes de rede enquanto a desvantagem está ocupando mais memória do servidor. Embora a quantidade de memória consumida seja provavelmente muito pequena na maioria dos casos, a largura de banda da rede raramente é um problema, já que a maioria dos servidores de banco de dados está diretamente conectada aos servidores de aplicativos com mais de 10 Megabit de conexões no mínimo (provavelmente 100 Megabit ou Gigabit atualmente) ( e alguns estão na mesma caixa ;-). Isso e a memória são quase sempre um recurso mais escasso do que a largura de banda da rede.

Suponho que, para quem escreve software que pode se conectar a vários bancos de dados, a conveniência de uma interface padrão pode alterar essa análise de custo / benefício. Mas, mesmo nesse caso, tenho que acreditar que ainda é fácil abstrair uma interface de banco de dados "padrão" que permita diferentes provedores (e, portanto, diferenças entre eles, como não precisar chamar Prepare()), considerando que isso é um objeto básico Programação orientada que você provavelmente já está fazendo no código do seu aplicativo ;-).

Solomon Rutzky
fonte
Obrigado por esta extensa resposta. Testei suas etapas e tive duas aberrações de suas expectativas / resultados: na etapa 4, obtive um resultado adicional. Na etapa 10 eu tenho outro plan_handle do que na etapa 2.
Magier
1
@ Magier Não tenho certeza sobre o passo 4 ... talvez você tenha opções SET diferentes ou, de alguma forma, o texto da consulta entre os dois seja diferente? Mesmo uma única diferença de personagem (visível ou não) será um plano diferente. Copiar e colar desta página da web pode não ajudar, portanto copie a consulta (tudo entre aspas simples) da sp_preparechamada para sp_executesql, apenas para ter certeza. Para a etapa 10, sim, fiz mais testes ontem e vi algumas ocasiões com alças diferentes para sp_prepare, mas ainda assim nunca sp_executesql. Vou testar mais uma coisa e atualizar minha resposta.
Solomon Rutzky 22/02
1
@ Magier Na verdade, o teste que eu executei antes o cobriu e não acho que exista uma reutilização verdadeira do plano, especialmente à luz do post do fórum do MSDN dizendo que ele armazena em cache apenas o SQL. Ainda estou curioso para saber como é possível reutilizar o plan_handle, enquanto sp_executesqlnão pode ou não. Mas, independentemente disso, o tempo de análise ainda estará lá sp_preparese o plano tiver sido removido do cache, então removerei essa parte da minha resposta (interessante, mas irrelevante). De qualquer forma, essa parte foi mais uma observação interessante, pois não abordou sua pergunta direta. Tudo o resto ainda se aplica.
Solomon Rutzky
1
Esse era o tipo de explicação que eu estava procurando.
CSharpie
5

Quanto a isso, pergunto-me se o método prepare () é meio inútil ou obsoleto ou se estou perdendo alguma coisa aqui?

Eu diria que o método Prepare tem um valor limitado no mundo SqlCommand, não que seja totalmente inútil. Um benefício é que o Prepare faz parte da interface IDbCommand que o SqlCommand implementa. Isso permite que o mesmo código seja executado em outros provedores de DBMS (que podem exigir Preparação) sem lógica condicional para determinar se a Preparação deve ser chamada ou não.

A preparação não tem valor com o .NET Provider for SQL Server além desses casos de uso, o AFAIK.

Dan Guzman
fonte