web-dev-qa-db-fra.com

Paramétrer une clause SQL IN

Comment paramétrer une requête contenant une clause IN avec un nombre variable d'arguments, comme celui-ci?

SELECT * FROM Tags 
WHERE Name IN ('Ruby','Rails','scruffy','rubyonrails')
ORDER BY Count DESC

Dans cette requête, le nombre d'arguments peut être compris entre 1 et 5.

Je préférerais ne pas utiliser de procédure stockée dédiée pour cela (ou XML), mais s'il existe une manière élégante spécifique à SQL Server 2008 , je suis ouvert à cela.

1007
Jeff Atwood

Voici une technique rapide et sale que j'ai utilisée:

SELECT * FROM Tags
WHERE '|Ruby|Rails|scruffy|rubyonrails|'
LIKE '%|' + Name + '|%'

Alors, voici le code 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);
}

Deux mises en garde:

  • La performance est terrible. Les requêtes LIKE "%...%" ne sont pas indexées.
  • Assurez-vous de ne pas avoir de balises |, vierges ou vides, sinon cela ne fonctionnera pas

Il y a d'autres moyens d'accomplir cela que certaines personnes peuvent considérer plus propres, alors continuez à lire.

306
Joel Spolsky

Vous pouvez paramétrer chaque valeur , donc quelque chose comme:

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]);
    }
}

Ce qui vous donnera:

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"

Non, ceci n'est pas ouvert à injection SQL . Le seul texte injecté dans CommandText n'est pas basé sur une entrée utilisateur. Il est uniquement basé sur le préfixe "@tag" codé en dur et sur l'index d'un tableau. L'indice sera toujours sous la forme d'un entier, n'est pas généré par l'utilisateur et est sécurisé.

Les valeurs entrées par l'utilisateur sont toujours intégrées dans les paramètres, il n'y a donc aucune vulnérabilité.

Modifier:

En ce qui concerne l’injection, veillez à noter que la construction du texte de la commande pour prendre en charge un nombre variable de paramètres (comme ci-dessus) empêche le serveur SQL de tirer parti des requêtes mises en cache. Le résultat net est que vous perdez presque certainement la valeur d'utiliser des paramètres (plutôt que d'insérer simplement les chaînes de prédicat dans le code SQL lui-même).

Non pas que les plans de requête en cache ne soient pas utiles, mais cette requête n’est pas assez compliquée pour en tirer beaucoup d’avantages. Bien que les coûts de compilation puissent s’approcher (voire même dépasser) les coûts d’exécution, vous parlez toujours en millisecondes.

Si vous disposez de suffisamment de RAM, SQL Server mettrait probablement également en cache un plan pour le nombre courant de paramètres. Je suppose que vous pouvez toujours ajouter cinq paramètres et laisser les balises non spécifiées être NULL - le plan de requête devrait être identique, mais cela me semble plutôt moche et je ne suis pas sûr que cela vaille la peine d'optimiser la micro-optimisation (bien que sur Stack Overflow - cela peut très bien en valoir la peine).

De plus, SQL Server 7 et les versions ultérieures seront requêtes auto-paramétrées , de sorte que l’utilisation de paramètres n’est pas vraiment nécessaire du point de vue des performances - elle est cependant critique du point de vue de la sécurité - en particulier avec les données saisies par l’utilisateur comme celle-ci.

710
Mark Brackett

Pour SQL Server 2008, vous pouvez utiliser un paramètre de valeur de table . C'est un peu de travail, mais c'est sans doute plus propre que mon autre méthode .

Tout d'abord, vous devez créer un type

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

Ensuite, votre code ADO.NET ressemble à ceci:

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;
    });
}
244
Mark Brackett

La question initiale était "Comment paramétrer une requête ..."

Permettez-moi de préciser ici que ceci n'est pas une réponse à la question initiale. Il existe déjà des démonstrations de cela dans d’autres bonnes réponses.

Cela dit, allez-y, signalez cette réponse, votez pour elle, notez-la comme une réponse ... faites ce que vous croyez être juste.

Voir la réponse de Mark Brackett pour la réponse préférée que moi (et 231 autres) ayons voté. L'approche donnée dans sa réponse permet 1) une utilisation efficace des variables liées, et 2) des prédicats qui sont sargables.

Réponse sélectionnée

Ce que je veux aborder ici est l’approche donnée dans la réponse de Joel Spolsky, la réponse "sélectionnée" comme étant la bonne réponse.

L'approche de Joel Spolsky est intelligente. Et cela fonctionne raisonnablement, il va présenter un comportement et des performances prévisibles, avec des valeurs "normales" et avec les cas Edge normatifs, tels que NULL et la chaîne vide. Et cela peut suffire pour une application particulière.

Mais en termes généraux, considérons également les cas les plus obscurs, comme lorsque la colonne Name contient un caractère générique (comme le reconnaît le prédicat LIKE.) Le caractère générique que je vois le plus couramment utilisé est % (un signe de pourcentage.). Parlons donc de cela ici maintenant, et plus tard, passons à d’autres cas.

