Parametrizar uma cláusula SQL IN


1041

Como parametrizar uma consulta que contém uma INclá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.



Respostas:


315

Aqui está uma técnica rápida e suja que eu usei:

SELECT * FROM Tags
WHERE '|ruby|rails|scruffy|rubyonrails|'
LIKE '%|' + Name + '|%'

Então aqui está o código C #:

string[] tags = new string[] { "ruby", "rails", "scruffy", "rubyonrails" };
const string cmdText = "select * from tags where '|' + @tags + '|' like '%|' + Name + '|%'";

using (SqlCommand cmd = new SqlCommand(cmdText)) {
   cmd.Parameters.AddWithValue("@tags", string.Join("|", tags);
}

Duas advertências:

  • O desempenho é terrível. LIKE "%...%"consultas não são indexadas.
  • Verifique se você não possui nenhuma |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.


119
Isso vai ser hella lento
Matt Rogish

13
Sim, esta é uma varredura de tabela. Ótimo para 10 linhas, péssimo para 100.000.
Will Hartung

17
Certifique-se de testar as tags que possuem tubos.
Joel Coehoorn

17
Isso nem mesmo responde à pergunta. Concedido, é fácil ver onde adicionar os parâmetros, mas como você pode aceitar isso como uma solução se nem se importa em parametrizar a consulta? Parece apenas mais simples do que o @Mark Brackett porque não está parametrizado.
tvanfosson 03/12/08

21
E se sua tag for 'ruby | rails'. Vai corresponder, o que estará errado. Ao implantar essas soluções, você precisa garantir que as tags não contenham tubos ou filtrá-las explicitamente: selecione * em Tags em que '| ruby ​​| trilhos | desalinhado | rubyonrails |' como '% |' + Nome + '|%' E o nome não gosta de '%!%' #
197/08 AK

729

Você pode parametrizar cada valor, então algo como:

string[] tags = new string[] { "ruby", "rails", "scruffy", "rubyonrails" };
string cmdText = "SELECT * FROM Tags WHERE Name IN ({0})";

string[] paramNames = tags.Select(
    (s, i) => "@tag" + i.ToString()
).ToArray();

string inClause = string.Join(", ", paramNames);
using (SqlCommand cmd = new SqlCommand(string.Format(cmdText, inClause))) {
    for(int i = 0; i < paramNames.Length; i++) {
       cmd.Parameters.AddWithValue(paramNames[i], tags[i]);
    }
}

O que lhe dará:

cmd.CommandText = "SELECT * FROM Tags WHERE Name IN (@tag0, @tag1, @tag2, @tag3)"
cmd.Parameters["@tag0"] = "ruby"
cmd.Parameters["@tag1"] = "rails"
cmd.Parameters["@tag2"] = "scruffy"
cmd.Parameters["@tag3"] = "rubyonrails"

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:

Questões sobre injeção à parte, observe que a construção do texto de comando para acomodar um número variável de parâmetros (como acima) impede a capacidade do servidor SQL de tirar proveito das consultas em cache. O resultado líquido é que você quase certamente perde o valor de usar parâmetros em primeiro lugar (em vez de simplesmente inserir as seqüências de caracteres predicadas no próprio SQL).

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.


2
Basicamente, o mesmo que a minha resposta à pergunta "relacionada" e, obviamente, a melhor solução, pois é construtiva e eficiente, e não interpretativa (muito mais difícil).
tvanfosson 03/12/08

49
É assim que o LINQ to SQL faz isso, BTW
Mark Cidade

3
@ Pure: O ponto principal disso é evitar a injeção de SQL, à qual você estaria vulnerável se usasse o SQL dinâmico.
Raio

4
@ Deus dos Dados - Sim, suponho que se você precisar de mais de 2100 tags, precisará de uma solução diferente. Mas o Basarb só poderia chegar a 2100 se o tamanho médio da tag fosse <3 caracteres (já que você também precisa de um delimitador). msdn.microsoft.com/pt-br/library/ms143432.aspx
Mark Brackett

2
@bonCodigo - seus valores selecionados estão em uma matriz; basta fazer um loop sobre o array e adicionar um parâmetro (com o sufixo do índice) para cada um.
Mark Brackett

249

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

CREATE TYPE dbo.TagNamesTableType AS TABLE ( Name nvarchar(50) )

Em seguida, seu código ADO.NET fica assim:

string[] tags = new string[] { "ruby", "rails", "scruffy", "rubyonrails" };
cmd.CommandText = "SELECT Tags.* FROM Tags JOIN @tagNames as P ON Tags.Name = P.Name";

// value must be IEnumerable<SqlDataRecord>
cmd.Parameters.AddWithValue("@tagNames", tags.AsSqlDataRecord("Name")).SqlDbType = SqlDbType.Structured;
cmd.Parameters["@tagNames"].TypeName = "dbo.TagNamesTableType";

// Extension method for converting IEnumerable<string> to IEnumerable<SqlDataRecord>
public static IEnumerable<SqlDataRecord> AsSqlDataRecord(this IEnumerable<string> values, string columnName) {
    if (values == null || !values.Any()) return null; // Annoying, but SqlClient wants null instead of 0 rows
    var firstRecord = values.First();
    var metadata = SqlMetaData.InferFromValue(firstRecord, columnName);
    return values.Select(v => 
    {
       var r = new SqlDataRecord(metadata);
       r.SetValues(v);
       return r;
    });
}

41
testamos isso e os parâmetros com valor de tabela são DOG lento. É literalmente mais rápido executar 5 consultas do que fazer um TVP.
Jeff Atwood

4
@ JeffAtwood - Você já tentou reorganizar a consulta para algo como 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 seja INCLUDEo número ideal) e o SQL Server deve fazer algumas tentativas para capturar todas as tags e suas contagens. Como é o plano?
Nick Chammas

