Parametrisieren Sie eine SQL IN-Klausel

1041

Wie parametrisiere ich eine Abfrage, die eine INKlausel mit einer variablen Anzahl von Argumenten enthält, wie dieses?

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

In dieser Abfrage kann die Anzahl der Argumente zwischen 1 und 5 liegen.

Ich würde es vorziehen, keine dedizierte gespeicherte Prozedur für diese (oder XML) zu verwenden, aber wenn es eine elegante Art gibt, die spezifisch für SQL Server 2008 ist , bin ich dafür offen.

Jeff Atwood
quelle

Antworten:

316

Hier ist eine schnelle und schmutzige Technik, die ich verwendet habe:

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

Hier ist also der C # -Code:

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

Zwei Vorbehalte:

  • Die Leistung ist schrecklich. LIKE "%...%"Abfragen werden nicht indiziert.
  • Stellen Sie sicher, dass Sie keine |, leeren oder Null-Tags haben , da dies sonst nicht funktioniert

Es gibt andere Möglichkeiten, um dies zu erreichen, die manche Leute als sauberer betrachten. Bitte lesen Sie weiter.

Joel Spolsky
quelle
119
Das wird hella langsam sein
Matt Rogish
13
Ja, dies ist ein Tabellenscan. Großartig für 10 Reihen, mies für 100.000.
Will Hartung
17
Stellen Sie sicher, dass Sie Tags testen, die Pipes enthalten.
Joel Coehoorn
17
Dies beantwortet nicht einmal die Frage. Zugegeben, es ist leicht zu erkennen, wo die Parameter hinzugefügt werden müssen, aber wie können Sie dies als Lösung akzeptieren, wenn es nicht einmal die Mühe macht, die Abfrage zu parametrisieren? Es sieht nur einfacher aus als das von @Mark Brackett, weil es nicht parametrisiert ist.
Tvanfosson
21
Was ist, wenn Ihr Tag "Ruby | Rails" ist? Es wird übereinstimmen, was falsch sein wird. Wenn Sie solche Lösungen einführen, müssen Sie entweder sicherstellen, dass Tags keine Pipes enthalten, oder sie explizit herausfiltern: Wählen Sie * unter Tags aus, bei denen '| ruby ​​| Rails | scruffy | rubyonrails |' wie '% |' + Name + '|%' UND Name nicht wie '%!%'
AK
729

Sie können jeden Wert parametrisieren , also wie folgt:

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

Welches wird Ihnen geben:

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"

Nein, dies ist nicht offen für SQL-Injection . Der einzige in CommandText eingefügte Text basiert nicht auf Benutzereingaben. Es basiert ausschließlich auf dem fest codierten Präfix "@tag" und dem Index eines Arrays. Der Index ist immer eine Ganzzahl, wird nicht vom Benutzer generiert und ist sicher.

Die vom Benutzer eingegebenen Werte werden weiterhin in Parameter gestopft, sodass dort keine Sicherheitsanfälligkeit besteht.

Bearbeiten:

Abgesehen von Bedenken hinsichtlich der Injektion ist zu beachten, dass das Erstellen des Befehlstextes für eine variable Anzahl von Parametern (wie oben) die Fähigkeit des SQL Servers beeinträchtigt, zwischengespeicherte Abfragen zu nutzen. Das Nettoergebnis ist, dass Sie mit ziemlicher Sicherheit den Wert der Verwendung von Parametern verlieren (im Gegensatz zum bloßen Einfügen der Prädikatzeichenfolgen in SQL selbst).

Nicht, dass zwischengespeicherte Abfragepläne nicht wertvoll sind, aber IMO ist diese Abfrage bei weitem nicht kompliziert genug, um einen großen Nutzen daraus zu ziehen. Während sich die Kompilierungskosten den Ausführungskosten nähern (oder diese sogar übersteigen), sprechen Sie immer noch von Millisekunden.

Wenn Sie über genügend RAM verfügen, würde SQL Server wahrscheinlich auch einen Plan für die allgemeinen Parameterzahlen zwischenspeichern. Ich nehme an, Sie könnten immer fünf Parameter hinzufügen und die nicht angegebenen Tags auf NULL setzen - der Abfrageplan sollte derselbe sein, aber er erscheint mir ziemlich hässlich und ich bin mir nicht sicher, ob sich die Mikrooptimierung lohnt (obwohl, on Stack Overflow - es kann sich durchaus lohnen).

SQL Server 7 und höher parametrisiert Abfragen automatisch , sodass die Verwendung von Parametern unter Leistungsgesichtspunkten nicht unbedingt erforderlich ist. Unter Sicherheitsgesichtspunkten ist dies jedoch von entscheidender Bedeutung - insbesondere bei vom Benutzer eingegebenen Daten wie diesen.

