Como parametrizar uma consulta que contém uma IN
cláusula com um número variável de argumentos, como este?
SELECT * FROM Tags
WHERE Name IN ('ruby','rails','scruffy','rubyonrails')
ORDER BY Count DESC
Nesta consulta, o número de argumentos pode estar entre 1 e 5.
Eu preferiria não usar um procedimento armazenado dedicado para isso (ou XML), mas se houver uma maneira elegante e específica para o SQL Server 2008 , estou aberto a isso.
sql
sql-server
parameters
Jeff Atwood
fonte
fonte
Respostas:
Aqui está uma técnica rápida e suja que eu usei:
Então aqui está o código C #:
Duas advertências:
LIKE "%...%"
consultas não são indexadas.|
tag em branco ou nula ou isso não funcionaráExistem outras maneiras de conseguir isso que algumas pessoas podem considerar mais limpas; portanto, continue lendo.
fonte
Você pode parametrizar cada valor, então algo como:
O que lhe dará:
Não, isso não está aberto à injeção de SQL . O único texto injetado no CommandText não se baseia na entrada do usuário. É baseado apenas no prefixo "@tag" codificado e no índice de uma matriz. O índice sempre será um número inteiro, não é gerado pelo usuário e é seguro.
Os valores inseridos pelo usuário ainda estão inseridos nos parâmetros, portanto, não há vulnerabilidade.
Editar:
Não que os planos de consulta em cache não sejam valiosos, mas na IMO essa consulta não é suficientemente complicada para obter muitos benefícios dela. Embora os custos de compilação possam se aproximar (ou até exceder) os custos de execução, você ainda está falando em milissegundos.
Se você tiver RAM suficiente, espero que o SQL Server provavelmente armazene em cache um plano para as contagens comuns de parâmetros. Suponho que você sempre possa adicionar cinco parâmetros e permitir que as tags não especificadas sejam NULL - o plano de consulta deve ser o mesmo, mas me parece muito feio e não tenho certeza de que valeria a micro otimização (embora, no Stack Overflow - pode valer a pena).
Além disso, o SQL Server 7 e versões posteriores parametrizam automaticamente as consultas , portanto, o uso de parâmetros não é realmente necessário do ponto de vista de desempenho - é, no entanto, crítico do ponto de vista de segurança - especialmente com dados inseridos pelo usuário como este.
fonte
Para o SQL Server 2008, você pode usar um parâmetro com valor de tabela . É um pouco de trabalho, mas é sem dúvida mais limpo do que meu outro método .
Primeiro, você deve criar um tipo
Em seguida, seu código ADO.NET fica assim:
fonte
SELECT * FROM tags WHERE tags.name IN (SELECT name from @tvp);
? Em teoria, essa realmente deve ser a abordagem mais rápida. Você pode usar índices relevantes (por exemplo, um índice no nome da tag que sejaINCLUDE
o número ideal) e o SQL Server deve fazer algumas tentativas para capturar todas as tags e suas contagens. Como é o plano?A pergunta original era "Como parametrizar uma consulta ..."
Deixe-me declarar aqui, que isso não é uma resposta para a pergunta original. Já existem demonstrações disso em outras boas respostas.
Com isso dito, vá em frente e marque esta resposta, faça voto negativo, marque-a como não uma resposta ... faça o que achar que está certo.
Veja a resposta de Mark Brackett para a resposta preferida que eu (e 231 outras) votaram. A abordagem dada em sua resposta permite 1) o uso eficaz de variáveis de ligação e 2) predicados que são sargáveis.
Resposta selecionada
O que quero abordar aqui é a abordagem dada na resposta de Joel Spolsky, a resposta "selecionada" como a resposta certa.
A abordagem de Joel Spolsky é inteligente. E funciona razoavelmente, exibirá comportamento previsível e desempenho previsível, dados valores "normais" e com os casos de borda normativos, como NULL e a sequência vazia. E isso pode ser suficiente para uma aplicação específica.
Mas, em termos de generalização dessa abordagem, vamos considerar também os casos de canto mais obscuros, como quando a
Name
coluna contém um caractere curinga (conforme reconhecido pelo predicado LIKE). O caractere curinga que eu vejo mais comumente usado é%
(um sinal de porcentagem). Então, vamos lidar com isso aqui agora e depois continuar com outros casos.Alguns problemas com o caractere%
Considere um valor de nome de
'pe%ter'
. (Para os exemplos aqui, eu uso um valor literal de sequência no lugar do nome da coluna.) Uma linha com o valor Name de '' pe% ter 'seria retornada por uma consulta do formulário:Mas essa mesma linha não será retornada se a ordem dos termos da pesquisa for revertida:
O comportamento que observamos é meio estranho. Alterar a ordem dos termos da pesquisa na lista altera o conjunto de resultados.
É quase desnecessário dizer que podemos não querer
pe%ter
combinar manteiga de amendoim, não importa o quanto ele goste.Caixa de canto obscura
(Sim, eu concordo que este é um caso obscuro. Provavelmente, provavelmente não será testado. Não esperamos um curinga em um valor de coluna. Podemos assumir que o aplicativo impede que esse valor seja armazenado. Mas Na minha experiência, raramente vi uma restrição de banco de dados que especificamente proibia caracteres ou padrões que seriam considerados curingas no lado direito de um
LIKE
operador de comparação.Corrigindo um buraco
Uma abordagem para corrigir esse buraco é escapar do
%
caractere curinga. (Para quem não conhece a cláusula de escape no operador, aqui está um link para a documentação do SQL Server .Agora podemos combinar o literal%. Obviamente, quando temos um nome de coluna, precisamos escapar dinamicamente do curinga. Podemos usar a
REPLACE
função para encontrar ocorrências do%
caractere e inserir um caractere de barra invertida na frente de cada um, assim:Portanto, isso resolve o problema com o curinga%. Quase.
Escapar da fuga
Reconhecemos que nossa solução introduziu outro problema. O caractere de escape. Vemos que também precisaremos escapar de quaisquer ocorrências do próprio personagem de escape. Desta vez, usamos o! como o caractere de escape:
O sublinhado também
Agora que estamos em um rolo, podemos adicionar outro
REPLACE
identificador ao curinga de sublinhado. E só por diversão, desta vez, usaremos $ como o caractere de escape.Eu prefiro essa abordagem do que escapar porque funciona no Oracle e no MySQL e no SQL Server. (Eu normalmente uso a barra invertida \ como o caractere de escape, pois esse é o caractere que usamos nas expressões regulares. Mas por que ser restringido pela convenção!
Aqueles suportes chatos
O SQL Server também permite que caracteres curinga sejam tratados como literais colocando-os entre colchetes
[]
. Portanto, ainda não terminamos a correção, pelo menos para o SQL Server. Como os pares de colchetes têm um significado especial, precisamos escapar deles também. Se conseguirmos escapar adequadamente dos colchetes, pelo menos não teremos que nos preocupar com o hífen-
e o quilate^
dentro dos colchetes. E podemos deixar qualquer caractere%
e_
dentro dos colchetes escapado, já que basicamente desativamos o significado especial dos colchetes.Encontrar pares de parênteses não deve ser tão difícil. É um pouco mais difícil do que lidar com as ocorrências de singleton% e _. (Observe que não é suficiente escapar apenas de todas as ocorrências de colchetes, porque um colchete único é considerado literal e não precisa ser escapado. A lógica está ficando um pouco mais confusa do que eu posso suportar sem executar mais casos de teste .)
Expressão em linha fica confusa
Essa expressão embutida no SQL está ficando mais longa e mais feia. Provavelmente, podemos fazer funcionar, mas o céu ajuda a pobre alma que fica para trás e precisa decifrá-la. Por mais que eu seja fã de expressões embutidas, estou inclinado a não usar uma aqui, principalmente porque não quero deixar um comentário explicando o motivo da bagunça e me desculpando.
Uma função onde?
Ok, então, se não tratarmos isso como uma expressão embutida no SQL, a alternativa mais próxima que temos é uma função definida pelo usuário. E sabemos que isso não acelerará as coisas (a menos que possamos definir um índice, como poderíamos com o Oracle.) Se precisarmos criar uma função, é melhor fazer isso no código que chama SQL declaração.
E essa função pode ter algumas diferenças de comportamento, dependentes do DBMS e da versão. (Um grito para todos os desenvolvedores de Java, que desejam tanto usar qualquer mecanismo de banco de dados de maneira intercambiável.)
Conhecimento de domínio
Podemos ter conhecimento especializado do domínio para a coluna (ou seja, o conjunto de valores permitidos aplicados à coluna. Podemos saber a priori que os valores armazenados na coluna nunca conterão um sinal de porcentagem, sublinhado ou colchete Nesse caso, apenas incluímos um comentário rápido de que esses casos são abordados.
Os valores armazenados na coluna podem permitir% ou _ caracteres, mas uma restrição pode exigir que esses valores sejam escapados, talvez usando um caractere definido, de modo que os valores sejam LIKE comparáveis "seguros". Novamente, um comentário rápido sobre o conjunto permitido de valores e, em particular, qual caractere é usado como caractere de escape, e siga a abordagem de Joel Spolsky.
Porém, sem o conhecimento especializado e uma garantia, é importante considerar pelo menos lidar com esses casos obscuros de esquina e considerar se o comportamento é razoável e "de acordo com a especificação".
Outras questões recapituladas
Acredito que outros já apontaram suficientemente algumas das outras áreas de preocupação comumente consideradas:
Injeção de SQL (pegar o que pareceria ser informações fornecidas pelo usuário e incluí-las no texto SQL em vez de fornecê-las através de variáveis de ligação. O uso de variáveis de ligação não é necessário, é apenas uma abordagem conveniente para impedir a injeção de SQL. Existem outras maneiras de lidar com isso:
plano de otimizador usando varredura de índice em vez de buscas de índice, possível necessidade de uma expressão ou função para escapar caracteres curinga (possível índice na expressão ou função)
o uso de valores literais no lugar de variáveis de ligação afeta a escalabilidade
Conclusão
Eu gosto da abordagem de Joel Spolsky. É esperto. E isso funciona.
Mas assim que o vi, imediatamente vi um problema em potencial, e não é da minha natureza deixá-lo escapar. Não quero criticar os esforços dos outros. Sei que muitos desenvolvedores levam o trabalho muito para o lado pessoal, porque investem muito nele e se preocupam muito com isso. Então, por favor, entenda, este não é um ataque pessoal. O que estou identificando aqui é o tipo de problema que surge na produção, em vez de testar.
Sim, fui longe da pergunta original. Mas onde mais deixar esta nota sobre o que considero uma questão importante com a resposta "selecionada" para uma pergunta?
fonte
Você pode passar o parâmetro como uma string
Então você tem a corda
Então tudo que você precisa fazer é passar a string como 1 parâmetro.
Aqui está a função de divisão que eu uso.
fonte
Ouvi Jeff / Joel falar sobre isso no podcast hoje ( episódio 34 , 2008-12-16 (MP3, 31 MB), 1 h 03 min 38 s - 1 h 06 min 45 s) e pensei em me lembrar de Stack Overflow estava usando LINQ to SQL , mas talvez tenha sido abandonado. Aqui está a mesma coisa no LINQ to SQL.
É isso aí. E, sim, o LINQ já olha para trás o suficiente, mas a
Contains
cláusula parece extra para mim. Quando tive que fazer uma consulta semelhante para um projeto no trabalho, naturalmente tentei fazer isso da maneira errada, fazendo uma junção entre a matriz local e a tabela do SQL Server, imaginando que o tradutor LINQ to SQL seria inteligente o suficiente para lidar com o tradução de alguma forma. Não foi, mas forneceu uma mensagem de erro descritiva e me indicou o uso de Contains .De qualquer forma, se você executar isso no LINQPad altamente recomendado e executar esta consulta, poderá visualizar o SQL real que o provedor SQL LINQ gerou. Ele mostrará cada um dos valores sendo parametrizados em uma
IN
cláusula.fonte
Se você estiver ligando do .NET, poderá usar o Dapper dot net :
Aqui Dapper pensa, então você não precisa. Algo semelhante é possível com o LINQ to SQL , é claro:
fonte
Esta é possivelmente uma maneira meio desagradável de fazê-lo, eu usei uma vez, foi bastante eficaz.
Dependendo dos seus objetivos, pode ser útil.
INSERT
cada valor de pesquisa nessa coluna.IN
, você pode apenas usar suasJOIN
regras padrão . (Flexibilidade ++)Isso tem um pouco de flexibilidade adicional no que você pode fazer, mas é mais adequado para situações em que você tem uma tabela grande para consultar, com boa indexação e deseja usar a lista parametrizada mais de uma vez. Economiza a execução duas vezes e todo o saneamento é feito manualmente.
Eu nunca cheguei a traçar exatamente o quão rápido era, mas na minha situação era necessário.
fonte
Em
SQL Server 2016+
você poderia usar aSTRING_SPLIT
função:ou:
Demonstração ao vivo
A resposta aceita funcionará, é claro, e é um caminho a percorrer, mas é antipadrão.
Adendo :
Para melhorar a
STRING_SPLIT
estimativa de linha da função de tabela, é uma boa ideia materializar valores divididos como variável temporária de tabela / tabela:SEDE - Demonstração ao vivo
Relacionado: Como passar uma lista de valores para um procedimento armazenado
A pergunta original tem requisito
SQL Server 2008
. Como essa pergunta é frequentemente usada como duplicada, adicionei esta resposta como referência.fonte
Temos uma função que cria uma variável de tabela à qual você pode se associar:
Assim:
fonte
Isso é nojento, mas se você tiver pelo menos uma garantia, poderá:
Ter IN ('tag1', 'tag2', 'tag1', 'tag1', 'tag1') será facilmente otimizado pelo SQL Server. Além disso, você obtém pesquisas diretas de índice
fonte
Na minha opinião, a melhor fonte para resolver este problema, é o que foi publicado neste site:
Syscomments. Dinakar Nethi
Usar:
CRÉDITOS PARA: Dinakar Nethi
fonte
Eu passaria um parâmetro do tipo de tabela (já que é o SQL Server 2008 ) e faria uma
where exists
junção interna. Você também pode usar XML, usandosp_xml_preparedocument
e até mesmo indexar essa tabela temporária.fonte
A maneira correta de IMHO é armazenar a lista em uma cadeia de caracteres (comprimento limitado pelo que o DBMS suporta); o único truque é que (para simplificar o processamento) eu tenho um separador (uma vírgula no meu exemplo) no início e no final da string. A idéia é "normalizar rapidamente", transformando a lista em uma tabela de uma coluna que contém uma linha por valor. Isso permite que você ligue
em um
ou (a solução que eu provavelmente preferiria) uma junção regular, se você adicionar uma "distinta" para evitar problemas com valores duplicados na lista.
Infelizmente, as técnicas para cortar uma string são bastante específicas do produto. Aqui está a versão do SQL Server:
A versão do Oracle:
e a versão do MySQL:
(É claro que "pivô" deve retornar tantas linhas quanto o número máximo de itens que podemos encontrar na lista)
fonte
Se você possui o SQL Server 2008 ou posterior, eu usaria um parâmetro com valor de tabela .
Se você tiver a sorte de ficar preso no SQL Server 2005, poderá adicionar uma função CLR como esta,
Que você poderia usar assim,
fonte
Eu acho que este é um caso em que uma consulta estática não é apenas o caminho a percorrer. Crie dinamicamente a lista para sua cláusula in, escape suas aspas simples e crie SQL dinamicamente. Nesse caso, você provavelmente não verá muita diferença em nenhum método devido à pequena lista, mas o método mais eficiente é realmente enviar o SQL exatamente como está escrito em sua postagem. Eu acho que é um bom hábito escrevê-lo da maneira mais eficiente, em vez de fazer o que torna o código mais bonito, ou considerar uma prática recomendada a criação dinâmica de SQL.
Vi que as funções de divisão demoram mais para serem executadas do que a própria consulta em muitos casos em que os parâmetros ficam grandes. Um procedimento armazenado com parâmetros com valor de tabela no SQL 2008 é a única outra opção que eu consideraria, embora isso provavelmente seja mais lento no seu caso. O TVP provavelmente será mais rápido para listas grandes apenas se você estiver pesquisando na chave primária do TVP, porque o SQL criará uma tabela temporária para a lista de qualquer maneira (se a lista for grande). Você não terá certeza, a menos que o teste.
Também vi procedimentos armazenados que tinham 500 parâmetros com valores padrão nulos e com WHERE Column1 IN (@ Param1, @ Param2, @ Param3, ..., @ Param500). Isso fez com que o SQL construa uma tabela temporária, faça uma classificação / distinção e faça uma varredura de tabela em vez de uma busca de índice. Isso é essencialmente o que você faria ao parametrizar essa consulta, embora em uma escala pequena o suficiente para que não faça uma diferença perceptível. Eu recomendo não ter NULL em suas listas IN, como se isso fosse alterado para NOT NOT, não funcionará como pretendido. Você poderia criar dinamicamente a lista de parâmetros, mas a única coisa óbvia que você obteria é que os objetos escapariam das aspas simples para você. Essa abordagem também é um pouco mais lenta no final do aplicativo, pois os objetos precisam analisar a consulta para encontrar os parâmetros.
A reutilização de planos de execução para procedimentos armazenados ou consultas parametrizadas pode proporcionar um ganho de desempenho, mas o bloqueará em um plano de execução determinado pela primeira consulta executada. Isso pode ser abaixo do ideal para consultas subseqüentes em muitos casos. No seu caso, a reutilização dos planos de execução provavelmente será uma vantagem, mas pode não fazer nenhuma diferença, pois o exemplo é uma consulta realmente simples.
Notas sobre falésias:
Para o seu caso, qualquer coisa que você faça, seja a parametrização com um número fixo de itens na lista (nulo se não for usado), a construção dinâmica da consulta com ou sem parâmetros ou o uso de procedimentos armazenados com parâmetros com valor de tabela não farão muita diferença . No entanto, minhas recomendações gerais são as seguintes:
Seu caso / consultas simples com alguns parâmetros:
SQL dinâmico, talvez com parâmetros, se o teste mostrar melhor desempenho.
Consultas com planos de execução reutilizáveis, chamados várias vezes, simplesmente alterando os parâmetros ou se a consulta é complicada:
SQL com parâmetros dinâmicos.
Consultas com listas grandes:
Procedimento armazenado com parâmetros com valor de tabela. Se a lista puder variar bastante, use WITH RECOMPILE no procedimento armazenado ou simplesmente use SQL dinâmico sem parâmetros para gerar um novo plano de execução para cada consulta.
fonte
Pode ser que possamos usar XML aqui:
fonte
CTE
e@x
pode ser eliminado / incorporado na subseleção, se feito com muito cuidado, conforme mostrado neste artigo .Eu abordaria isso por padrão, passando uma função com valor de tabela (que retorna uma tabela de uma string) para a condição IN.
Aqui está o código para o UDF (eu o peguei no Stack Overflow em algum lugar, não consigo encontrar a fonte no momento)
Depois de conseguir isso, seu código seria tão simples quanto isto:
A menos que você tenha uma cadeia ridiculamente longa, isso deve funcionar bem com o índice da tabela.
Se necessário, você pode inseri-lo em uma tabela temporária, indexá-lo e executar uma junção ...
fonte
Outra solução possível é, em vez de passar um número variável de argumentos para um procedimento armazenado, passar uma única string contendo os nomes que você procura, mas torná-los únicos cercando-os com '<>'. Em seguida, use PATINDEX para encontrar os nomes:
fonte
Use o seguinte procedimento armazenado. Ele usa uma função de divisão personalizada, que pode ser encontrada aqui .
fonte
Se tivermos cadeias armazenadas dentro da cláusula IN com a vírgula (,) delimitada, podemos usar a função charindex para obter os valores. Se você usa o .NET, pode mapear com SqlParameters.
Script DDL:
T-SQL:
Você pode usar a instrução acima no seu código .NET e mapear o parâmetro com SqlParameter.
Demonstração do violinista
EDIT: Crie a tabela SelectedTags usando o seguinte script.
Script DDL:
T-SQL:
fonte
Para um número variável de argumentos como este, a única maneira que eu conheço é gerar o SQL explicitamente ou fazer algo que envolva preencher uma tabela temporária com os itens desejados e ingressar na tabela temporária.
fonte
No ColdFusion , apenas fazemos:
fonte
Aqui está uma técnica que recria uma tabela local para ser usada em uma string de consulta. Fazer dessa maneira elimina todos os problemas de análise.
A string pode ser criada em qualquer idioma. Neste exemplo, usei o SQL, pois esse era o problema original que estava tentando resolver. Eu precisava de uma maneira limpa de passar os dados da tabela rapidamente em uma string para ser executada mais tarde.
O uso de um tipo definido pelo usuário é opcional. A criação do tipo é criada apenas uma vez e pode ser feita com antecedência. Caso contrário, basta adicionar um tipo de tabela completo à declaração na string.
O padrão geral é fácil de estender e pode ser usado para passar tabelas mais complexas.
fonte
No SQL Server 2016 ou superior, outra possibilidade é usar a
OPENJSON
funçãoEssa abordagem é publicada em blog no OPENJSON - uma das melhores maneiras de selecionar linhas por lista de IDs .
Um exemplo completo abaixo
fonte
Aqui está outra alternativa. Basta passar uma lista delimitada por vírgula como um parâmetro de string para o procedimento armazenado e:
E a função:
fonte
Eu tenho uma resposta que não requer um UDF, XML, porque IN aceita uma instrução select, por exemplo, SELECT * FROM Teste em que Data IN (SELECT Value FROM TABLE)
Você realmente só precisa de uma maneira de converter a string em uma tabela.
Isso pode ser feito com um CTE recursivo ou com uma tabela numérica (ou Master..spt_value)
Aqui está a versão CTE.
fonte
Eu uso uma versão mais concisa da principal resposta votada :
Ele percorre os parâmetros do tag duas vezes; mas isso não importa na maioria das vezes (não será o seu gargalo; se for, desenrole o loop).
Se você está realmente interessado em desempenho e não deseja percorrer o loop duas vezes, aqui está uma versão menos bonita:
fonte
Aqui está outra resposta para esse problema.
(nova versão publicada em 4/6/13).
Felicidades.
fonte
O único movimento vencedor é não jogar.
Nenhuma variabilidade infinita para você. Somente variabilidade finita.
No SQL, você tem uma cláusula como esta:
No código C #, você faz algo assim:
Então, basicamente, se a contagem é 0, então não há filtro e tudo passa. Se a contagem for maior que 0, o valor deverá estar na lista, mas a lista foi aumentada para cinco com valores impossíveis (para que o SQL ainda faça sentido)
Às vezes, a solução coxa é a única que realmente funciona.
fonte