9
Também testei isso e é RÁPIDO COMO RELÂMPAGO (em comparação com a construção de uma grande string IN). Eu tive alguns problemas ao definir o parâmetro, pois estava constantemente recebendo "Falha ao converter o valor do parâmetro de um Int32 [] para um IEnumerable`1". De qualquer forma, resolvido isso e aqui está um exemplo que eu fiz pastebin.com/qHP05CXc
Fredrik Johansson

6
@FredrikJohansson - De 130 upvotes, você pode ser a única corrida que realmente tentou executar isso! Cometi um erro ao ler os documentos e você realmente precisa de um IEnumerable <SqlDataRecord>, não apenas de um IEnumerable. O código foi atualizado.
Mark Brackett

3
@MarkBrackett Ótimo com uma atualização! Na verdade, esse código realmente salvou o dia para mim, pois estou consultando um índice de pesquisa Lucene e, às vezes, retorna mais de 50.000 ou mais ocorrências que precisam ser verificadas duas vezes no servidor SQL - por isso, crio uma matriz de int [] (document / chaves SQL) e, em seguida, o código acima vem em todo o OP agora leva menos de 200ms :).
Fredrik Johansson

188

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 Namecoluna 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:

select ...
 where '|peanut|butter|' like '%|' + 'pe%ter' + '|%'

Mas essa mesma linha não será retornada se a ordem dos termos da pesquisa for revertida:

select ...
 where '|butter|peanut|' like '%|' + 'pe%ter' + '|%'

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%tercombinar 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 LIKEoperador 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 .

select ...
 where '|peanut|butter|'
  like '%|' + 'pe\%ter' + '|%' escape '\'

Agora podemos combinar o literal%. Obviamente, quando temos um nome de coluna, precisamos escapar dinamicamente do curinga. Podemos usar a REPLACEfunção para encontrar ocorrências do %caractere e inserir um caractere de barra invertida na frente de cada um, assim:

select ...
 where '|pe%ter|'
  like '%|' + REPLACE( 'pe%ter' ,'%','\%') + '|%' escape '\'

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:

select ...
 where '|pe%t!r|'
  like '%|' + REPLACE(REPLACE( 'pe%t!r' ,'!','!!'),'%','!%') + '|%' escape '!'

O sublinhado também

Agora que estamos em um rolo, podemos adicionar outro REPLACEidentificador ao curinga de sublinhado. E só por diversão, desta vez, usaremos $ como o caractere de escape.

