Paramétrer une clause SQL IN

1041

Comment paramétrer une requête contenant une INclause 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 une 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.

Jeff Atwood
la source

Réponses:

315

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

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

Voici donc 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. LIKE "%...%"les requêtes ne sont pas indexées.
  • Assurez-vous que vous n'avez pas de |balises vides ou nulles ou cela ne fonctionnera pas

Il existe d'autres façons d'accomplir cela que certaines personnes peuvent considérer comme plus propres, alors continuez à lire.

Joel Spolsky
la source
119
Ce sera un peu lent
Matt Rogish
13
Oui, c'est un scan de table. Idéal pour 10 lignes, moche pour 100 000.
Will Hartung
17
Assurez-vous de tester sur les balises contenant des tuyaux.
Joel Coehoorn
17
Cela ne répond même pas à la question. Certes, il est facile de voir où ajouter les paramètres, mais comment pouvez-vous accepter cette solution si elle ne prend même pas la peine de paramétrer la requête? Il semble seulement plus simple que celui de @Mark Brackett car il n'est pas paramétré.
tvanfosson
21
Et si votre tag est "ruby | rails". Cela correspondra, ce qui sera faux. Lorsque vous déployez de telles solutions, vous devez vous assurer que les balises ne contiennent pas de tuyaux ou les filtrer explicitement: sélectionnez * dans les balises où '| ruby ​​| rails | scruffy | rubyonrails |' comme '% |' + Nom + '|%' ET nom différent de '%!%'
AK
729

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, ce n'est pas ouvert à l' injection SQL . Le seul texte injecté dans CommandText n'est pas basé sur l'entrée utilisateur. Il est uniquement basé sur le préfixe "@tag" codé en dur et l'index d'un tableau. L'index sera toujours un entier, n'est pas généré par l'utilisateur et est sûr.

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

Éditer:

Mis à part les problèmes d'injection, prenez soin de noter que la construction du texte de commande pour accueillir 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 en premier lieu (au lieu d'insérer simplement les chaînes de prédicat dans le SQL lui-même).

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

Si vous avez suffisamment de RAM, je suppose que SQL Server mettra probablement en cache un plan pour le nombre de paramètres communs. 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 le même, mais il me semble assez moche et je ne suis pas sûr que cela vaut la micro-optimisation (bien que, sur Stack Overflow - cela peut en valoir la peine).

De plus, SQL Server 7 et versions ultérieures paramètrent automatiquement les requêtes , donc 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 des données saisies par l'utilisateur comme celle-ci.

Mark Brackett
la source
2
Fondamentalement, la même chose que ma réponse à la question "connexe" et évidemment la meilleure solution car elle est constructive et efficace plutôt qu'interprétative (beaucoup plus difficile).
tvanfosson
49
C'est ainsi que LINQ to SQL le fait, BTW
Mark Cidade
3
@Pure: Le but de tout cela est d'éviter l'injection SQL, à laquelle vous seriez vulnérable si vous utilisiez du SQL dynamique.
Ray
4
@ God of Data - Oui, je suppose que si vous avez besoin de plus de 2100 balises, vous aurez besoin d'une solution différente. Mais Basarb ne pouvait atteindre 2100 que si la longueur moyenne des balises était <3 caractères (car vous avez également besoin d'un délimiteur). msdn.microsoft.com/en-us/library/ms143432.aspx
Mark Brackett
2
@bonCodigo - vos valeurs sélectionnées sont dans un tableau; il vous suffit de parcourir le tableau et d'ajouter un paramètre (suffixé avec l'index) pour chacun.
Mark Brackett
249

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 .