Quelques problèmes avec le caractère%

Considérons une valeur de nom de 'pe%ter'. (Pour les exemples ici, j'utilise une valeur de chaîne littérale à la place du nom de la colonne.) Une ligne avec une valeur de Nom de "pe% ter" serait renvoyée par une requête de la forme:

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

Mais cette même ligne ne sera pas si l'ordre des termes de la recherche est inversé:

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

Le comportement que nous observons est un peu étrange. Changer l'ordre des termes de recherche dans la liste modifie le jeu de résultats.

Il va presque de soi que nous ne voulons peut-être pas que pe%ter soit identique au beurre de cacahuète, même s'il l'aime bien.

Coins obscurs

(Oui, je conviendrai qu'il s'agit d'un cas obscur. Probablement un cas peu susceptible d'être testé. Nous ne nous attendions pas à un caractère générique dans une valeur de colonne. Nous pouvons supposer que l'application empêche le stockage d'une telle valeur. Mais D'après mon expérience, j'ai rarement vu une contrainte de base de données qui interdisait spécifiquement les caractères ou les modèles considérés comme des caractères génériques à droite d'un opérateur de comparaison LIKE.

Corriger un trou

Une façon de corriger ce trou consiste à échapper au caractère générique %. (Pour ceux qui ne sont pas familiers avec la clause d'échappement de l'opérateur, voici un lien vers documentation de SQL Server .

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

Nous pouvons maintenant faire correspondre le% littéral. Bien sûr, lorsque nous avons un nom de colonne, nous allons devoir échapper dynamiquement au caractère générique. Nous pouvons utiliser la fonction REPLACE pour rechercher les occurrences du caractère % et insérer un caractère de barre oblique inversée devant chaque caractère, comme ceci:

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

Donc, cela résout le problème avec le caractère générique%. Presque.

Échapper à l'évasion

Nous reconnaissons que notre solution a introduit un autre problème. Le personnage d'évasion. Nous voyons que nous allons également avoir besoin d'échapper à toutes les occurrences du personnage d'évasion lui-même. Cette fois, nous utilisons le! en tant que personnage d'échappement:

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

Le trait de soulignement aussi

Maintenant que nous sommes lancés, nous pouvons ajouter un autre REPLACE gérer le caractère générique de soulignement. Et juste pour le fun, cette fois, nous utiliserons $ comme caractère d'échappement.

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

Je préfère cette approche à l’échappement car elle fonctionne aussi bien dans Oracle et MySQL que dans SQL Server. (J'utilise généralement le\backslash comme caractère d'échappement, car c'est le caractère que nous utilisons dans les expressions régulières. Mais pourquoi être contraint par la convention!

Ces parenthèses embêtantes

SQL Server permet également de traiter les caractères génériques comme des littéraux en les mettant entre crochets []. Nous n’avons donc pas encore fini de réparer, du moins pour SQL Server. Les paires de crochets ayant une signification particulière, nous devons également y échapper. Si nous parvenons à échapper correctement aux crochets, nous n'aurons au moins pas à nous préoccuper du trait d'union - et du carat ^ entre crochets. Et nous pouvons laisser tous les caractères % et _ entre crochets, car nous aurons en principe désactivé la signification particulière des crochets.

Trouver des paires de crochets identiques ne devrait pas être si difficile. C'est un peu plus difficile que de gérer les occurrences de singleton% et _. (Notez qu'il n'est pas suffisant d'échapper à toutes les occurrences de crochets, car un crochet singleton est considéré comme un littéral et n'a pas besoin d'être échappé. La logique devient un peu plus floue que ce que je peux gérer sans exécuter plus de scénarios de test. .)

L'expression en ligne devient confuse

Cette expression en ligne dans le SQL est de plus en plus longue et laide. Nous pouvons probablement le faire fonctionner, mais le ciel aide la pauvre âme qui vient derrière et doit le déchiffrer. En tant que fan, je suis pour les expressions en ligne, je suis enclin à ne pas en utiliser une ici, principalement parce que je ne veux pas avoir à laisser de commentaire expliquant la raison du désordre et en présentant des excuses.

Une fonction où?

Donc, si nous ne traitons pas cela comme une expression en ligne dans le code SQL, l’alternative la plus proche est une fonction définie par l’utilisateur. Et nous savons que cela ne va pas accélérer les choses (à moins de pouvoir définir un index dessus, comme avec Oracle.) Si nous devons créer une fonction, nous ferions mieux de le faire dans le code qui appelle SQL déclaration.

Et cette fonction peut avoir certaines différences de comportement, dépendant du SGBD et de la version. (Un grand merci à tous les Java développeurs, qui souhaitent pouvoir utiliser n'importe quel moteur de base de données de manière interchangeable.)

Connaissance du domaine

Nous pouvons posséder des connaissances spécialisées du domaine de la colonne (c'est-à-dire de l'ensemble des valeurs autorisées appliquées à la colonne. Nous pouvons savoir à priori que les valeurs stockées dans la colonne ne contiendra jamais de signe de pourcentage, de trait de soulignement ou de paire de crochets. Dans ce cas, nous incluons simplement un commentaire rapide indiquant que ces cas sont couverts.

Les valeurs stockées dans la colonne peuvent autoriser des caractères% ou _, mais une contrainte peut nécessiter un échappement de ces valeurs, éventuellement à l'aide d'un caractère défini, de sorte que les valeurs correspondent à une comparaison LIKE "sans danger". Encore une fois, un bref commentaire sur les valeurs autorisées, et en particulier sur le caractère utilisé comme caractère d'échappement, et va dans le sens de l'approche de Joel Spolsky.

Mais, en l'absence de connaissances spécialisées et d'une garantie, il est important pour nous de considérer au moins le traitement de ces cas obscurs, et de déterminer si le comportement est raisonnable et "conforme aux spécifications".


Autres questions récapitulées

Je pense que d’autres ont déjà suffisamment souligné certains des autres sujets de préoccupation généralement considérés:

  • injection SQL (en prenant ce qui semble être une information fournie par l'utilisateur, et l'inclure dans le texte SQL plutôt que de la fournir au moyen de variables de liaison. Utiliser des variables de liaison n'est pas obligatoire, ce n'est qu'une approche pratique pour contrecarrer avec injection SQL. Il existe d’autres solutions:

  • plan d'optimiseur utilisant l'analyse d'index plutôt que la recherche d'index, nécessité éventuelle d'une expression ou d'une fonction pour échapper des caractères génériques (index possible sur une expression ou une fonction)

  • l'utilisation de valeurs littérales au lieu de variables de liaison a un impact sur l'évolutivité


Conclusion

J'aime l'approche de Joel Spolsky. C'est malin. Et il fonctionne.

Mais dès que je l'ai vu, j'ai immédiatement vu un problème potentiel, et ce n'est pas dans ma nature de le laisser glisser. Je ne veux pas critiquer les efforts des autres. Je sais que beaucoup de développeurs prennent leur travail très à cœur, car ils y investissent beaucoup et s’y intéressent beaucoup. Alors s'il vous plaît comprendre, ce n'est pas une attaque personnelle. Ce que j'identifie ici, c'est le type de problème qui survient dans la production plutôt que dans les tests.

Oui, je suis loin de la question initiale. Mais où d'autre laisser cette note concernant ce que je considère être un problème important avec la réponse "sélectionnée" à une question?

183
spencer7593

Vous pouvez passer le paramètre sous forme de chaîne

Donc vous avez la ficelle

DECLARE @tags

SET @tags = ‘Ruby|Rails|scruffy|rubyonrails’

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

Ensuite, tout ce que vous avez à faire est de passer la chaîne en tant que 1 paramètre.

Voici la fonction split que j'utilise.

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
130
David Basarab

J'ai entendu Jeff/Joel en parler aujourd'hui sur le podcast (épisode 34, 2008-12-16 (MP3, 31 Mo), 1 h 03 min 38 s - 1 h 06 min 45 s), et je pensais me rappeler que Stack Overflow utilisait LINQ to SQL , mais peut-être a-t-il été abandonné. Voici la même chose dans LINQ to SQL.

var inValues = new [] { "Ruby","Rails","scruffy","rubyonrails" };

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

C'est ça. Et, oui, LINQ regarde déjà assez en arrière, mais la clause Contains me semble extra en arrière. Quand j'ai dû faire une requête similaire pour un projet au travail, j'ai naturellement essayé de le faire de manière incorrecte en faisant une jointure entre le tableau local et la table SQL Server, en pensant que le traducteur LINQ to SQL serait assez intelligent pour gérer le traduction en quelque sorte. Cela n’a pas été le cas, mais cela a généré un message d’erreur descriptif qui m’a dirigée vers l’utilisation de , contient .

Quoi qu'il en soit, si vous exécutez ceci dans le très recommandé LINQPad et exécutez cette requête, vous pouvez afficher le code SQL réel généré par le fournisseur SQL LINQ. Il vous montrera chacune des valeurs en cours de paramétrage dans une clause IN.

65
Peter Meyer

Si vous appelez depuis .NET, vous pouvez utiliser 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});

Ici, Dapper réfléchit, pour que vous n'ayez pas à le faire. Quelque chose de similaire est possible avec LINQ to SQL , bien sûr:

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;
48
Marc Gravell

C’est peut-être une façon assez désagréable de le faire, je l’avais utilisée une fois, était plutôt efficace.

En fonction de vos objectifs, cela peut être utile.

  1. Créez un table temporaire avec une colonne.
  2. INSERT chaque valeur de recherche dans cette colonne.
  3. Au lieu d'utiliser un IN, vous pouvez simplement utiliser vos règles standard JOIN. (Flexibility ++)

Cela offre une certaine flexibilité supplémentaire dans ce que vous pouvez faire, mais il est plus adapté aux situations dans lesquelles vous avez une grande table à interroger, avec une bonne indexation et où vous souhaitez utiliser la liste paramétrée plus d'une fois. Cela évite de l'exécuter deux fois et de procéder à toutes les opérations d'assainissement manuellement.

Je n'ai jamais eu le temps de décrire exactement comment rapide c'était, mais dans ma situation, c'était nécessaire.

27
Kent Fredric

Nous avons une fonction qui crée une variable de table à laquelle vous pouvez rejoindre:

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 

Alors:

@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
23
David Robbins

C'est dégueulasse, mais si vous êtes assuré d'en avoir au moins un, vous pourriez faire:

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

Avoir IN ('tag1', 'tag2', 'tag1', 'tag1', 'tag1') sera facilement optimisé par SQL Server. De plus, vous obtenez un index direct cherche

19
Matt Rogish

Dans _SQL Server 2016+_, vous pouvez utiliser STRING_SPLIT fonction:

_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;
_

LiveDemo

Le réponse acceptée fonctionnera bien sûr et c’est l’un des moyens à prendre, mais c’est anti-modèle.

E. Rechercher des lignes par liste de valeurs

Ceci remplace un anti-modèle courant, tel que la création d'une chaîne SQL dynamique dans la couche d'application ou Transact-SQL, ou à l'aide de l'opérateur LIKE:

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

La question initiale comporte l'exigence _SQL Server 2008_. Comme cette question est souvent utilisée en double, j'ai ajouté cette réponse à titre de référence.
19
Lukasz Szozda

À mon avis, la meilleure source pour résoudre ce problème est ce qui a été posté sur ce site:

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

Utilisation:

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

CRÉDITS POUR: Dinakar Nethi

17
Paulo Henrique

Je voudrais passer un paramètre de type table (puisqu'il s'agit de SQL Server 2008 ) et effectuer un where exists ou une jointure interne. Vous pouvez également utiliser XML en utilisant sp_xml_preparedocument, puis même indexer cette table temporaire.

17
eulerfx

La manière appropriée, à mon humble avis, consiste à stocker la liste dans une chaîne de caractères (limitée par la longueur prise en charge par le SGBD); le seul truc est que (pour simplifier le traitement), j’ai un séparateur (une virgule dans mon exemple) au début et à la fin de la chaîne. L'idée est de "normaliser à la volée" en transformant la liste en un tableau à une colonne contenant une ligne par valeur. Cela vous permet de tourner

dans (ct1, ct2, ct3 ... ctn)

dans un

dans (sélectionnez ...)

ou (la solution que je préférerais probablement) une jointure régulière, si vous ajoutez simplement un "distinct" pour éviter les problèmes de doublons dans la liste.

Malheureusement, les techniques pour couper une chaîne sont assez spécifiques à un produit. Voici la version de 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;

La version 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, ',', ''))));