Mark Brackett
quelle
2
Im Grunde das gleiche wie meine Antwort auf die "verwandte" Frage und offensichtlich die beste Lösung, da sie eher konstruktiv und effizient als interpretativ ist (viel schwieriger).
Tvanfosson
49
So macht es LINQ to SQL, BTW
Mark Cidade
3
@Pure: Der springende Punkt dabei ist, SQL Injection zu vermeiden, für das Sie anfällig wären, wenn Sie dynamisches SQL verwenden würden.
Ray
4
@God of Data - Ja, ich nehme an, wenn Sie mehr als 2100 Tags benötigen, benötigen Sie eine andere Lösung. Basarb's konnte jedoch nur 2100 erreichen, wenn die durchschnittliche Tag-Länge <3 Zeichen betrug (da Sie auch ein Trennzeichen benötigen). msdn.microsoft.com/en-us/library/ms143432.aspx
Mark Brackett
2
@bonCodigo - Ihre ausgewählten Werte befinden sich in einem Array. Sie durchlaufen einfach das Array und fügen für jedes einen Parameter hinzu (mit dem Index versehen).
Mark Brackett
249

Für SQL Server 2008 können Sie einen Tabellenwertparameter verwenden . Es ist ein bisschen Arbeit, aber es ist wohl sauberer als meine andere Methode .

Zuerst müssen Sie einen Typ erstellen

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

Dann sieht Ihr ADO.NET-Code folgendermaßen aus:

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
quelle
41
Wir haben dies getestet und die Parameter mit Tabellenwerten sind DOG langsam. Es ist buchstäblich schneller, 5 Abfragen auszuführen als einen TVP.
Jeff Atwood
4
@ JeffAtwood - Haben Sie versucht, die Abfrage auf so etwas wie neu zu mischen SELECT * FROM tags WHERE tags.name IN (SELECT name from @tvp);? Theoretisch sollte dies wirklich der schnellste Ansatz sein. Sie können relevante Indizes verwenden (z. B. einen Index für den Tag-Namen, dessen INCLUDEAnzahl ideal wäre), und SQL Server sollte einige Versuche unternehmen, um alle Tags und ihre Anzahl zu erfassen. Wie sieht der Plan aus?
Nick Chammas
9
Ich habe dies auch getestet und es ist SCHNELL WIE EIN BLITZ (im Vergleich zum Erstellen eines großen IN-Strings). Ich hatte jedoch einige Probleme beim Einstellen des Parameters, da ständig "Fehler beim Konvertieren des Parameterwerts von einem Int32 [] in einen IEnumerable`1" angezeigt wurde. Wie dem auch sei gelöst, dass und hier ist ein Beispiel habe ich pastebin.com/qHP05CXc
Fredrik Johansson
6
@FredrikJohansson - Von 130 Upvotes sind Sie möglicherweise der einzige Lauf, der tatsächlich versucht hat, dies auszuführen! Ich habe beim Lesen der Dokumente einen Fehler gemacht, und Sie benötigen tatsächlich einen IEnumerable <SqlDataRecord>, nicht irgendeinen IEnumerable. Code wurde aktualisiert.
Mark Brackett
3
@ MarkBrackett Großartig mit einem Update! Eigentlich hat mir dieser Code wirklich den Tag gerettet, da ich einen Lucene-Suchindex abfrage und manchmal mehr als 50.000 Treffer zurückgibt, die gegen SQL Server doppelt geprüft werden müssen. Also erstelle ich ein Array von int [] (document / SQL-Schlüssel) und dann kommt der obige Code herein. Das gesamte OP dauert jetzt weniger als 200 ms :)
Fredrik Johansson
188

Die ursprüngliche Frage lautete "Wie parametriere ich eine Abfrage ..."

Lassen Sie mich gleich hier feststellen, dass dies keine Antwort ist auf die ursprüngliche Frage ist. Es gibt bereits einige Demonstrationen davon in anderen guten Antworten.

Wenn dies gesagt ist, markieren Sie diese Antwort, stimmen Sie sie ab, markieren Sie sie als keine Antwort ... tun Sie, was Sie für richtig halten.

In der Antwort von Mark Brackett finden Sie die bevorzugte Antwort, die ich (und 231 andere) positiv bewertet habe. Der in seiner Antwort angegebene Ansatz ermöglicht 1) die effektive Verwendung von Bindungsvariablen und 2) die Verwendung von Prädikaten, die sarkierbar sind.

Ausgewählte Antwort

Was ich hier ansprechen möchte, ist der Ansatz in Joel Spolskys Antwort, die Antwort "ausgewählt" als die richtige Antwort.

Joel Spolskys Ansatz ist klug. Und es funktioniert vernünftig, es wird vorhersehbares Verhalten und vorhersehbare Leistung zeigen, wenn "normale" Werte gegeben sind und mit den normativen Randfällen wie NULL und der leeren Zeichenfolge. Und es kann für eine bestimmte Anwendung ausreichend sein.

Um diesen Ansatz zu verallgemeinern, betrachten wir auch die dunkeleren Eckfälle, z. B. wenn die NameSpalte ein Platzhalterzeichen enthält (wie vom LIKE-Prädikat erkannt). Das am häufigsten verwendete Platzhalterzeichen ist %(ein Prozentzeichen). Lassen Sie uns jetzt hier damit umgehen und später mit anderen Fällen fortfahren.