select ...
 where '|p_%t!r|'
  like '%|' + REPLACE(REPLACE(REPLACE( 'p_%t!r' ,'$','$$'),'%','$%'),'_','$_') + '|%' 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?


você pode nos informar se usa ou gosta de consultas parametrizadas? neste caso em particular, é correto pular a regra de 'usar consultas parametrizadas' e higienizar com o idioma original? Muito obrigado
Luis Siquot

2
@ Luis: sim, eu prefiro usar variáveis ​​de ligação em instruções SQL e evitarei apenas variáveis ​​de ligação quando usá-las causar um problema de desempenho. meu padrão normativo para o problema original seria criar dinamicamente a instrução SQL com o número necessário de espaços reservados na lista IN e vincular cada valor a um dos espaços reservados. Veja a resposta de Mark Brackett, que é a resposta que eu (e 231 outros) votaram.
precisa saber é o seguinte

133

Você pode passar o parâmetro como uma string

Então você tem a corda

DECLARE @tags

SET @tags = ruby|rails|scruffy|rubyonrails

select * from Tags 
where Name in (SELECT item from fnSplit(@tags, ‘|’))
order by Count desc

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.

CREATE FUNCTION [dbo].[fnSplit](
    @sInputList VARCHAR(8000) -- List of delimited items
  , @sDelimiter VARCHAR(8000) = ',' -- delimiter that separates items
) RETURNS @List TABLE (item VARCHAR(8000))

BEGIN
DECLARE @sItem VARCHAR(8000)
WHILE CHARINDEX(@sDelimiter,@sInputList,0) <> 0
 BEGIN
 SELECT
  @sItem=RTRIM(LTRIM(SUBSTRING(@sInputList,1,CHARINDEX(@sDelimiter,@sInputList,0)-1))),
  @sInputList=RTRIM(LTRIM(SUBSTRING(@sInputList,CHARINDEX(@sDelimiter,@sInputList,0)+LEN(@sDelimiter),LEN(@sInputList))))

 IF LEN(@sItem) > 0
  INSERT INTO @List SELECT @sItem
 END

IF LEN(@sInputList) > 0
 INSERT INTO @List SELECT @sInputList -- Put the last item in
RETURN
END

2
Você também pode ingressar na função de tabela com essa abordagem.
Michael Haren

Eu uso uma solução semelhante a esta no Oracle. Ele não precisa ser analisado novamente, como algumas das outras soluções.
Leigh Riffel

9
Essa é uma abordagem de banco de dados pura, a outra exige trabalho no código fora do banco de dados.
David Basarab

Isso faz uma varredura de tabela ou pode tirar proveito de índices etc.?
Pure.Krome

melhor seria usar CROSS APLICAR contra a função de tabela SQL (pelo menos em 2005 em diante), que essencialmente se junta contra a mesa que é devolvido
Adolf alho

66

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.

var inValues = new [] { "ruby","rails","scruffy","rubyonrails" };

var results = from tag in Tags
              where inValues.Contains(tag.Name)
              select tag;

É isso aí. E, sim, o LINQ já olha para trás o suficiente, mas a Containsclá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 INcláusula.


50

Se você estiver ligando do .NET, poderá usar o Dapper dot net :