et la version de 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, ',', ''));

(Bien sûr, "pivot" doit renvoyer autant de lignes que le nombre maximum d'éléments que nous pouvons trouver dans la liste)

11
SFA

Si vous avez SQL Server 2008 ou une version ultérieure, j'utilise un paramètre de table .

Si vous êtes assez malchanceux pour rester bloqué sur SQL Server 2005 , vous pouvez ajouter une fonction CLR comme celle-ci,

[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 vous pourriez utiliser comme ça,

declare @desiredTags nvarchar(MAX);
set @desiredTags = 'Ruby,Rails,scruffy,rubyonrails';

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

Peut-être pouvons-nous utiliser XML ici:

    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)
9
MindLoggedOut

J'approcherais cela par défaut en passant une fonction table (qui retourne une table d'une chaîne) à la condition IN.

Voici le code pour le fichier UDF (Je l’ai obtenu quelque part, je ne trouve pas la source pour le moment)

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
  )

Une fois que vous avez ceci, votre code serait aussi simple que cela:

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

À moins que vous n'ayez une chaîne ridiculement longue, cela devrait bien fonctionner avec l'index de la table.

Si nécessaire, vous pouvez l'insérer dans une table temporaire, l'indexer, puis exécuter une jointure ...

9
Eli Ekstein