Einige Probleme mit% Zeichen

Betrachten Sie einen Namenswert von 'pe%ter' . (Für die Beispiele hier verwende ich anstelle des Spaltennamens einen Literalzeichenfolgenwert.) Eine Zeile mit dem Namenswert "pe% ter" wird von einer Abfrage des Formulars zurückgegeben:

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

Aber die gleiche Reihe wird nicht nicht zurückgegeben, wenn die Reihenfolge der Suchbegriffe umgekehrt wird:

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

Das Verhalten, das wir beobachten, ist seltsam. Durch Ändern der Reihenfolge der Suchbegriffe in der Liste wird die Ergebnismenge geändert.

Es ist fast selbstverständlich, dass wir nicht wollen pe%ter mit Erdnussbutter mithalten wollen, egal wie sehr er es mag.

Dunkler Eckfall

(Ja, ich werde zustimmen, dass dies ein dunkler Fall ist. Wahrscheinlich einer, der wahrscheinlich nicht getestet wird. Wir würden keinen Platzhalter in einem Spaltenwert erwarten. Wir können davon ausgehen, dass die Anwendung die Speicherung eines solchen Werts verhindert Nach meiner Erfahrung habe ich selten eine Datenbankbeschränkung gesehen, die Zeichen oder Muster, die als Platzhalter auf der rechten Seite von a gelten, ausdrücklich nicht zulässt LIKE Vergleichsoperators .

Ein Loch flicken

Ein Ansatz zum Patchen dieses Lochs besteht darin, dem %Platzhalterzeichen zu entkommen . (Für alle, die mit der Escape-Klausel des Operators nicht vertraut sind, finden Sie hier einen Link zur SQL Server-Dokumentation .

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

Jetzt können wir das Literal% abgleichen. Wenn wir einen Spaltennamen haben, müssen wir natürlich den Platzhalter dynamisch umgehen. Wir können die REPLACEFunktion verwenden, um Vorkommen des %Zeichens zu finden und vor jedem ein Backslash-Zeichen einzufügen, wie folgt:

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

Damit ist das Problem mit dem Platzhalter% gelöst. Fast.

Entkomme der Flucht

Wir erkennen an, dass unsere Lösung ein weiteres Problem eingeführt hat. Der Fluchtcharakter. Wir sehen, dass wir auch allen Vorkommen des Fluchtcharakters selbst entkommen müssen. Dieses Mal verwenden wir die! als Fluchtcharakter:

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

Der Unterstrich auch

Jetzt, da wir auf einer Rolle sind, können wir REPLACEdem Unterstrich-Platzhalter ein weiteres Handle hinzufügen . Und nur zum Spaß verwenden wir diesmal $ als Fluchtzeichen.

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

Ich ziehe diesen Ansatz dem Escape vor, da er sowohl in Oracle und MySQL als auch in SQL Server funktioniert. (Normalerweise verwende ich den \ backslash als Escape-Zeichen, da dies das Zeichen ist, das wir in regulären Ausdrücken verwenden. Aber warum durch Konventionen eingeschränkt werden?

Diese nervigen Klammern

Mit SQL Server können Platzhalterzeichen auch als Literale behandelt werden, indem sie in Klammern gesetzt werden []. Wir sind also noch nicht mit dem Reparieren fertig, zumindest für SQL Server. Da Klammerpaare eine besondere Bedeutung haben, müssen wir uns auch diesen entziehen. Wenn es uns gelingt, den Klammern richtig zu entkommen, müssen wir uns zumindest nicht um den Bindestrich -und das Karat ^in den Klammern kümmern. Und wir können jeden verlassen %und_ Zeichen in den Klammern entkommen, da wir im Grunde die besondere Bedeutung der Klammern deaktiviert haben.

Es sollte nicht so schwer sein, passende Klammerpaare zu finden. Es ist etwas schwieriger als das Auftreten von Singleton% und _ zu behandeln. (Beachten Sie, dass es nicht ausreicht, nur alle Vorkommen von Klammern zu maskieren, da eine einzelne Klammer als Literal betrachtet wird und nicht maskiert werden muss. Die Logik wird etwas unschärfer, als ich verarbeiten kann, ohne mehr Testfälle auszuführen .)

Inline-Ausdruck wird chaotisch

Dieser Inline-Ausdruck in SQL wird länger und hässlicher. Wir können es wahrscheinlich zum Laufen bringen, aber der Himmel hilft der armen Seele, die zurückkommt und es entziffern muss. Als Fan von Inline-Ausdrücken neige ich dazu, hier keinen zu verwenden, hauptsächlich, weil ich keinen Kommentar hinterlassen möchte, der den Grund für das Durcheinander erklärt und mich dafür entschuldigt.

Eine Funktion wo?

Okay, wenn wir das nicht als Inline-Ausdruck in SQL behandeln, ist die nächstgelegene Alternative eine benutzerdefinierte Funktion. Und wir wissen, dass dies die Dinge nicht beschleunigen wird (es sei denn, wir können einen Index dafür definieren, wie wir es mit Oracle könnten). Wenn wir eine Funktion erstellen müssen, tun wir dies möglicherweise besser in dem Code, der SQL aufruft Aussage.

Und diese Funktion kann je nach DBMS und Version einige Verhaltensunterschiede aufweisen. (Ein Dankeschön an alle Java-Entwickler, die daran interessiert sind, jedes Datenbankmodul austauschbar zu verwenden.)

Fachwissen

Möglicherweise verfügen wir über spezielle Kenntnisse der Domäne für die Spalte (dh der Menge der zulässigen Werte, die für die Spalte erzwungen werden. Möglicherweise wissen wir a priori, dass die in der Spalte gespeicherten Werte niemals ein Prozentzeichen, einen Unterstrich oder eine Klammer enthalten In diesem Fall fügen wir nur einen kurzen Kommentar hinzu, dass diese Fälle abgedeckt sind.

Die in der Spalte gespeicherten Werte können% oder _ Zeichen zulassen, aber eine Einschränkung kann erfordern, dass diese Werte maskiert werden, möglicherweise unter Verwendung eines definierten Zeichens, so dass die Werte wie der Vergleich "sicher" sind. Nochmals ein kurzer Kommentar zu den zulässigen Werten und insbesondere zu dem Zeichen, das als Escape-Zeichen verwendet wird, und zu Joel Spolskys Ansatz.

Ohne das Fachwissen und eine Garantie ist es für uns jedoch wichtig, zumindest die Behandlung dieser obskuren Eckfälle in Betracht zu ziehen und zu prüfen, ob das Verhalten angemessen und "gemäß der Spezifikation" ist.


Andere Themen zusammengefasst

Ich glaube, andere haben bereits ausreichend auf einige der anderen häufig betrachteten Problembereiche hingewiesen:

  • SQL-Injection (unter Verwendung von scheinbar vom Benutzer bereitgestellten Informationen und Aufnahme dieser Informationen in den SQL-Text, anstatt sie über Bindevariablen bereitzustellen. Die Verwendung von Bindevariablen ist nicht erforderlich, sondern nur ein praktischer Ansatz, um die SQL-Injection zu verhindern. Es gibt andere Möglichkeiten, damit umzugehen:

  • Optimierungsplan mit Index-Scan anstelle von Index-Suchvorgängen, mögliche Notwendigkeit eines Ausdrucks oder einer Funktion zum Escape-Platzieren von Platzhaltern (möglicher Index für Ausdruck oder Funktion)

  • Die Verwendung von Literalwerten anstelle von Bindungsvariablen wirkt sich auf die Skalierbarkeit aus


Fazit

Ich mag Joel Spolskys Ansatz. Es ist klug. Und es funktioniert.

Aber sobald ich es sah, sah ich sofort ein potenzielles Problem damit, und es liegt nicht in meiner Natur, es gleiten zu lassen. Ich will nicht kritisch gegenüber den Bemühungen anderer sein. Ich weiß, dass viele Entwickler ihre Arbeit sehr persönlich nehmen, weil sie so viel in sie investieren und sich so sehr darum kümmern. Bitte haben Sie Verständnis dafür, dass dies kein persönlicher Angriff ist. Was ich hier identifiziere, ist die Art von Problem, das eher in der Produktion als beim Testen auftritt.

Ja, ich bin weit von der ursprünglichen Frage entfernt. Aber wo sonst sollte ich diesen Hinweis zu dem hinterlassen, was ich für ein wichtiges Problem mit der "ausgewählten" Antwort auf eine Frage halte?

spencer7593
quelle
Können Sie uns bitte mitteilen, ob Sie parametrisierte Abfragen verwenden oder mögen? Ist es in diesem speziellen Fall richtig, über die Regel der Verwendung parametrisierter Abfragen zu springen und mit der Originalsprache zu bereinigen? Vielen Dank
Luis Siquot
2
@Luis: Ja, ich bevorzuge die Verwendung von Bindevariablen in SQL-Anweisungen und vermeide Bindevariablen nur, wenn ihre Verwendung ein Leistungsproblem verursacht. Mein normatives Muster für das ursprüngliche Problem wäre, die SQL-Anweisung mit der erforderlichen Anzahl von Platzhaltern in der IN-Liste dynamisch zu erstellen und dann jeden Wert an einen der Platzhalter zu binden. Siehe die Antwort von Mark Brackett, die ich (und 231 andere) positiv bewertet habe.
Spencer7593
133

Sie können den Parameter als Zeichenfolge übergeben

Sie haben also die Zeichenfolge

DECLARE @tags

SET @tags = ruby|rails|scruffy|rubyonrails

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

Dann müssen Sie nur noch den String als 1-Parameter übergeben.

Hier ist die Split-Funktion, die ich benutze.

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
quelle
2
Mit diesem Ansatz können Sie auch der Tabellenfunktion beitreten.
Michael Haren
Ich verwende eine ähnliche Lösung in Oracle. Es muss nicht wie bei einigen anderen Lösungen erneut analysiert werden.
Leigh Riffel
9
Dies ist ein reiner Datenbankansatz, für den andere Arbeiten im Code außerhalb der Datenbank erforderlich sind.
David Basarab
Handelt es sich um einen Tabellenscan oder kann es Indizes usw. nutzen?
Pure.Krome
Besser wäre es, CROSS APPLY für die SQL-Tabellenfunktion (zumindest ab 2005) zu verwenden, die sich im Wesentlichen mit der zurückgegebenen Tabelle verbindet
Adolf Knoblauch
66

Ich habe Jeff / Joel heute im Podcast darüber sprechen hören ( Folge 34 , 16.12.2008 (MP3, 31 MB), 1 Stunde 03 Minuten 38 Sekunden - 1 Stunde 06 Minuten 45 Sekunden), und ich dachte, ich erinnere mich an Stack Overflow benutzte LINQ to SQL , aber vielleicht wurde es fallen gelassen. Hier ist das Gleiche in LINQ to SQL.

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

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

Das ist es. Und ja, LINQ schaut schon rückwärts genug, aber die ContainsKlausel scheint mir extra rückwärts zu sein. Wenn ich bei der Arbeit eine ähnliche Abfrage für ein Projekt durchführen musste, habe ich natürlich versucht, dies falsch zu machen, indem ich eine Verknüpfung zwischen dem lokalen Array und der SQL Server-Tabelle hergestellt habe Übersetzung irgendwie. Dies war nicht der Fall, aber es wurde eine beschreibende Fehlermeldung angezeigt, die mich auf die Verwendung von Contains hinwies .

Wenn Sie dies im empfohlenen LINQPad ausführen und diese Abfrage ausführen, können Sie das tatsächliche SQL anzeigen, das der SQL LINQ-Anbieter generiert hat. Es zeigt Ihnen jeden der Werte, die in einer INKlausel parametrisiert werden .

Peter Meyer
quelle
50

Wenn Sie von .NET aus anrufen, können Sie Dapper dot net verwenden :

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

Hier denkt Dapper, also musst du nicht. Ähnliches ist natürlich mit LINQ to SQL möglich :

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
quelle
11
Das ist zufällig das, was wir auf dieser Seite für die eigentliche Frage verwenden ( dapper
Sam Saffron
3
Beachten Sie, dass dapper jetzt auch Tabellenwertparameter als erstklassige Bürger unterstützt
Marc Gravell
Dies fällt um, wenn die Namen lang sind
cs0815
29

Dies ist möglicherweise eine halb böse Art, es zu tun, ich habe es einmal benutzt, war ziemlich effektiv.

Abhängig von Ihren Zielen kann es von Nutzen sein.

  1. Erstellen Sie eine temporäre Tabelle mit einer Spalte.
  2. INSERT jeder Suchwert in dieser Spalte.
  3. Anstatt ein zu verwenden IN, können Sie dann einfach Ihre Standardregeln verwenden JOIN. (Flexibilität ++)

Dies bietet ein wenig mehr Flexibilität bei Ihren Aufgaben, eignet sich jedoch besser für Situationen, in denen Sie eine große Tabelle mit guter Indizierung abfragen müssen und die parametrisierte Liste mehrmals verwenden möchten. Spart es, es zweimal ausführen zu müssen und alle Hygienemaßnahmen manuell durchführen zu lassen.

Ich bin nie dazu gekommen, genau zu profilieren, wie schnell es war, aber in meiner Situation war es notwendig.

Kent Fredric
quelle
Das ist überhaupt nicht böse! Noch mehr, es ist meiner Meinung nach ein sehr sauberer Weg. Und wenn Sie sich den Ausführungsplan ansehen, sehen Sie, dass er mit der IN-Klausel identisch ist. Anstelle einer temporären Tabelle können Sie auch eine feste Tabelle mit Indizes erstellen, in der Sie die Parameter zusammen mit der SESSIONID speichern.
SQL Police
27

In SQL Server 2016+könnten Sie STRING_SPLITFunktion verwenden:

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

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

oder:

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

Die akzeptierte Antwort wird natürlich funktionieren und ist ein Weg, aber es ist ein Anti-Muster.

E. Suchen Sie Zeilen anhand der Werteliste

Dies ist ein Ersatz für gängige Anti-Patterns wie das Erstellen einer dynamischen SQL-Zeichenfolge in der Anwendungsschicht oder Transact-SQL oder die Verwendung des LIKE-Operators:

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

Nachtrag :

Zur Verbesserung der STRING_SPLIT Tabellenfunktionen empfiehlt es sich, geteilte Werte als temporäre Tabelle / Tabellenvariable zu verwenden:

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 - Live Demo

Verbunden: Übergeben einer Werteliste an eine gespeicherte Prozedur


Die ursprüngliche Frage ist erforderlich SQL Server 2008. Da diese Frage häufig als Duplikat verwendet wird, habe ich diese Antwort als Referenz hinzugefügt.

Lukasz Szozda
quelle
1
Ich habe dies noch nicht perfekt getestet, aber ich denke, dies ist die sauberste Lösung für 2016+. Ich würde immer noch gerne in der Lage sein, nur ein Array von int zu übergeben, aber bis dahin ...
Daniel
24

Wir haben eine Funktion, die eine Tabellenvariable erstellt, der Sie beitreten können:

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 

Damit:

@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
quelle
20

Dies ist grob, aber wenn Sie garantiert mindestens eine haben, können Sie Folgendes tun:

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

Mit IN ('tag1', 'tag2', 'tag1', 'tag1', 'tag1') kann SQL Server problemlos optimieren. Außerdem erhalten Sie direkte Indexsuchen

Matt Rogish
quelle
1
Optionale Parameter mit Null überprüfen die Leistung, da der Optimierer die Anzahl der Parameter benötigt, die zum Erstellen effizienter Abfragen verwendet werden. Eine Abfrage für 5 Parameter erfordert möglicherweise einen anderen Abfrageplan als eine für 500 Parameter.
Erik Hart
18

Meiner Meinung nach ist die beste Quelle zur Lösung dieses Problems das, was auf dieser Website veröffentlicht wurde:

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

Verwenden:

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

KREDITE FÜR: Dinakar Nethi

Paulo Henrique
quelle
Tolle Antwort, sauber und modular, superschnelle Ausführung, bis auf die anfängliche CSV-Analyse in einer Tabelle (einmalig, geringe Anzahl von Elementen). Obwohl könnte einfacher / schneller charindex () anstelle von patindex () verwendet werden? Charindex () erlaubt auch das Argument 'start_location', mit dem möglicherweise vermieden werden kann, dass die Eingabezeichenfolge bei jedem Iter unterbrochen wird. Um die ursprüngliche Frage zu beantworten, kann einfach mit dem Funktionsergebnis verbunden werden.
Crokusek
18

Ich würde einen Tabellentyp-Parameter übergeben (da es sich um SQL Server 2008 handelt ) und einen where existsoder inneren Join ausführen. Sie können auch XML verwenden sp_xml_preparedocument, diese temporäre Tabelle verwenden und dann sogar indizieren.

eulerfx
quelle
Die Antwort von Ph.E enthält ein Beispiel für die Erstellung einer temporären Tabelle (von csv).
Crokusek
12

IMHO besteht die richtige Methode darin, die Liste in einer Zeichenfolge zu speichern (die Länge wird durch die Unterstützung des DBMS begrenzt). Der einzige Trick ist, dass ich (um die Verarbeitung zu vereinfachen) am Anfang und am Ende der Zeichenfolge ein Trennzeichen (in meinem Beispiel ein Komma) habe. Die Idee ist, "on the fly zu normalisieren" und die Liste in eine einspaltige Tabelle umzuwandeln, die eine Zeile pro Wert enthält. Dadurch können Sie drehen

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

In ein

in (auswählen ...)

oder (die Lösung, die ich wahrscheinlich vorziehen würde) eine reguläre Verknüpfung, wenn Sie nur eine "eindeutige" hinzufügen, um Probleme mit doppelten Werten in der Liste zu vermeiden.

Leider sind die Techniken zum Schneiden einer Zeichenfolge ziemlich produktspezifisch. Hier ist die SQL Server-Version:

 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;

Die Oracle-Version:

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

und die MySQL-Version:

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

(Natürlich muss "Pivot" so viele Zeilen zurückgeben, wie die maximale Anzahl von Elementen in der Liste enthalten ist.)

Jeff Atwood
quelle
11

Wenn Sie SQL Server 2008 oder höher haben, würde ich einen Tabellenwertparameter verwenden .

Wenn Sie das Pech haben, in SQL Server 2005 stecken zu bleiben, können Sie eine CLR- Funktion wie diese hinzufügen :

[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());
}

Was du so gebrauchen könntest,

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

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

Ich denke, dies ist ein Fall, in dem eine statische Abfrage einfach nicht der richtige Weg ist. Erstellen Sie dynamisch die Liste für Ihre in-Klausel, schließen Sie Ihre einfachen Anführungszeichen und erstellen Sie dynamisch SQL. In diesem Fall werden Sie aufgrund der kleinen Liste wahrscheinlich keinen großen Unterschied zu einer Methode feststellen, aber die effizienteste Methode besteht darin, die SQL genau so zu senden, wie sie in Ihrem Beitrag geschrieben ist. Ich denke, es ist eine gute Angewohnheit, es so effizient wie möglich zu schreiben, anstatt das zu tun, was den schönsten Code ausmacht, oder es als schlechte Praxis zu betrachten, SQL dynamisch zu erstellen.

Ich habe gesehen, dass die Ausführung der Split-Funktionen in vielen Fällen, in denen die Parameter groß werden, länger dauert als die Abfrage selbst. Eine gespeicherte Prozedur mit Tabellenwertparametern in SQL 2008 ist die einzige andere Option, die ich in Betracht ziehen würde, obwohl dies in Ihrem Fall wahrscheinlich langsamer sein wird. TVP ist wahrscheinlich nur bei großen Listen schneller, wenn Sie nach dem Primärschlüssel des TVP suchen, da SQL ohnehin eine temporäre Tabelle für die Liste erstellt (wenn die Liste groß ist). Sie werden es nicht sicher wissen, wenn Sie es nicht testen.

Ich habe auch gespeicherte Prozeduren gesehen, die 500 Parameter mit den Standardwerten null und WHERE Column1 IN (@ Param1, @ Param2, @ Param3, ..., @ Param500) hatten. Dies führte dazu, dass SQL eine temporäre Tabelle erstellte, eine Sortierung / Unterscheidung durchführte und dann anstelle einer Indexsuche einen Tabellenscan durchführte. Dies ist im Wesentlichen das, was Sie tun würden, wenn Sie diese Abfrage parametrisieren, obwohl sie so klein ist, dass sie keinen spürbaren Unterschied macht. Ich empfehle dringend, NULL nicht in Ihren IN-Listen zu haben, da dies nicht wie beabsichtigt funktioniert, wenn dies in NOT IN geändert wird. Sie könnten die Parameterliste dynamisch erstellen, aber das einzig offensichtliche, was Sie gewinnen würden, ist, dass die Objekte den einfachen Anführungszeichen für Sie entgehen würden. Dieser Ansatz ist auch auf der Anwendungsseite etwas langsamer, da die Objekte die Abfrage analysieren müssen, um die Parameter zu finden.

Die Wiederverwendung von Ausführungsplänen für gespeicherte Prozeduren oder parametrisierte Abfragen kann zu einem Leistungsgewinn führen, bindet Sie jedoch an einen Ausführungsplan, der durch die erste ausgeführte Abfrage bestimmt wird. Dies kann in vielen Fällen für nachfolgende Abfragen weniger als ideal sein. In Ihrem Fall ist die Wiederverwendung von Ausführungsplänen wahrscheinlich ein Plus, aber es macht möglicherweise überhaupt keinen Unterschied, da das Beispiel eine wirklich einfache Abfrage ist.

Cliffs Notizen:

Für Ihren Fall macht alles, was Sie tun, sei es die Parametrisierung mit einer festen Anzahl von Elementen in der Liste (null, wenn nicht verwendet), das dynamische Erstellen der Abfrage mit oder ohne Parameter oder die Verwendung gespeicherter Prozeduren mit tabellenwertigen Parametern keinen großen Unterschied . Meine allgemeinen Empfehlungen lauten jedoch wie folgt:

Ihr Fall / einfache Fragen mit wenigen Parametern:

Dynamisches SQL, möglicherweise mit Parametern, wenn das Testen eine bessere Leistung zeigt.

Abfragen mit wiederverwendbaren Ausführungsplänen, die mehrmals aufgerufen werden, indem einfach die Parameter geändert werden oder wenn die Abfrage kompliziert ist:

SQL mit dynamischen Parametern.

Abfragen mit großen Listen:

Gespeicherte Prozedur mit Tabellenwertparametern. Wenn die Liste stark variieren kann, verwenden Sie WITH RECOMPILE für die gespeicherte Prozedur oder verwenden Sie einfach dynamisches SQL ohne Parameter, um einen neuen Ausführungsplan für jede Abfrage zu generieren.

Scott
quelle
Was meinst du hier mit "gespeicherte Prozedur"? Könnten Sie ein Beispiel posten?
struhtanov
9

Vielleicht können wir hier XML verwenden:

    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
quelle
1
CTEund @xkann entfernt / in die Unterauswahl eingefügt werden, wenn dies sehr sorgfältig durchgeführt wird, wie in diesem Artikel gezeigt .
Robert4
9

Ich würde dies standardmäßig mit der Übergabe einer Tabellenwertfunktion (die eine Tabelle aus einer Zeichenfolge zurückgibt) an die IN-Bedingung angehen.

Hier ist der Code für die UDF (ich habe ihn irgendwo von Stack Overflow bekommen, ich kann die Quelle momentan nicht finden)

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
  )

Sobald Sie dies erhalten haben, wäre Ihr Code so einfach:

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

Sofern Sie keine lächerlich lange Zeichenfolge haben, sollte dies mit dem Tabellenindex gut funktionieren.

Bei Bedarf können Sie es in eine temporäre Tabelle einfügen, indizieren und dann einen Join ausführen ...

Eli Ekstein
quelle
8

Eine andere mögliche Lösung besteht darin, anstelle einer variablen Anzahl von Argumenten an eine gespeicherte Prozedur eine einzelne Zeichenfolge zu übergeben, die die Namen enthält, nach denen Sie suchen, sie jedoch eindeutig zu machen, indem Sie sie mit '<>' umgeben. Verwenden Sie dann PATINDEX, um die Namen zu finden:

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

Verwenden Sie die folgende gespeicherte Prozedur. Es verwendet eine benutzerdefinierte Aufteilungsfunktion, die hier zu finden ist .

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

Wenn in der IN-Klausel Zeichenfolgen mit dem Komma (,) getrennt sind, können wir die charindex-Funktion verwenden, um die Werte abzurufen. Wenn Sie .NET verwenden, können Sie mit SqlParameters zuordnen.

DDL-Skript:

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

Sie können die obige Anweisung in Ihrem .NET-Code verwenden und den Parameter mit SqlParameter zuordnen.

Fiddler-Demo

BEARBEITEN: Erstellen Sie die Tabelle mit dem Namen SelectedTags mit dem folgenden Skript.

DDL-Skript:

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
quelle
Können Sie ein Beispiel für diese Arbeit zeigen, bei der es keine fest codierte Liste möglicher Werte gibt?
John Saunders
@ JohnSaunders, ich habe das Skript ohne Verwendung einer fest codierten Liste bearbeitet. Bitte überprüfen.
Gowdhaman008
3
Eine Einschränkung bei dieser Option. CharIndex gibt 1 zurück, wenn die Zeichenfolge gefunden wird. IN gibt eine Übereinstimmung für genaue Begriffe zurück. CharIndex für "Stack" gibt 1 für einen Begriff zurück, "StackOverflow" IN nicht. Es gibt eine kleine Woche zu dieser Antwort mit PatIndex oben, die Namen mit '<'% name% '>' enthält, die diese Einschränkung überwinden. Kreative Lösung für dieses Problem.
Richard Vivian
7

Für eine variable Anzahl solcher Argumente ist mir nur bekannt, dass Sie entweder explizit SQL generieren oder eine temporäre Tabelle mit den gewünschten Elementen füllen und mit der temporären Tabelle verknüpfen.

ConcernedOfTunbridgeWells
quelle
7

In ColdFusion machen wir einfach:

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

Hier ist eine Technik, mit der eine lokale Tabelle neu erstellt wird, die in einer Abfragezeichenfolge verwendet werden soll. Auf diese Weise werden alle Analyseprobleme beseitigt.

Die Zeichenfolge kann in einer beliebigen Sprache erstellt werden. In diesem Beispiel habe ich SQL verwendet, da dies das ursprüngliche Problem war, das ich zu lösen versuchte. Ich brauchte eine saubere Methode, um Tabellendaten im laufenden Betrieb in einer Zeichenfolge zu übergeben, die später ausgeführt werden sollte.

Die Verwendung eines benutzerdefinierten Typs ist optional. Das Erstellen des Typs wird nur einmal erstellt und kann im Voraus erfolgen. Andernfalls fügen Sie der Deklaration in der Zeichenfolge einfach einen vollständigen Tabellentyp hinzu.

Das allgemeine Muster ist einfach zu erweitern und kann zum Übergeben komplexerer Tabellen verwendet werden.

-- 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)
Steinfisch
quelle
7

In SQL Server 2016+ besteht eine andere Möglichkeit darin, die OPENJSONFunktion zu verwenden.

Über diesen Ansatz wird in OPENJSON gebloggt - eine der besten Möglichkeiten, Zeilen anhand der Liste der IDs auszuwählen .

Ein voll funktionsfähiges Beispiel unten

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

Hier ist eine andere Alternative. Übergeben Sie einfach eine durch Kommas getrennte Liste als Zeichenfolgenparameter an die gespeicherte Prozedur und:

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

Und die Funktion:

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
Metapher
quelle
6

Ich habe eine Antwort, die keine UDF, XML erfordert. Da IN eine select-Anweisung akzeptiert, z. B. SELECT * FROM Test, wobei Data IN (SELECT Value FROM TABLE)

Sie brauchen wirklich nur eine Möglichkeit, die Zeichenfolge in eine Tabelle zu konvertieren.

Dies kann mit einem rekursiven CTE oder einer Abfrage mit einer Nummerntabelle (oder Master..spt_value) erfolgen.

Hier ist die CTE-Version.

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
quelle
6

Ich verwende eine präzisere Version der am besten bewerteten Antwort :

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

Die Tag-Parameter werden zweimal durchlaufen. Aber das spielt die meiste Zeit keine Rolle (es wird nicht Ihr Engpass sein; wenn ja, rollen Sie die Schleife ab).

Wenn Sie wirklich an Leistung interessiert sind und nicht zweimal durch die Schleife iterieren möchten, finden Sie hier eine weniger schöne Version:

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
quelle
5

Hier ist eine weitere Antwort auf dieses Problem.

(neue Version veröffentlicht am 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;
    }

Prost.

Darek
quelle
4

Der einzige Gewinnzug ist, nicht zu spielen.

Keine unendliche Variabilität für Sie. Nur endliche Variabilität.

In SQL haben Sie eine Klausel wie diese:

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

Im C # -Code machen Sie ungefähr so:

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

Wenn also die Anzahl 0 ist, gibt es keinen Filter und alles geht durch. Wenn die Anzahl höher als 0 ist, muss der Wert in der Liste enthalten sein, aber die Liste wurde mit unmöglichen Werten auf fünf aufgefüllt (damit SQL immer noch Sinn macht).

Manchmal ist die lahme Lösung die einzige, die tatsächlich funktioniert.

Jason Henriksen
quelle