string[] names = new string[] {"ruby","rails","scruffy","rubyonrails"};
var tags = dataContext.Query<Tags>(@"
select * from Tags 
where Name in @names
order by Count desc", new {names});

Aqui Dapper pensa, então você não precisa. Algo semelhante é possível com o LINQ to SQL , é claro:

string[] names = new string[] {"ruby","rails","scruffy","rubyonrails"};
var tags = from tag in dataContext.Tags
           where names.Contains(tag.Name)
           orderby tag.Count descending
           select tag;

11
que passa a ser o que usamos nesta página, para a questão real perguntou (dapper) i.stack.imgur.com/RBAjL.png
Sam Saffron


Isso cai se os nomes forem longos
cs0815

29

Esta é possivelmente uma maneira meio desagradável de fazê-lo, eu usei uma vez, foi bastante eficaz.

Dependendo dos seus objetivos, pode ser útil.

  1. Crie uma tabela temporária com uma coluna.
  2. INSERT cada valor de pesquisa nessa coluna.
  3. Em vez de usar um IN, você pode apenas usar suas JOINregras 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.


Isso não é nada desagradável! Ainda mais, é IMHO uma maneira muito limpa. E se você examinar o plano de execução, verá que é o mesmo que a cláusula IN. Em vez de uma tabela temporária, você também pode criar uma tabela fixa com índices, onde armazena os parâmetros juntamente com o SESSIONID.
Polícia SQL

27

Em SQL Server 2016+você poderia usar a STRING_SPLITfunção:

DECLARE @names NVARCHAR(MAX) = 'ruby,rails,scruffy,rubyonrails';

SELECT * 
FROM Tags
WHERE Name IN (SELECT [value] FROM STRING_SPLIT(@names, ','))
ORDER BY [Count] DESC;

ou:

DECLARE @names NVARCHAR(MAX) = 'ruby,rails,scruffy,rubyonrails';

SELECT t.*
FROM Tags t
JOIN STRING_SPLIT(@names,',')
  ON t.Name = [value]
ORDER BY [Count] DESC;

Demonstração ao vivo

A resposta aceita funcionará, é claro, e é um caminho a percorrer, mas é antipadrão.

E. Encontre linhas por lista de valores

Isso substitui o antipadrão comum, como a criação de uma sequência dinâmica de SQL na camada de aplicativo ou no Transact-SQL ou usando o operador LIKE:

SELECT ProductId, Name, Tags
FROM Product
WHERE ',1,2,3,' LIKE '%,' + CAST(ProductId AS VARCHAR(20)) + ',%';

Adendo :

Para melhorar a STRING_SPLITestimativa de linha da função de tabela, é uma boa ideia materializar valores divididos como variável temporária de tabela / tabela:

DECLARE @names NVARCHAR(MAX) = 'ruby,rails,scruffy,rubyonrails,sql';

CREATE TABLE #t(val NVARCHAR(120));
INSERT INTO #t(val) SELECT s.[value] FROM STRING_SPLIT(@names, ',') s;

SELECT *
FROM Tags tg
JOIN #t t
  ON t.val = tg.TagName
ORDER BY [Count] DESC;

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.


1
Não testei isso com perfeição, mas sinto que essa é a solução mais limpa para 2016+. Eu ainda gostaria de ser capaz de simplesmente passar um array de int, mas até então ...
Daniel

24

Temos uma função que cria uma variável de tabela à qual você pode se associar:

ALTER FUNCTION [dbo].[Fn_sqllist_to_table](@list  AS VARCHAR(8000),
                                           @delim AS VARCHAR(10))
RETURNS @listTable TABLE(
  Position INT,
  Value    VARCHAR(8000))
AS
  BEGIN
      DECLARE @myPos INT

      SET @myPos = 1

      WHILE Charindex(@delim, @list) > 0
        BEGIN
            INSERT INTO @listTable
                        (Position,Value)
            VALUES     (@myPos,LEFT(@list, Charindex(@delim, @list) - 1))

            SET @myPos = @myPos + 1

            IF Charindex(@delim, @list) = Len(@list)
              INSERT INTO @listTable
                          (Position,Value)
              VALUES     (@myPos,'')

            SET @list = RIGHT(@list, Len(@list) - Charindex(@delim, @list))
        END

      IF Len(@list) > 0
        INSERT INTO @listTable
                    (Position,Value)
        VALUES     (@myPos,@list)

      RETURN
  END 

Assim:

@Name varchar(8000) = null // parameter for search values    

select * from Tags 
where Name in (SELECT value From fn_sqllist_to_table(@Name,',')))
order by Count desc

20

Isso é nojento, mas se você tiver pelo menos uma garantia, poderá:

SELECT ...
       ...
 WHERE tag IN( @tag1, ISNULL( @tag2, @tag1 ), ISNULL( @tag3, @tag1 ), etc. )

Ter IN ('tag1', 'tag2', 'tag1', 'tag1', 'tag1') será facilmente otimizado pelo SQL Server. Além disso, você obtém pesquisas diretas de índice