Vous devez d'abord 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;
    });
}
Mark Brackett
la source
41
nous l'avons testé et les paramètres de table sont lents DOG. Il est littéralement plus rapide d'exécuter 5 requêtes que de faire un TVP.
Jeff Atwood
4
@JeffAtwood - Avez-vous essayé de remanier la requête en quelque chose comme SELECT * FROM tags WHERE tags.name IN (SELECT name from @tvp);? En théorie, cela devrait vraiment être l'approche la plus rapide. Vous pouvez utiliser des index pertinents (par exemple, un index sur le nom de la balise dont INCLUDEle nombre serait idéal), et SQL Server devrait faire quelques recherches pour récupérer toutes les balises et leur nombre. À quoi ressemble le plan?
Nick Chammas
9
J'ai également testé cela et c'est FAST AS LIGHTNING (par rapport à la construction d'une grande chaîne IN). J'ai cependant eu quelques problèmes pour définir le paramètre car je recevais constamment "Impossible de convertir la valeur du paramètre d'un Int32 [] en un IEnumerable`1.". Quoi qu'il en soit, j'ai résolu cela et voici un exemple que j'ai fait pastebin.com/qHP05CXc
Fredrik Johansson
6
@FredrikJohansson - Sur 130 votes positifs, vous êtes peut-être le seul run qui ait réellement essayé de faire ça! J'ai fait une erreur en lisant les documents, et vous avez en fait besoin d'un IEnumerable <SqlDataRecord>, pas seulement d'un IEnumerable. Le code a été mis à jour.
Mark Brackett
3
@MarkBrackett Super avec une mise à jour! En fait, ce code a vraiment sauvé la journée pour moi car je recherche un index de recherche Lucene et il renvoie parfois plus de 50 000 hits qui doivent être vérifiés par rapport au serveur SQL - Je crée donc un tableau d'int [] (document / Clés SQL), puis le code ci-dessus entre en jeu. L'ensemble de l'OP prend maintenant moins de 200 ms :)
Fredrik Johansson
188

La question d'origine était "Comment paramétrer une requête ..."

Permettez-moi de dire ici que ce n'est pas une réponse à la question initiale. Il y a déjà quelques démonstrations de cela dans d'autres bonnes réponses.

Cela dit, allez-y et marquez cette réponse, votez-la, marquez-la comme n'étant pas une réponse ... faites tout ce que vous pensez être juste.

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

Réponse sélectionnée

Ce que je veux aborder ici, c'est l'approche donnée dans la réponse de Joel Spolsky, la réponse «sélectionnée» comme bonne réponse.

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

Mais en termes de généralisation de cette approche, considérons également les cas d'angle les plus obscurs, comme lorsque la Namecolonne contient un caractère générique (tel que reconnu par le prédicat LIKE.) Le caractère générique que je vois le plus couramment utilisé est %(un signe de pourcentage). Traitons donc cela ici maintenant, et passons plus tard à d'autres cas.

Quelques problèmes avec% character

Considérez une valeur 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 Name de `` pe% ter 'serait retournée par une requête de la forme:

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

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

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

Le comportement que nous observons est assez étrange. La modification de l'ordre des termes de recherche dans la liste modifie l'ensemble de résultats.

Il va presque sans dire que nous pourrions ne pas vouloir pe%terfaire correspondre le beurre d'arachide, peu importe combien il l'aime.

Boîtier d'angle obscur

(Oui, je conviendrai que c'est un cas obscur. Probablement un qui n'est pas susceptible d'être testé. Nous ne nous attendrions pas à un caractère générique dans une valeur de colonne. Nous pouvons supposer que l'application empêche une telle valeur d'être stockée. 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 qui seraient considérés comme des caractères génériques sur le côté droit d'un LIKEopérateur de comparaison.

Patcher un trou

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

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

Maintenant, nous pouvons 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 REPLACEfonction pour trouver des occurrences du %caractère et insérer un caractère barre oblique inverse devant chacun, comme ceci:

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

Cela résout donc 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 à toute occurrence de caractère d'échappement lui-même. Cette fois, nous utilisons le! comme personnage d'échappement:

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

Le soulignement aussi

Maintenant que nous sommes sur une lancée, nous pouvons ajouter une autre REPLACEpoignée le caractère générique de soulignement. Et juste pour le plaisir, 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'évasion car elle fonctionne dans Oracle et MySQL ainsi que SQL Server. (J'utilise habituellement la \ backslash comme caractère d'échappement, puisque c'est le caractère que nous utilisons dans les expressions régulières. Mais pourquoi être contraint par convention!

Ces supports embêtants

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 terminé la correction, du moins pour SQL Server. Étant donné que les paires de crochets ont une signification particulière, nous devrons également y échapper. Si nous parvenons à échapper correctement les crochets, alors au moins nous n'aurons pas à nous soucier du trait d'union -et du carat ^entre les crochets. Et nous pouvons laisser tout caractère %et _à l'intérieur des crochets échappés, car nous aurons essentiellement désactivé la signification spéciale des crochets.

Trouver des paires de supports correspondants ne devrait pas être si difficile. C'est un peu plus difficile que de gérer les occurrences de singleton% et _. (Notez qu'il ne suffit pas 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 je ne peux gérer sans exécuter plus de cas de test .)

L'expression en ligne devient désordonnée

Cette expression en ligne dans le SQL devient plus longue et plus laide. Nous pouvons probablement le faire fonctionner, mais le ciel aide la pauvre âme qui vient derrière et doit la déchiffrer. Comme je suis un grand fan des expressions en ligne, je suis enclin à ne pas en utiliser ici, principalement parce que je ne veux pas avoir à laisser un commentaire expliquant la raison du désordre et m'en excusant.

Une fonction où?

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

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

Connaissance du domaine

Nous pouvons avoir une connaissance spécialisée du domaine de la colonne (c'est-à-dire l'ensemble des valeurs autorisées appliquées pour la colonne. Nous pouvons savoir a priori que les valeurs stockées dans la colonne ne contiendront jamais de signe de pourcentage, de soulignement ou de parenthèse 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 exiger que ces valeurs soient échappées, peut-être en utilisant un caractère défini, de sorte que les valeurs soient COMME une comparaison "sûre". Encore une fois, un bref commentaire sur l'ensemble de valeurs autorisé, et en particulier sur le caractère utilisé comme caractère d'échappement, et suivez l'approche de Joel Spolsky.

Mais, en l'absence de connaissances spécialisées et d'une garantie, il est important pour nous d'envisager au moins de gérer ces cas de coins obscurs et de déterminer si le comportement est raisonnable et "selon les spécifications".


Autres questions récapitulées

Je crois que d'autres ont déjà suffisamment souligné certains des autres sujets de préoccupation couramment considérés:

  • Injection SQL (en prenant ce qui semble être des informations fournies par l'utilisateur, et en l'incluant dans le texte SQL plutôt que de les fournir via des variables de liaison. L'utilisation de variables de liaison n'est pas requise, ce n'est qu'une approche pratique pour contrecarrer l'injection SQL. Il existe d'autres comment y faire face:

  • plan d'optimisation utilisant le balayage d'index plutôt que la recherche d'index, besoin éventuel d'une expression ou d'une fonction pour échapper les caractères génériques (index possible sur l'expression ou la fonction)

  • l'utilisation de valeurs littérales à la place des variables de liaison affecte l'évolutivité


Conclusion

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

Mais dès que je l'ai vu, j'ai immédiatement vu un problème potentiel avec lui, et ce n'est pas ma nature de le laisser glisser. Je ne veux pas critiquer les efforts des autres. Je sais que de nombreux développeurs prennent leur travail très personnellement, car ils y investissent tellement et ils se soucient tellement de lui. Comprenez donc, 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 allé loin de la question d'origine. Mais où laisser cette note concernant ce que je considère comme un problème important avec la réponse "sélectionnée" à une question?

spencer7593
la source
pouvez-vous s'il vous plaît nous faire savoir si vous utilisez ou aimez les requêtes paramétrées? dans ce cas particulier, est-il correct de sauter par-dessus la règle d '«utiliser des requêtes paramétrées» et de désinfecter avec la langue d'origine? MERCI beaucoup
Luis Siquot
2
@Luis: oui, je préfère utiliser des variables de liaison dans les instructions SQL, et j'éviterai uniquement les variables de liaison lorsque leur utilisation entraîne un problème de performances. mon modèle normatif pour le problème d'origine serait de créer dynamiquement l'instruction SQL avec le nombre requis d'espaces réservés dans la liste IN, puis de lier chaque valeur à l'un des espaces réservés. Voir la réponse de Mark Brackett, qui est la réponse que j'ai (et 231 autres) votée.
spencer7593
133

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

Vous avez donc la chaîne

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 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
David Basarab
la source
2
Vous pouvez également rejoindre la fonction table avec cette approche.
Michael Haren
J'utilise une solution similaire à celle-ci dans Oracle. Il ne doit pas être analysé à nouveau comme le font certaines des autres solutions.
Leigh Riffel
9
Il s'agit d'une approche de base de données pure, l'autre nécessite un travail dans le code en dehors de la base de données.
David Basarab
Est-ce que cela pour une analyse de table ou peut-il tirer parti des index, etc.?
Pure.Krome
mieux serait d'utiliser CROSS APPLY contre la fonction de table SQL (au moins en 2005), qui se joint essentiellement à la table qui est retournée
adolf garlic
66

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 avoir rappelé Stack Overflow utilisait LINQ to SQL , mais il a peut-être é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 Containsclause me semble très en arrière. Quand j'ai dû faire une requête similaire pour un projet au travail, j'ai naturellement essayé de le faire dans le mauvais sens 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. Il ne l'a pas fait, mais il a fourni un message d'erreur qui était descriptif et m'a dirigé vers l'utilisation de Contains .

Quoi qu'il en soit, si vous l'exécutez dans le LINQPad hautement recommandé et exécutez cette requête, vous pouvez afficher le SQL réel généré par le fournisseur SQL LINQ. Il vous montrera chacune des valeurs paramétrées dans une INclause.

Peter Meyer
la source
50

Si vous appelez à partir de .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, vous n'avez donc pas à le faire. Bien sûr, quelque chose de similaire est possible avec LINQ to SQL :

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;
Marc Gravell
la source
11
qui se trouve être ce que nous utilisons sur cette page, pour la vraie question posée (dapper) i.stack.imgur.com/RBAjL.png
Sam Saffron
3
Notez que dapper prend désormais également en charge les paramètres de valeur de table en tant que citoyens de première classe
Marc Gravell
Cela tombe si les noms sont longs
cs0815
29

C'est peut-être une façon à moitié méchante de le faire, je l'ai utilisé une fois, c'était plutôt efficace.

Selon vos objectifs, cela pourrait être utile.

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

Cela a un peu plus de flexibilité dans ce que vous pouvez faire, mais il est plus adapté aux situations où vous avez une grande table à interroger, avec une bonne indexation, et que vous souhaitez utiliser la liste paramétrée plus d'une fois. Vous évitez de l'exécuter deux fois et de faire tout l'assainissement manuellement.

Je n'ai jamais réussi à profiler exactement à quelle vitesse c'était, mais dans ma situation c'était nécessaire.

Kent Fredric
la source
Ce n'est pas méchant du tout! Encore plus, c'est à mon humble avis un moyen très propre. Et si vous regardez dans le plan d'exécution, vous voyez que c'est la même chose que la clause IN. Au lieu d'une table temporaire, vous pouvez également créer une table fixe avec des index, où vous stockez les paramètres avec le SESSIONID.
SQL Police
27

Dans SQL Server 2016+vous pourriez utiliser la STRING_SPLITfonction:

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

La réponse acceptée fonctionnera bien sûr et c'est l'une des voies à suivre, mais elle est anti-modèle.

E. Trouver des lignes par liste de valeurs

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

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

Addendum :

Pour améliorer l' STRING_SPLITestimation des lignes de la fonction de table, il est judicieux de matérialiser les valeurs fractionnées en tant que variable table / table temporaire:

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 - Démo en direct

Connexe: Comment passer une liste de valeurs dans une procédure stockée


La question d'origine a une exigence SQL Server 2008. Parce que cette question est souvent utilisée en double, j'ai ajouté cette réponse comme référence.

Lukasz Szozda
la source
1
Je n'ai pas testé cela, mais j'ai l'impression que c'est la solution la plus propre 2016+. J'aimerais toujours pouvoir passer un tableau d'int, mais jusque-là ...
Daniel
24

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

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 

Donc:

@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
David Robbins
la source
20

C'est brut, mais si vous êtes assuré d'en avoir au moins un, vous pouvez 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 des recherches d'index directes

Matt Rogish
la source
1
Les paramètres facultatifs avec Null vérifient les performances, car l'optimiseur nécessite le nombre de paramètres utilisés pour créer des requêtes efficaces. Une requête pour 5 paramètres peut nécessiter un plan de requête différent de celui pour 500 paramètres.
Erik Hart
18

À mon avis, la meilleure source pour résoudre ce problème est ce qui a été publié sur ce 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

Utilisation:

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

CRÉDITS POUR: Dinakar Nethi

Paulo Henrique
la source
Excellente réponse, propre et modulaire, exécution super rapide, à l'exception de l'analyse CSV initiale dans une table (une fois, petit nombre d'éléments). Bien que pourrait utiliser charindex () plus simple / plus rapide au lieu de patindex ()? Charindex () autorise également l'argument 'start_location' qui peut éviter de couper la chaîne d'entrée à chaque iter? Pour répondre à la question d'origine, il suffit de joindre le résultat de la fonction.
crokusek
18

Je passerais un paramètre de type de table (puisque c'est SQL Server 2008 ) et ferais une where existsjointure interne. Vous pouvez également utiliser XML, en utilisant sp_xml_preparedocument, puis même indexer cette table temporaire.

eulerfx
la source
La réponse de Ph.E a un exemple de table de construction de temp (de csv).
crokusek
12

La manière appropriée à mon humble avis est de stocker la liste dans une chaîne de caractères (limitée en longueur par ce que le SGBD prend en charge); la seule astuce est que (afin de 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 qui contient une ligne par valeur. Cela vous permet de tourner

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

dans un

dans (sélectionner ...)

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 avec les valeurs en double dans la liste.

Malheureusement, les techniques de découpage d'une chaîne sont assez spécifiques au 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 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)

Jeff Atwood
la source
11

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

Si vous n'avez pas la chance d'être 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
Jodrell
la source
10

Je pense que c'est un cas où une requête statique n'est tout simplement pas la voie à suivre. Créez dynamiquement la liste de votre clause in, échappez à vos guillemets simples et créez dynamiquement SQL. Dans ce cas, vous ne verrez probablement pas beaucoup de différence avec une méthode en raison de la petite liste, mais la méthode la plus efficace consiste vraiment à envoyer le SQL exactement tel qu'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 comme une mauvaise pratique de construire dynamiquement SQL.

J'ai vu que les fonctions fractionnées prennent plus de temps à exécuter que la requête elles-mêmes dans de nombreux cas où les paramètres deviennent volumineux. Une procédure stockée avec des paramètres de table dans SQL 2008 est la seule autre option que je considérerais, bien que ce sera probablement plus lent dans votre cas. TVP ne sera probablement plus rapide pour les grandes listes que si vous effectuez une recherche sur la clé primaire du TVP, car SQL créera de toute façon une table temporaire pour la liste (si la liste est grande). 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 nulles et ayant WHERE Column1 IN (@ Param1, @ Param2, @ Param3, ..., @ Param500). Cela a amené SQL à créer une table temporaire, à effectuer un tri / distinct, 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 pas de différence notable. Je déconseille fortement d'avoir NULL dans vos listes IN, car si cela est changé en NOT IN, il n'agira pas comme prévu. Vous pouvez créer dynamiquement la liste des paramètres, mais la seule chose évidente que vous gagneriez est que les objets échapperaient aux guillemets simples pour vous. 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.

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 enferme dans un plan d'exécution déterminé par la première requête exécutée. Dans de nombreux cas, cela peut être loin d'être idéal pour les requêtes ultérieures. Dans votre cas, la réutilisation des plans d'exécution sera probablement un plus, mais cela pourrait ne faire aucune différence car l'exemple est une requête très simple.

Notes sur les falaises:

Pour votre cas, tout ce que vous faites, que ce soit le paramétrage avec un nombre fixe d'éléments dans la liste (null s'il n'est pas 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 de valeur de 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 des 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 valeur de table. Si la liste peut varier considérablement, utilisez WITH RECOMPILE sur la procédure stockée, ou utilisez simplement SQL dynamique sans paramètres pour générer un nouveau plan d'exécution pour chaque requête.

Scott
la source
Qu'entendez-vous ici par "procédure stockée"? Pourriez-vous poster un exemple?
struhtanov
9

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)
MindLoggedOut
la source
1
CTEet @xpeut être éliminé / inséré dans la sous-sélection, si cela est fait très soigneusement, comme indiqué dans cet article .
robert4
9

J'aborderais cela par défaut en passant une fonction de valeur de table (qui renvoie une table à partir d'une chaîne) à la condition IN.

Voici le code pour l'UDF (je l'ai obtenu de Stack Overflow quelque part, je ne trouve pas la source en ce 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 l'avez obtenu, 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

Sauf si vous avez une chaîne ridiculement longue, cela devrait bien fonctionner avec l'index de table.

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

Eli Ekstein
la source
8

Une autre solution possible consiste à ne pas passer un nombre variable d'arguments à une procédure stockée, à passer une seule chaîne contenant les noms que vous recherchez, mais à les rendre uniques en les entourant de '<>'. Utilisez ensuite PATINDEX pour trouver les noms:

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

Utilisez la procédure stockée suivante. Il utilise une fonction de partage personnalisée, que vous pouvez trouver 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
mangeshkt
la source
8

Si nous avons des chaînes stockées dans la clause IN avec la virgule (,) délimitée, 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 du violoneux

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

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
Gowdhaman008
la source
Pouvez-vous montrer un exemple de ce fonctionnement où il n'y a pas de liste codée en dur de valeurs possibles?
John Saunders
@JohnSaunders, j'ai édité le script sans utiliser de liste codée en dur. Veuillez vérifier.
Gowdhaman008
3
Une limitation avec cette option. CharIndex renvoie 1 si la chaîne est trouvée. IN renvoie une correspondance pour des termes exacts. CharIndex pour "Stack" renverra 1 pour un terme "StackOverflow" IN ne le sera pas. Il y a un tweek mineur à cette réponse en utilisant PatIndex ci-dessus qui entoure les noms avec '<'% name% '>' qui surmonte cette limitation. Solution créative à ce problème.
Richard Vivian
7

Pour un nombre variable d'arguments comme celui-ci, la seule façon dont je suis au courant est de générer explicitement le SQL ou de faire quelque chose qui implique de remplir une table temporaire avec les éléments que vous voulez et de les joindre à la table temporaire.

ConcernedOfTunbridgeWells
la source
7

Dans ColdFusion, nous faisons juste:

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

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'ai utilisé SQL car c'était le problème d'origine 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 à exécuter plus tard.

L'utilisation d'un type défini par l'utilisateur est facultative. La création du type n'est créée qu'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 tables 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)
Sébaste
la source
7

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

Cette approche est décrite dans OPENJSON - l'une des meilleures façons de sélectionner des lignes par liste d'ID .

Un exemple complet travaillé 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 
Martin Smith
la source
7

Voici une autre alternative. Passez simplement une liste séparé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
Métaphore
la source
6

J'ai une réponse qui ne nécessite pas d'UDF, XML car IN accepte une instruction select, par exemple SELECT * FROM Test où les données IN (SELECT Value FROM TABLE)

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

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);
Runonthespot
la source
6

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

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);
George Stocker
la source
5

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

(nouvelle version publiée le 6/4/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é.

Darek
la source
4

Le seul coup gagnant est de ne pas jouer.

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

Dans le 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 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 SQL ait toujours du sens)

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

Jason Henriksen
la source