Je pense que c'est le cas lorsqu'une requête statique n'est tout simplement pas la solution. Générez dynamiquement la liste de votre clause in, échappez vos guillemets simples et construisez dynamiquement du SQL. Dans ce cas, vous ne verrez probablement pas beaucoup de différence avec une méthode donnée en raison de la petite liste, mais la méthode la plus efficace consiste à envoyer le code SQL exactement comme il est écrit dans votre message. Je pense que c'est une bonne habitude de l'écrire de la manière la plus efficace, plutôt que de faire ce qui fait le code le plus joli, ou de considérer qu'il est une mauvaise pratique de générer dynamiquement du SQL.

J'ai vu les fonctions divisées prendre plus de temps à exécuter que la requête elle-même dans de nombreux cas où les paramètres sont volumineux. Une procédure stockée avec des paramètres table dans SQL 2008 est la seule autre option que je considérerais, bien que cela soit probablement plus lent dans votre cas. TVP ne sera probablement plus rapide que pour les grandes listes si vous effectuez une recherche sur la clé primaire du TVP, car SQL créera quand même une table temporaire pour la liste (si la liste est longue). Vous ne saurez à coup sûr que si vous le testez.

J'ai également vu des procédures stockées qui avaient 500 paramètres avec des valeurs par défaut de null et WHERE Column1 IN (@ Param1, @ Param2, @ Param3, ..., @ Param500). Cela a amené SQL à créer une table temporaire, à effectuer un tri/une distinction, puis à effectuer une analyse de table au lieu d’une recherche d’index. C’est essentiellement ce que vous feriez en paramétrant cette requête, bien qu’à une échelle suffisamment petite pour que cela ne fasse aucune différence notable. Je recommande fortement de ne pas avoir NULL dans vos listes IN, car si cela devient un NOT IN, il n'agira pas comme prévu. Vous pouvez créer dynamiquement la liste de paramètres, mais la seule chose évidente à gagner est que les objets échapperaient pour vous aux guillemets simples. Cette approche est également légèrement plus lente du côté de l'application, car les objets doivent analyser la requête pour trouver les paramètres. Cela peut être plus rapide avec SQL, car les requêtes paramétrées appellent sp_prepare, sp_execute autant de fois que vous exécutez la requête, suivies de sp_unprepare.