1
Parâmetros opcionais com verificações nulas prejudicam o desempenho, pois o otimizador requer o número de parâmetros usados ​​para criar consultas eficientes. Uma consulta para 5 parâmetros pode precisar de um plano de consulta diferente de um para 500 parâmetros.
Erik Hart

18

Na minha opinião, a melhor fonte para resolver este problema, é o que foi publicado neste site:

Syscomments. Dinakar Nethi

CREATE FUNCTION dbo.fnParseArray (@Array VARCHAR(1000),@separator CHAR(1))
RETURNS @T Table (col1 varchar(50))
AS 
BEGIN
 --DECLARE @T Table (col1 varchar(50))  
 -- @Array is the array we wish to parse
 -- @Separator is the separator charactor such as a comma
 DECLARE @separator_position INT -- This is used to locate each separator character
 DECLARE @array_value VARCHAR(1000) -- this holds each array value as it is returned
 -- For my loop to work I need an extra separator at the end. I always look to the
 -- left of the separator character for each array value

 SET @array = @array + @separator

 -- Loop through the string searching for separtor characters
 WHILE PATINDEX('%' + @separator + '%', @array) <> 0 
 BEGIN
    -- patindex matches the a pattern against a string
    SELECT @separator_position = PATINDEX('%' + @separator + '%',@array)
    SELECT @array_value = LEFT(@array, @separator_position - 1)
    -- This is where you process the values passed.
    INSERT into @T VALUES (@array_value)    
    -- Replace this select statement with your processing
    -- @array_value holds the value of this element of the array
    -- This replaces what we just processed with and empty string
    SELECT @array = STUFF(@array, 1, @separator_position, '')
 END
 RETURN 
END

Usar:

SELECT * FROM dbo.fnParseArray('a,b,c,d,e,f', ',')

CRÉDITOS PARA: Dinakar Nethi


Ótima resposta, execução limpa e modular, super rápida, exceto pelo CSV inicial analisando em uma tabela (uma vez, pequeno número de elementos). Embora possa usar charindex () mais simples / mais rápido em vez de patindex ()? Charindex () também permite o argumento 'start_location', que pode evitar cortar a string de entrada a cada iter? Para responder à pergunta original, basta juntar-se ao resultado da função.
crokusek

18

Eu passaria um parâmetro do tipo de tabela (já que é o SQL Server 2008 ) e faria uma where existsjunção interna. Você também pode usar XML, usando sp_xml_preparedocumente até mesmo indexar essa tabela temporária.


A resposta de Ph.E tem um exemplo de construção de tabela temporária (de csv).
crokusek

12

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 (ct1, ct2, ct3 ... ctn)

em um

in (selecione ...)

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:

 with qry(n, names) as
       (select len(list.names) - len(replace(list.names, ',', '')) - 1 as n,
               substring(list.names, 2, len(list.names)) as names
        from (select ',Doc,Grumpy,Happy,Sneezy,Bashful,Sleepy,Dopey,' names) as list
        union all
        select (n - 1) as n,
               substring(names, 1 + charindex(',', names), len(names)) as names
        from qry
        where n > 1)
 select n, substring(names, 1, charindex(',', names) - 1) dwarf
 from qry;

A versão do Oracle:

 select n, substr(name, 1, instr(name, ',') - 1) dwarf
 from (select n,
             substr(val, 1 + instr(val, ',', 1, n)) name
      from (select rownum as n,
                   list.val
            from  (select ',Doc,Grumpy,Happy,Sneezy,Bashful,Sleepy,Dopey,' val
                   from dual) list
            connect by level < length(list.val) -
                               length(replace(list.val, ',', ''))));

e a versão do MySQL:

select pivot.n,
      substring_index(substring_index(list.val, ',', 1 + pivot.n), ',', -1) from (select 1 as n
     union all
     select 2 as n
     union all
     select 3 as n
     union all
     select 4 as n
     union all
     select 5 as n
     union all
     select 6 as n
     union all
     select 7 as n
     union all
     select 8 as n
     union all
     select 9 as n
     union all
     select 10 as n) pivot,    (select ',Doc,Grumpy,Happy,Sneezy,Bashful,Sleepy,Dopey,' val) as list where pivot.n <  length(list.val) -
                   length(replace(list.val, ',', ''));

