Comment paramétrer une requête contenant une IN
clause 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.
sql
sql-server
parameters
Jeff Atwood
la source
la source
Réponses:
Voici une technique rapide et sale que j'ai utilisée:
Voici donc le code C #:
Deux mises en garde:
LIKE "%...%"
les requêtes ne sont pas indexées.|
balises vides ou nulles ou cela ne fonctionnera pasIl existe d'autres façons d'accomplir cela que certaines personnes peuvent considérer comme plus propres, alors continuez à lire.
la source
Vous pouvez paramétrer chaque valeur, donc quelque chose comme:
Ce qui vous donnera:
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:
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.
la source
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
Ensuite, votre code ADO.NET ressemble à ceci:
la source
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 dontINCLUDE
le 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?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
Name
colonne 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:Mais cette même ligne ne sera pas retournée si l'ordre des termes de recherche est inversé:
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%ter
faire 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
LIKE
opé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 .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
REPLACE
fonction pour trouver des occurrences du%
caractère et insérer un caractère barre oblique inverse devant chacun, comme ceci: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:
Le soulignement aussi
Maintenant que nous sommes sur une lancée, nous pouvons ajouter une autre
REPLACE
poignée le caractère générique de soulignement. Et juste pour le plaisir, cette fois, nous utiliserons $ comme caractère d'échappement.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?
la source
Vous pouvez passer le paramètre sous forme de chaîne
Vous avez donc la chaîne
Ensuite, tout ce que vous avez à faire est de passer la chaîne en 1 paramètre.
Voici la fonction split que j'utilise.
la source
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.
C'est ça. Et, oui, LINQ regarde déjà assez en arrière, mais la
Contains
clause 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
IN
clause.la source
Si vous appelez à partir de .NET, vous pouvez utiliser Dapper dot net :
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 :
la source
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.
INSERT
chaque valeur de recherche dans cette colonne.IN
, vous pouvez alors simplement utiliser vosJOIN
rè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.
la source
Dans
SQL Server 2016+
vous pourriez utiliser laSTRING_SPLIT
fonction:ou:
LiveDemo
La réponse acceptée fonctionnera bien sûr et c'est l'une des voies à suivre, mais elle est anti-modèle.
Addendum :
Pour améliorer l'
STRING_SPLIT
estimation des lignes de la fonction de table, il est judicieux de matérialiser les valeurs fractionnées en tant que variable table / table temporaire: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.la source
Nous avons une fonction qui crée une variable de table à laquelle vous pouvez vous joindre:
Donc:
la source
C'est brut, mais si vous êtes assuré d'en avoir au moins un, vous pouvez faire:
Avoir IN ('tag1', 'tag2', 'tag1', 'tag1', 'tag1') sera facilement optimisé par SQL Server. De plus, vous obtenez des recherches d'index directes
la source
À mon avis, la meilleure source pour résoudre ce problème est ce qui a été publié sur ce site:
Syscomments. Dinakar Nethi
Utilisation:
CRÉDITS POUR: Dinakar Nethi
la source
Je passerais un paramètre de type de table (puisque c'est SQL Server 2008 ) et ferais une
where exists
jointure interne. Vous pouvez également utiliser XML, en utilisantsp_xml_preparedocument
, puis même indexer cette table temporaire.la source
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
dans un
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:
La version Oracle:
et la version MySQL:
(Bien sûr, "pivot" doit renvoyer autant de lignes que le nombre maximum d'éléments que nous pouvons trouver dans la liste)
la source
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,
Que vous pourriez utiliser comme ça,
la source
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.
la source
Peut-être pouvons-nous utiliser XML ici:
la source
CTE
et@x
peut être éliminé / inséré dans la sous-sélection, si cela est fait très soigneusement, comme indiqué dans cet article .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)
Une fois que vous l'avez obtenu, votre code serait aussi simple que cela:
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 ...
la source
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:
la source
Utilisez la procédure stockée suivante. Il utilise une fonction de partage personnalisée, que vous pouvez trouver ici .
la source
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:
T-SQL:
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:
T-SQL:
la source
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.
la source
Dans ColdFusion, nous faisons juste:
la source
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.
la source
Dans SQL Server 2016+, une autre possibilité consiste à utiliser la
OPENJSON
fonction.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
la source
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:
Et la fonction:
la source
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.
la source
J'utilise une version plus concise de la réponse la plus votée :
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:
la source
Voici une autre réponse à ce problème.
(nouvelle version publiée le 6/4/13).
À votre santé.
la source
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:
Dans le code C #, vous faites quelque chose comme ceci:
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.
la source