La réutilisation des plans d'exécution pour les procédures stockées ou les requêtes paramétrées peut vous apporter un gain de performances, mais elle vous verrouillera sur un plan d'exécution déterminé par la première requête exécutée. Cela peut être moins qu'idéal pour les requêtes ultérieures dans de nombreux cas. Dans votre cas, la réutilisation des plans d'exécution sera probablement un avantage, mais cela ne changera peut-être rien car l'exemple est une requête très simple.

Notes de falaises:

Dans votre cas, quoi que vous fassiez, que ce soit le paramétrage avec un nombre fixe d’éléments dans la liste (null si non utilisé), la construction dynamique de la requête avec ou sans paramètres, ou l’utilisation de procédures stockées avec des paramètres table ne fera pas beaucoup de différence. . Cependant, mes recommandations générales sont les suivantes:

Votre cas/requêtes simples avec peu de paramètres:

SQL dynamique, peut-être avec des paramètres si les tests montrent de meilleures performances.

Requêtes avec plans d'exécution réutilisables, appelées plusieurs fois en changeant simplement les paramètres ou si la requête est compliquée:

SQL avec paramètres dynamiques.

Requêtes avec de grandes listes:

Procédure stockée avec des paramètres de table. Si la liste peut varier considérablement, utilisez WITH RECOMPILE sur la procédure stockée ou utilisez simplement un SQL dynamique sans paramètres pour générer un nouveau plan d'exécution pour chaque requête.

9
Scott

Si nous avons des chaînes stockées dans la clause IN avec des virgules (,) délimitées, nous pouvons utiliser la fonction charindex pour obtenir les valeurs. Si vous utilisez .NET, vous pouvez mapper avec 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

Vous pouvez utiliser l'instruction ci-dessus dans votre code .NET et mapper le paramètre avec SqlParameter.

Démo Fiddler

EDIT: Créez la table appelée SelectedTags à l'aide du script suivant.

DDL Script:

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
8
Gowdhaman008

Une autre solution possible consiste à ne pas transmettre un nombre variable d'arguments à une procédure stockée, mais à ne transmettre qu'une seule chaîne contenant les noms recherchés, mais de les rendre uniques en les entourant de "<>". Puis utilisez PATINDEX pour trouver les noms:

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

Utilisez la procédure stockée suivante. Il utilise une fonction de partage personnalisée, qui peut être trouvée ici .

 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
mangeshkt

Dans SQL Server 2016+, une autre possibilité consiste à utiliser la fonction OPENJSON .

Cette approche est bloguée dans OPENJSON - l’un des meilleurs moyens de sélectionner des lignes par liste d’identifiants .

Un exemple complet ci-dessous

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
Martin Smith

Voici une autre alternative. Passez simplement une liste délimitée par des virgules en tant que paramètre de chaîne à la procédure stockée et:

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

Et la fonction:

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

Pour un nombre variable d'arguments comme celui-ci, la seule façon dont je suis au courant consiste à générer explicitement le code SQL ou à effectuer quelque chose qui implique de renseigner une table temporaire avec les éléments de votre choix et de les joindre à la table temporaire.