(É claro que "pivô" deve retornar tantas linhas quanto o número máximo de itens que podemos encontrar na lista)


11

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,

[SqlFunction(
    DataAccessKind.None,
    IsDeterministic = true,
    SystemDataAccess = SystemDataAccessKind.None,
    IsPrecise = true,
    FillRowMethodName = "SplitFillRow",
    TableDefinintion = "s NVARCHAR(MAX)"]
public static IEnumerable Split(SqlChars seperator, SqlString s)
{
    if (s.IsNull)
        return new string[0];

    return s.ToString().Split(seperator.Buffer);
}

public static void SplitFillRow(object row, out SqlString s)
{
    s = new SqlString(row.ToString());
}

Que você poderia usar assim,

declare @desiredTags nvarchar(MAX);
set @desiredTags = 'ruby,rails,scruffy,rubyonrails';

select * from Tags
where Name in [dbo].[Split] (',', @desiredTags)
order by Count desc

10

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.


O que você quer dizer com "procedimento armazenado" aqui? Você poderia postar um exemplo?
struhtanov

9

Pode ser que possamos usar XML aqui:

    declare @x xml
    set @x='<items>
    <item myvalue="29790" />
    <item myvalue="31250" />
    </items>
    ';
    With CTE AS (
         SELECT 
            x.item.value('@myvalue[1]', 'decimal') AS myvalue
        FROM @x.nodes('//items/item') AS x(item) )

    select * from YourTable where tableColumnName in (select myvalue from cte)

1
CTEe @xpode ser eliminado / incorporado na subseleção, se feito com muito cuidado, conforme mostrado neste artigo .
robert4

9

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)

CREATE FUNCTION [dbo].[Split] (@sep char(1), @s varchar(8000))
RETURNS table
AS
RETURN (
    WITH Pieces(pn, start, stop) AS (
      SELECT 1, 1, CHARINDEX(@sep, @s)
      UNION ALL
      SELECT pn + 1, stop + 1, CHARINDEX(@sep, @s, stop + 1)
      FROM Pieces
      WHERE stop > 0
    )
    SELECT 
      SUBSTRING(@s, start, CASE WHEN stop > 0 THEN stop-start ELSE 512 END) AS s
    FROM Pieces
  )

Depois de conseguir isso, seu código seria tão simples quanto isto:

select * from Tags 
where Name in (select s from dbo.split(';','ruby;rails;scruffy;rubyonrails'))
order by Count desc

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 ...


8

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:

SELECT * 
FROM Tags 
WHERE PATINDEX('%<' + Name + '>%','<jo>,<john>,<scruffy>,<rubyonrails>') > 0

8

Use o seguinte procedimento armazenado. Ele usa uma função de divisão personalizada, que pode ser encontrada aqui .

 create stored procedure GetSearchMachingTagNames 
    @PipeDelimitedTagNames varchar(max), 
    @delimiter char(1) 
    as  
    begin
         select * from Tags 
         where Name in (select data from [dbo].[Split](@PipeDelimitedTagNames,@delimiter) 
    end

8

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:

CREATE TABLE Tags
    ([ID] int, [Name] varchar(20))
;

INSERT INTO Tags
    ([ID], [Name])
VALUES
    (1, 'ruby'),
    (2, 'rails'),
    (3, 'scruffy'),
    (4, 'rubyonrails')
;

T-SQL:

DECLARE @Param nvarchar(max)

SET @Param = 'ruby,rails,scruffy,rubyonrails'

SELECT * FROM Tags
WHERE CharIndex(Name,@Param)>0

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:

Create table SelectedTags
(Name nvarchar(20));

INSERT INTO SelectedTags values ('ruby'),('rails')

T-SQL:

DECLARE @list nvarchar(max)
SELECT @list=coalesce(@list+',','')+st.Name FROM SelectedTags st

SELECT * FROM Tags
WHERE CharIndex(Name,@Param)>0

Você pode mostrar um exemplo desse trabalho em que não há uma lista codificada de valores possíveis?
John Saunders

@JohnSaunders, editei o script sem usar nenhuma lista codificada. Por favor verifique.
precisa saber é o seguinte

3
Uma limitação com esta opção. CharIndex retorna 1 se a sequência for encontrada. IN retorna uma correspondência para os termos exatos. CharIndex para "Stack" retornará 1 para o termo "StackOverflow" IN não. Há uma pequena revisão nesta resposta usando o PatIndex acima, que inclui nomes com '<'% name% '>' que supera essa limitação. Solução criativa para esse problema.
Richard Vivian

7

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.


7

No ColdFusion , apenas fazemos:

<cfset myvalues = "ruby|rails|scruffy|rubyonrails">
    <cfquery name="q">
        select * from sometable where values in <cfqueryparam value="#myvalues#" list="true">
    </cfquery>

7

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.

-- Create a user defined type for the list.
CREATE TYPE [dbo].[StringList] AS TABLE(
    [StringValue] [nvarchar](max) NOT NULL
)

-- Create a sample list using the list table type.
DECLARE @list [dbo].[StringList]; 
INSERT INTO @list VALUES ('one'), ('two'), ('three'), ('four')

-- Build a string in which we recreate the list so we can pass it to exec
-- This can be done in any language since we're just building a string.
DECLARE @str nvarchar(max);
SET @str = 'DECLARE @list [dbo].[StringList]; INSERT INTO @list VALUES '

-- Add all the values we want to the string. This would be a loop in C++.
SELECT @str = @str + '(''' + StringValue + '''),' FROM @list

-- Remove the trailing comma so the query is valid sql.
SET @str = substring(@str, 1, len(@str)-1)

-- Add a select to test the string.
SET @str = @str + '; SELECT * FROM @list;'

-- Execute the string and see we've pass the table correctly.
EXEC(@str)

7

No SQL Server 2016 ou superior, outra possibilidade é usar a OPENJSONfunção

Essa abordagem é publicada em blog no OPENJSON - uma das melhores maneiras de selecionar linhas por lista de IDs .

Um exemplo completo abaixo

CREATE TABLE dbo.Tags
  (
     Name  VARCHAR(50),
     Count INT
  )

INSERT INTO dbo.Tags
VALUES      ('VB',982), ('ruby',1306), ('rails',1478), ('scruffy',1), ('C#',1784)

GO

CREATE PROC dbo.SomeProc
@Tags VARCHAR(MAX)
AS
SELECT T.*
FROM   dbo.Tags T
WHERE  T.Name IN (SELECT J.Value COLLATE Latin1_General_CI_AS
                  FROM   OPENJSON(CONCAT('[', @Tags, ']')) J)
ORDER  BY T.Count DESC

GO

EXEC dbo.SomeProc @Tags = '"ruby","rails","scruffy","rubyonrails"'

DROP TABLE dbo.Tags 

7

Aqui está outra alternativa. Basta passar uma lista delimitada por vírgula como um parâmetro de string para o procedimento armazenado e:

CREATE PROCEDURE [dbo].[sp_myproc]
    @UnitList varchar(MAX) = '1,2,3'
AS
select column from table
where ph.UnitID in (select * from CsvToInt(@UnitList))

E a função:

CREATE Function [dbo].[CsvToInt] ( @Array varchar(MAX))
returns @IntTable table
(IntValue int)
AS
begin
    declare @separator char(1)
    set @separator = ','
    declare @separator_position int
    declare @array_value varchar(MAX)

    set @array = @array + ','

    while patindex('%,%' , @array) <> 0
    begin

        select @separator_position = patindex('%,%' , @array)
        select @array_value = left(@array, @separator_position - 1)

        Insert @IntTable
        Values (Cast(@array_value as int))
        select @array = stuff(@array, 1, @separator_position, '')
    end
    return
end

6

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.

DECLARE @InputString varchar(8000) = 'ruby,rails,scruffy,rubyonrails'

SELECT @InputString = @InputString + ','

;WITH RecursiveCSV(x,y) 
AS 
(
    SELECT 
        x = SUBSTRING(@InputString,0,CHARINDEX(',',@InputString,0)),
        y = SUBSTRING(@InputString,CHARINDEX(',',@InputString,0)+1,LEN(@InputString))
    UNION ALL
    SELECT 
        x = SUBSTRING(y,0,CHARINDEX(',',y,0)),
        y = SUBSTRING(y,CHARINDEX(',',y,0)+1,LEN(y))
    FROM 
        RecursiveCSV 
    WHERE
        SUBSTRING(y,CHARINDEX(',',y,0)+1,LEN(y)) <> '' OR 
        SUBSTRING(y,0,CHARINDEX(',',y,0)) <> ''
)
SELECT
    * 
FROM 
    Tags
WHERE 
    Name IN (select x FROM RecursiveCSV)
OPTION (MAXRECURSION 32767);

6

Eu uso uma versão mais concisa da principal resposta votada :

List<SqlParameter> parameters = tags.Select((s, i) => new SqlParameter("@tag" + i.ToString(), SqlDbType.NVarChar(50)) { Value = s}).ToList();

var whereCondition = string.Format("tags in ({0})", String.Join(",",parameters.Select(s => s.ParameterName)));

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:

var parameters = new List<SqlParameter>();
var paramNames = new List<string>();
for (var i = 0; i < tags.Length; i++)  
{
    var paramName = "@tag" + i;

    //Include size and set value explicitly (not AddWithValue)
    //Because SQL Server may use an implicit conversion if it doesn't know
    //the actual size.
    var p = new SqlParameter(paramName, SqlDbType.NVarChar(50) { Value = tags[i]; } 
    paramNames.Add(paramName);
    parameters.Add(p);
}

var inClause = string.Join(",", paramNames);

5

Aqui está outra resposta para esse problema.

(nova versão publicada em 4/6/13).

    private static DataSet GetDataSet(SqlConnectionStringBuilder scsb, string strSql, params object[] pars)
    {
        var ds = new DataSet();
        using (var sqlConn = new SqlConnection(scsb.ConnectionString))
        {
            var sqlParameters = new List<SqlParameter>();
            var replacementStrings = new Dictionary<string, string>();
            if (pars != null)
            {
                for (int i = 0; i < pars.Length; i++)
                {
                    if (pars[i] is IEnumerable<object>)
                    {
                        List<object> enumerable = (pars[i] as IEnumerable<object>).ToList();
                        replacementStrings.Add("@" + i, String.Join(",", enumerable.Select((value, pos) => String.Format("@_{0}_{1}", i, pos))));
                        sqlParameters.AddRange(enumerable.Select((value, pos) => new SqlParameter(String.Format("@_{0}_{1}", i, pos), value ?? DBNull.Value)).ToArray());
                    }
                    else
                    {
                        sqlParameters.Add(new SqlParameter(String.Format("@{0}", i), pars[i] ?? DBNull.Value));
                    }
                }
            }
            strSql = replacementStrings.Aggregate(strSql, (current, replacementString) => current.Replace(replacementString.Key, replacementString.Value));
            using (var sqlCommand = new SqlCommand(strSql, sqlConn))
            {
                if (pars != null)
                {
                    sqlCommand.Parameters.AddRange(sqlParameters.ToArray());
                }
                else
                {
                    //Fail-safe, just in case a user intends to pass a single null parameter
                    sqlCommand.Parameters.Add(new SqlParameter("@0", DBNull.Value));
                }
                using (var sqlDataAdapter = new SqlDataAdapter(sqlCommand))
                {
                    sqlDataAdapter.Fill(ds);
                }
            }
        }
        return ds;
    }

Felicidades.


4

O único movimento vencedor é não jogar.

Nenhuma variabilidade infinita para você. Somente variabilidade finita.

No SQL, você tem uma cláusula como esta:

and ( {1}==0 or b.CompanyId in ({2},{3},{4},{5},{6}) )

No código C #, você faz algo assim:

  int origCount = idList.Count;
  if (origCount > 5) {
    throw new Exception("You may only specify up to five originators to filter on.");
  }
  while (idList.Count < 5) { idList.Add(-1); }  // -1 is an impossible value
  return ExecuteQuery<PublishDate>(getValuesInListSQL, 
               origCount,   
               idList[0], idList[1], idList[2], idList[3], idList[4]);

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.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.