Dans ColdFusion nous faisons simplement:

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

Voici une technique qui recrée une table locale à utiliser dans une chaîne de requête. Le faire de cette façon élimine tous les problèmes d'analyse.

La chaîne peut être construite dans n'importe quelle langue. Dans cet exemple, j’utilisais SQL car c’était le problème initial que j’essayais de résoudre. J'avais besoin d'un moyen propre de transmettre des données de table à la volée dans une chaîne pour qu'elle soit exécutée ultérieurement.

L'utilisation d'un type défini par l'utilisateur est facultative. La création du type est créée une seule fois et peut être effectuée à l'avance. Sinon, ajoutez simplement un type de table complet à la déclaration dans la chaîne.

Le modèle général est facile à étendre et peut être utilisé pour passer des tableaux plus complexes.

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

J'ai une réponse qui ne nécessite pas de fichier UDF, XML, car IN accepte une instruction select, par exemple. SELECT * FROM Test où Data IN (valeur SELECT de la table)

Vous n'avez vraiment besoin que d'un moyen de convertir la chaîne en table.

Cela peut être fait avec un CTE récursif, ou une requête avec une table numérique (ou Master..spt_value)

Voici la version 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
Runonthespot

J'utilise une version plus concise de la réponse votée en haut :

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)));

Il boucle deux fois les paramètres de la balise; mais cela n'a pas d'importance la plupart du temps (ce ne sera pas votre goulot d'étranglement; si c'est le cas, déroulez la boucle).

Si vous êtes vraiment intéressé par les performances et que vous ne voulez pas parcourir deux fois la boucle, voici une version moins belle:

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);
6
George Stocker

Voici une autre réponse à ce problème.

(nouvelle version publiée le 04/06/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;
    }

À votre santé.

5
Darek

Voici un post-cross pour une solution au même problème. Plus robuste que les délimiteurs réservés - inclut les tableaux d'échappement et imbriqués, et comprend les valeurs NULL et les tableaux vides.

Chaîne C # et T-SQL [] Fonctions utilitaires Pack/Unpack

Vous pouvez ensuite rejoindre la fonction table.

4
Jason Kleban

Vous pouvez le faire de manière réutilisable en procédant comme suit -

public static class SqlWhereInParamBuilder
{
    public static string BuildWhereInClause<t>(string partialClause, string paramPrefix, IEnumerable<t> parameters)
    {
        string[] parameterNames = parameters.Select(
            (paramText, paramNumber) => "@" + paramPrefix + paramNumber.ToString())
            .ToArray();

        string inClause = string.Join(",", parameterNames);
        string whereInClause = string.Format(partialClause.Trim(), inClause);

        return whereInClause;
    }

    public static void AddParamsToCommand<t>(this SqlCommand cmd, string paramPrefix, IEnumerable<t> parameters)
    {
        string[] parameterValues = parameters.Select((paramText) => paramText.ToString()).ToArray();

        string[] parameterNames = parameterValues.Select(
            (paramText, paramNumber) => "@" + paramPrefix + paramNumber.ToString()
            ).ToArray();

        for (int i = 0; i < parameterNames.Length; i++)
        {
            cmd.Parameters.AddWithValue(parameterNames[i], parameterValues[i]);
        }
    }
}

Pour plus de détails, consultez cet article de blog - clause SQL WHERE IN paramétrée c)

4
Bryan

(Edit: si les paramètres de la table ne sont pas disponibles) Il semble préférable de scinder un grand nombre de paramètres IN en plusieurs requêtes de longueur fixe. avoir un certain nombre d'instructions SQL connues avec un nombre de paramètres fixe et aucune valeur fictive/en double, ni d'analyse syntaxique de chaînes, XML, etc.

Voici du code en C # que j'ai écrit sur ce sujet:

public static T[][] SplitSqlValues<T>(IEnumerable<T> values)
{
    var sizes = new int[] { 1000, 500, 250, 125, 63, 32, 16, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 };
    int processed = 0;
    int currSizeIdx = sizes.Length - 1; /* start with last (smallest) */
    var splitLists = new List<T[]>();

    var valuesDistSort = values.Distinct().ToList(); /* remove redundant */
    valuesDistSort.Sort();
    int totalValues = valuesDistSort.Count;

    while (totalValues > sizes[currSizeIdx] && currSizeIdx > 0)
    currSizeIdx--; /* bigger size, by array pos. */

    while (processed < totalValues)
    {
        while (totalValues - processed < sizes[currSizeIdx]) 
            currSizeIdx++; /* smaller size, by array pos. */
        var partList = new T[sizes[currSizeIdx]];
        valuesDistSort.CopyTo(processed, partList, 0, sizes[currSizeIdx]);
        splitLists.Add(partList);
        processed += sizes[currSizeIdx];
    }
    return splitLists.ToArray();
}

(vous pouvez avoir d'autres idées, omettre le tri, utilisez valuesDistSort.Skip (traité). Prenez (taille [...]) au lieu de la liste/du tableau CopyTo).

Lorsque vous insérez des variables de paramètre, vous créez quelque chose comme:

foreach(int[] partList in splitLists)
{
    /* here: question mark for param variable, use named/numbered params if required */
    string sql = "select * from Items where Id in("
        + string.Join(",", partList.Select(p => "?")) 
        + ")"; /* comma separated ?, one for each partList entry */

    /* create command with sql string, set parameters, execute, merge results */
}

J'ai regardé le code SQL généré par le mappeur relationnel objet NHibernate (lors de la requête de données pour créer des objets), et qui donne de meilleurs résultats avec plusieurs requêtes. Dans NHibernate, on peut spécifier une taille de lot; si de nombreuses lignes de données d'objet doivent être extraites, il essaie de récupérer le nombre de lignes équivalent à la taille du lot.

SELECT * FROM MyTable WHERE Id IN (@p1, @p2, @p3, ... , @p[batch-size])

, au lieu d’envoyer des centaines ou des milliers de

SELECT * FROM MyTable WHERE Id=@id

Lorsque les ID restants ont une taille inférieure à la taille d'un lot, mais qu'il en reste plusieurs, il se scinde en instructions plus petites, mais avec une certaine longueur.

Si vous avez une taille de lot de 100 et une requête avec 118 paramètres, cela créerait 3 requêtes:

  • une avec 100 paramètres (taille du lot),
  • alors un avec 12
  • et un autre avec 6,

mais aucun avec 118 ou 18. De cette façon, il limite les instructions SQL possibles aux instructions connues connues, évitant ainsi trop de plans de requêtes différentes, donc trop nombreux, qui remplissent le cache et ne sont en grande partie jamais réutilisés. Le code ci-dessus fait la même chose, mais avec des longueurs de 1000, 500, 250, 125, 63, 32, 16, 10 à 1. Les listes de paramètres contenant plus de 1000 éléments sont également divisées, empêchant ainsi une erreur de base de données due à une taille limite.

Quoi qu'il en soit, il est préférable de disposer d'une interface de base de données qui envoie directement du SQL paramétré, sans instruction Prepare ni traitement d'appel distincts. Les bases de données telles que SQL Server et Oracle mémorisent le SQL par égalité de chaîne (les valeurs changent, les paramètres de liaison dans SQL non!) Et réutilisent les plans de requête, si disponibles. Pas besoin d'instructions de préparation séparées et maintenance fastidieuse des descripteurs de requête dans le code! ADO.NET fonctionne comme ceci, mais il semble que Java utilise toujours prepare/execute par handle (incertain).

J'avais ma propre question sur ce sujet, suggérant à l'origine de remplir la clause IN avec des doublons, mais préférant ensuite la déclaration d'instruction de style NHibernate: SQL paramétré - entrée/pas avec un nombre fixe de paramètres, pour l'optimisation du cache du plan de requête ?

Cette question est toujours intéressante, même plus de 5 ans après avoir été posée ...

EDIT: J'ai remarqué que les requêtes IN comportant de nombreuses valeurs (telles que 250 ou plus) ont encore tendance à être lentes, dans le cas donné, sur SQL Server. Alors que je m'attendais à ce que la base de données crée une sorte de table temporaire en interne et se joint à celle-ci, il m'a semblé qu'elle ne faisait que répéter la seule expression SELECT une fois, à une valeur. Le temps passait à environ 200 ms par requête, ce qui est encore pire que de joindre les données SELECT d'extraction des ID d'origine aux autres tables associées. De plus, SQL Server Profiler contenait de 10 à 15 UC, ce qui est inhabituel pour une exécution répétée du même paramétrage. requêtes, ce qui suggère que de nouveaux plans de requête ont été créés lors d'appels répétés. Peut-être que des requêtes ponctuelles comme des requêtes individuelles ne sont pas du tout pires. J'ai dû comparer ces requêtes à des requêtes non divisées avec des tailles différentes pour une conclusion finale, mais pour l'instant, il semble que les clauses IN longues doivent de toute façon être évitées.

4
Erik Hart

Le seul coup gagnant est de ne pas jouer.

Pas de variabilité infinie pour vous. Seulement variabilité finie.

Dans le code SQL, vous avez une clause comme celle-ci:

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

Dans le code C #, vous faites quelque chose comme ceci:

  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]);

Donc, fondamentalement, si le nombre est égal à 0, il n'y a pas de filtre et tout passe. Si le nombre est supérieur à 0, alors la valeur doit être dans la liste, mais la liste a été complétée à cinq avec des valeurs impossibles (pour que le code SQL ait toujours un sens).

Parfois, la solution boiteuse est la seule qui fonctionne réellement.

4
Jason Henriksen
    create FUNCTION [dbo].[ConvertStringToList]


      (@str VARCHAR (MAX), @delimeter CHAR (1))
        RETURNS 
        @result TABLE (
            [ID] INT NULL)
    AS
    BEG

IN

    DECLARE @x XML 
    SET @x = '<t>' + REPLACE(@str, @delimeter, '</t><t>') + '</t>'

    INSERT INTO @result
    SELECT DISTINCT x.i.value('.', 'int') AS token
    FROM @x.nodes('//t') x(i)
    ORDER BY 1

RETURN
END

--VOTRE REQUÊTE

select * from table where id in ([dbo].[ConvertStringToList(YOUR comma separated string ,',')])
3
ASP.Net Developer

Il s'agit d'une variante réutilisable de la solution dans l'excellente réponse de Mark Bracket.

Méthode d'extension:

public static class ParameterExtensions
{
    public static Tuple<string, SqlParameter[]> ToParameterTuple<T>(this IEnumerable<T> values)
    {
        var createName = new Func<int, string>(index => "@value" + index.ToString());
        var paramTuples = values.Select((value, index) => 
        new Tuple<string, SqlParameter>(createName(index), new SqlParameter(createName(index), value))).ToArray();
        var inClause = string.Join(",", paramTuples.Select(t => t.Item1));
        var parameters = paramTuples.Select(t => t.Item2).ToArray();
        return new Tuple<string, SqlParameter[]>(inClause, parameters);
    }
}

Usage:

        string[] tags = {"Ruby", "Rails", "scruffy", "rubyonrails"};
        var paramTuple = tags.ToParameterTuple();
        var cmdText = $"SELECT * FROM Tags WHERE Name IN ({paramTuple.Item1})";

        using (var cmd = new SqlCommand(cmdText))
        {
            cmd.Parameters.AddRange(paramTuple.Item2);
        }
3
Derek Greer

Utilisez une requête dynamique. Le front-end est uniquement destiné à générer le format requis:

DECLARE @invalue VARCHAR(100)
SELECT @invalue = '''Bishnu'',''Gautam'''

DECLARE @dynamicSQL VARCHAR(MAX)
SELECT @dynamicSQL = 'SELECT * FROM #temp WHERE [name] IN (' + @invalue + ')'
EXEC (@dynamicSQL)

SQL Fiddle

2
brykneval

Il existe un moyen agréable, simple et éprouvé de le faire:

/* Create table-value string: */
CREATE TYPE [String_List] AS TABLE ([Your_String_Element] varchar(max) PRIMARY KEY);
GO
/* Create procedure which takes this table as parameter: */

CREATE PROCEDURE [dbo].[usp_ListCheck]
@String_List_In [String_List] READONLY  
AS   
SELECT a.*
FROM [dbo].[Tags] a
JOIN @String_List_In b ON a.[Name] = b.[Your_String_Element];

J'ai commencé à utiliser cette méthode pour résoudre les problèmes que nous avions avec le framework d'entité (n'était pas assez robuste pour notre application). Nous avons donc décidé de donner une chance à Dapper (identique à Stack). En spécifiant également votre liste de chaînes sous forme de table avec colonne PK, corrigez beaucoup vos plans d'exécution. Ici est un bon article sur la façon de passer une table à Dapper - tout est rapide et PROPRE.

2
Bartosz X

Étape 1:-

string[] Ids = new string[] { "3", "6", "14" };
string IdsSP = string.Format("'|{0}|'", string.Join("|", Ids));

Étape 2:-

@CurrentShipmentStatusIdArray [nvarchar](255) = NULL

Étape 3:-

Where @CurrentShipmentStatusIdArray is null or @CurrentShipmentStatusIdArray LIKE '%|' + convert(nvarchar,Shipments.CurrentShipmentStatusId) + '|%'

ou

Where @CurrentShipmentStatusIdArray is null or @CurrentShipmentStatusIdArray LIKE '%|' + Shipments.CurrentShipmentStatusId+ '|%'
1
Adel Mourad

Créez une table temporaire dans laquelle les noms sont stockés, puis utilisez la requête suivante:

select * from Tags 
where Name in (select distinct name from temp)
order by Count desc
0
guru008

Dans SQL SERVER 2016 ou supérieur, vous pouvez utiliser STRING_SPLIT.

DECLARE @InParaSeprated VARCHAR(MAX) = 'Ruby,Rails,scruffy,rubyonrails'
DECLARE @Delimeter VARCHAR(10) = ','
SELECT 
    * 
FROM 
    Tags T
    INNER JOIN STRING_SPLIT(@InputParameters,@Delimeter) SS ON T.Name = SS.value
ORDER BY 
    Count DESC

J'utilise ceci parce que, parfois, l'adhésion est plus rapide que Like Operator fonctionne dans mes requêtes.
De plus, vous pouvez mettre un nombre illimité d’entrées dans n’importe quel format séparé que vous aimez.
J'aime ça ..

0
Mili