Sparametryzuj klauzulę SQL IN

1041

Jak sparametryzować zapytanie zawierające INklauzulę o zmiennej liczbie argumentów, na przykład ten?

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

W tym zapytaniu liczba argumentów może wynosić od 1 do 5.

Wolałbym nie używać dedykowanej procedury składowanej do tego (lub XML), ale jeśli istnieje jakiś elegancki sposób specyficzny dla SQL Server 2008 , jestem na to otwarty.

Jeff Atwood
źródło

Odpowiedzi:

316

Oto szybka i brudna technika, której użyłem:

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

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

Dwa zastrzeżenia:

  • Wydajność jest okropna. LIKE "%...%"zapytania nie są indeksowane.
  • Upewnij się, że nie masz żadnych |pustych lub pustych tagów , bo to nie zadziała

Istnieją inne sposoby osiągnięcia tego celu, które niektórzy mogą uznać za czystsze, więc czytaj dalej.

Joel Spolsky
źródło
119
To będzie bardzo wolne
Matt Rogish,
13
Tak, to jest skanowanie tabeli. Idealne dla 10 rzędów, kiepskie dla 100 000.
Will Hartung,
17
Upewnij się, że testujesz tagi z rurami.
Joel Coehoorn,
17
To nawet nie odpowiada na pytanie. To prawda, że ​​łatwo jest zobaczyć, gdzie dodać parametry, ale jak możesz zaakceptować to rozwiązanie, jeśli nawet nie sparametryzuje zapytania? Wygląda tylko prostiej niż @Mark Brackett, ponieważ nie jest sparametryzowany.
tvanfosson
21
Co jeśli twój tag to „ruby | rails”. Będzie pasować, co będzie źle. Kiedy wdrażasz takie rozwiązania, musisz upewnić się, że tagi nie zawierają rur, lub jawnie je odfiltrować: wybierz * z tagów, w których „| ruby ​​| szyny | niechlujny | rubyonrails | ' jak „% |” + Nazwa + '|%' ORAZ nazwa inna niż '%!%'
AK
729

Możesz sparametryzować każdą wartość, więc coś takiego:

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

Co da ci:

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"

Nie, to nie jest otwarte na iniekcję SQL . Jedyny wstrzyknięty tekst do CommandText nie jest oparty na danych wprowadzonych przez użytkownika. Opiera się wyłącznie na zapisanym na stałe prefiksie „@tag” i indeksie tablicy. Indeks zawsze będzie liczbą całkowitą, nie będzie generowany przez użytkownika i jest bezpieczny.

Wprowadzane przez użytkownika wartości są nadal upakowane w parametry, więc nie ma tam żadnej luki w zabezpieczeniach.

Edytować:

Pomijając obawy związane z zastrzykami, pamiętaj, że skonstruowanie tekstu polecenia w celu dostosowania do zmiennej liczby parametrów (jak wyżej) utrudnia serwerowi SQL korzystanie z buforowanych zapytań. W rezultacie prawie na pewno tracisz wartość używania parametrów w pierwszej kolejności (w przeciwieństwie do zwykłego wstawiania łańcuchów predykatów do samego SQL).

Nie jest tak, że buforowane plany zapytań nie są cenne, ale IMO to zapytanie nie jest wystarczająco skomplikowane, aby zobaczyć z niego wiele korzyści. Podczas gdy koszty kompilacji mogą zbliżać się (a nawet przekraczać) koszty wykonania, wciąż mówisz o milisekundach.

Jeśli masz wystarczającą ilość pamięci RAM, spodziewałbym się, że SQL Server prawdopodobnie buforuje również plan dla wspólnej liczby parametrów. Przypuszczam, że zawsze można dodać pięć parametrów i pozwolić, aby nieokreślone tagi miały wartość NULL - plan zapytań powinien być taki sam, ale wydaje mi się to dość brzydkie i nie jestem pewien, czy warto mikrooptymalizacji (chociaż na przepełnieniu stosu - może być tego warte).

Ponadto SQL Server 7 i nowsze wersje automatycznie parametryzują zapytania , więc używanie parametrów nie jest tak naprawdę konieczne z punktu widzenia wydajności - jest to jednak krytyczne z punktu widzenia bezpieczeństwa - szczególnie w przypadku danych wprowadzanych przez użytkownika w ten sposób.

Mark Brackett
źródło
2
Zasadniczo taka sama jak moja odpowiedź na pytanie „pokrewne” i oczywiście najlepsze rozwiązanie, ponieważ jest konstruktywne i skuteczne, a nie interpretacyjne (znacznie trudniejsze).
tvanfosson
49
W ten sposób robi to LINQ na SQL, BTW
Mark Cidade,
3
@Pure: Chodzi o to, aby uniknąć wstrzykiwania SQL, na które byłbyś narażony, gdybyś używał dynamicznego SQL.
Ray
4
@God of Data - Tak, przypuszczam, że jeśli potrzebujesz więcej niż 2100 tagów, potrzebujesz innego rozwiązania. Ale Basarb może osiągnąć 2100 tylko, jeśli średnia długość znacznika wynosi <3 znaki (ponieważ potrzebujesz również separatora). msdn.microsoft.com/en-us/library/ms143432.aspx
Mark Brackett
2
@bonCodigo - wybrane wartości są w tablicy; po prostu zapętlamy tablicę i dodajemy parametr (z indeksem) dla każdego z nich.
Mark Brackett
249

W przypadku SQL Server 2008 można użyć parametru o wartości tabeli . To trochę pracy, ale jest prawdopodobnie czystsze niż moja inna metoda .

Najpierw musisz utworzyć typ

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

Następnie kod ADO.NET wygląda następująco:

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
źródło
41
przetestowaliśmy to i parametry wycenione w tabeli są wolne od DOG. Wykonanie 5 zapytań jest dosłownie szybsze niż wykonanie jednego TVP.
Jeff Atwood
4
@JeffAtwood - Czy próbowałeś przetasować zapytanie do czegoś podobnego SELECT * FROM tags WHERE tags.name IN (SELECT name from @tvp);? Teoretycznie powinno to być najszybsze podejście. Możesz użyć odpowiednich indeksów (np. Indeks nazwy znacznika, który INCLUDEbyłby idealny), a SQL Server powinien robić kilka prób, aby pobrać wszystkie znaczniki i ich liczbę. Jak wygląda plan?
Nick Chammas
9
Przetestowałem to również i jest to SZYBKIE JAKO OŚWIETLENIE (w porównaniu do konstruowania dużego ciągu IN). Miałem jednak pewne problemy z ustawieniem parametru, ponieważ ciągle otrzymywałem komunikat „Nie udało się przekonwertować wartości parametru z Int32 [] na IEnumerable`1.”. W każdym razie rozwiązałem to i oto próbka, którą zrobiłem pastebin.com/qHP05CXc
Fredrik Johansson
6
@FredrikJohansson - Spośród 130 głosów pozytywnych możesz być jedynym, który tak naprawdę próbował to uruchomić! Popełniłem błąd, czytając dokumenty, a tak naprawdę potrzebujesz IEnumerable <SqlDataRecord>, a nie tylko IEnumerable. Kod został zaktualizowany.
Mark Brackett
3
@MarkBrackett Świetnie z aktualizacją! Dokładnie ten kod naprawdę zaoszczędził mi dzień, ponieważ szukam indeksu wyszukiwania Lucene i czasami zwraca ponad 50 000 trafień, które należy dwukrotnie sprawdzić względem serwera SQL - więc tworzę tablicę int [] (document / Klucze SQL), a następnie pojawia się powyższy kod. Cały OP zajmuje teraz mniej niż 200 ms :)
Fredrik Johansson
188

Pierwotne pytanie brzmiało: „Jak sparametryzować zapytanie ...”

Pozwolę sobie stwierdzić, że nie jest to odpowiedź na pierwotne pytanie. Istnieją już pewne tego dowody w innych dobrych odpowiedziach.

Mając to na uwadze, śmiało zaznaczaj tę odpowiedź, głosuj na nią, oznacz ją jako brak odpowiedzi ... rób wszystko, co uważasz za słuszne.

Zobacz odpowiedź Marka Bracketta na preferowaną odpowiedź, którą głosowałem (i 231 innych osób). Podane w jego odpowiedzi podejście pozwala 1) na efektywne wykorzystanie zmiennych wiązania i 2) na predykaty, które są podatne na kalkulację.

Wybrana odpowiedź

Chciałbym się tutaj zająć podejściem podanym w odpowiedzi Joela Spolsky'ego, odpowiedź „wybrana” jako właściwa odpowiedź.

Podejście Joela Spolsky'ego jest sprytne. I działa rozsądnie, będzie wykazywać przewidywalne zachowanie i przewidywalną wydajność, biorąc pod uwagę „normalne” wartości, a także z normatywnymi przypadkami krawędzi, takimi jak NULL i pusty ciąg. I może być wystarczające dla konkretnego zastosowania.

Ale jeśli chodzi o uogólnienie tego podejścia, rozważmy również bardziej niejasne przypadki narożników, na przykład gdy Namekolumna zawiera znak wieloznaczny (rozpoznawany przez predykat LIKE). Najczęściej stosowanym znakiem wieloznacznym jest% znakiem (znak procentu.). Zajmijmy się tym teraz, a później przejdźmy do innych spraw.

Niektóre problemy ze znakiem%

Rozważ wartość nazwy 'pe%ter'. (W podanych tu przykładach zamiast nazwy kolumny używam literału). Wiersz o wartości nazwy `` pe% ter '' zostanie zwrócony przez zapytanie o formę:

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

Ale ten sam wiersz nie zostanie zwrócony, jeśli kolejność wyszukiwanych haseł zostanie odwrócona:

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

Zachowanie, które obserwujemy, jest dość dziwne. Zmiana kolejności wyszukiwanych haseł na liście zmienia zestaw wyników.

To prawie oczywiste, że możemy nie chcieć pe%ter dopasowywać masła orzechowego, bez względu na to, jak bardzo to lubi.

Niewyraźna obudowa narożna

(Tak, zgodzę się, że jest to niejasny przypadek. Prawdopodobnie taki, który prawdopodobnie nie zostanie przetestowany. Nie spodziewalibyśmy się znaku wieloznacznego w wartości kolumny. Możemy założyć, że aplikacja zapobiega przechowywaniu takiej wartości. Ale z mojego doświadczenia rzadko widziałem ograniczenie bazy danych, które wyraźnie zabraniało znaków lub wzorców, które byłyby uważane za symbole wieloznaczne po prawej stronie LIKEoperatora porównania.

Łatanie dziury

Jednym ze sposobów na załatanie tej dziury jest uniknięcie %znaku wieloznacznego. (Dla każdego, kto nie zna klauzuli Escape na operatorze, oto link do dokumentacji SQL Server .

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

Teraz możemy dopasować dosłowny%. Oczywiście, kiedy będziemy mieli nazwę kolumny, będziemy musieli dynamicznie uciec od znaku wieloznacznego. Możemy użyć tej REPLACEfunkcji, aby znaleźć wystąpienia %znaku i wstawić przed każdym odwrotnym ukośnikiem:

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

To rozwiązuje problem z% wildcard. Prawie.

Uciec ucieczki

Zdajemy sobie sprawę, że nasze rozwiązanie wprowadziło kolejny problem. Postać ucieczki. Widzimy, że będziemy również musieli uciec przed wszelkimi przypadkami samej postaci ucieczki. Tym razem korzystamy z! jako postać ucieczki:

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

Podkreślenie też

Teraz, gdy jesteśmy na rzucie, możemy dodać kolejny REPLACEuchwyt znaku wieloznacznego podkreślenia. I dla zabawy, tym razem użyjemy $ jako postaci ucieczki.

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

Wolę to podejście od ucieczki, ponieważ działa w Oracle i MySQL oraz SQL Server. (Zazwyczaj używam ukośnika odwrotnego \ jako znaku ucieczki, ponieważ jest to znak, którego używamy w wyrażeniach regularnych. Ale po co ograniczać się konwencją!

Te nieznośne nawiasy

SQL Server pozwala również traktować znaki wieloznaczne jako literały, umieszczając je w nawiasach []. Więc nie skończyliśmy jeszcze naprawiania, przynajmniej dla SQL Server. Ponieważ pary nawiasów mają specjalne znaczenie, musimy również uciec przed nimi. Jeśli uda nam się odpowiednio uciec od nawiasów, to przynajmniej nie będziemy musieli zawracać sobie głowy łącznikiem -i karatem ^w nawiasach. I możemy zostawić każdego %i_ znaków wewnątrz nawiasów uciekł, ponieważ będziemy mieli w zasadzie wyłączone specjalnego znaczenia nawiasów.

Znalezienie pasujących par nawiasów nie powinno być takie trudne. Jest to trochę trudniejsze niż radzenie sobie z wystąpieniem singletonu% i _. (Zauważ, że nie wystarczy po prostu uciec od wszystkich wystąpień nawiasów, ponieważ nawias singletonowy jest uważany za dosłowny i nie trzeba go uciekać. Logika staje się nieco bardziej niewyraźna niż mogę obsłużyć więcej testów .)

Wyrażenie wbudowane staje się nieporządne

To wyrażenie wbudowane w SQL staje się dłuższe i brzydsze. Prawdopodobnie możemy sprawić, by działało, ale niebo pomoże biednej duszy, która za nią stoi i musi ją rozszyfrować. Ponieważ jestem fanem wyrażeń wbudowanych, nie jestem skłonny do używania go tutaj, głównie dlatego, że nie chcę zostawiać komentarza wyjaśniającego przyczynę bałaganu i przepraszającego za to.

Funkcja gdzie?

Ok, więc jeśli nie będziemy traktować tego jako wyrażenia wbudowanego w SQL, najbliższą alternatywą, jaką mamy, jest funkcja zdefiniowana przez użytkownika. I wiemy, że to niczego nie przyspieszy (chyba że możemy zdefiniować na nim indeks, tak jak moglibyśmy to zrobić z Oracle.) Jeśli musimy stworzyć funkcję, lepiej to zrobić w kodzie wywołującym SQL komunikat.

Ta funkcja może mieć pewne różnice w zachowaniu, zależne od DBMS i wersji. (Pozdrawiam wszystkich programistów Java, którzy tak chętnie korzystają z dowolnego silnika bazy danych zamiennie).

Znajomość domen

Możemy mieć specjalistyczną wiedzę na temat domeny dla kolumny (tj. Zestawu dopuszczalnych wartości wymuszonych dla kolumny. Możemy z góry wiedzieć, że wartości przechowywane w kolumnie nigdy nie będą zawierały znaku procentu, podkreślenia lub nawiasu pary. W takim przypadku zamieszczamy tylko krótki komentarz, że te przypadki są objęte gwarancją.

Wartości przechowywane w kolumnie mogą zezwalać na znaki% lub _, ale ograniczenie może wymagać zmiany tych wartości, być może przy użyciu określonego znaku, tak aby wartości LIKE były „bezpieczne”. Ponownie, szybki komentarz na temat dozwolonego zestawu wartości, a zwłaszcza tego, która postać jest używana jako postać ucieczki, i podejdź do podejścia Joela Spolsky'ego.

Ale bez specjalistycznej wiedzy i gwarancji ważne jest, abyśmy przynajmniej rozważyli obsługę tych niejasnych narożnych spraw i zastanowili się, czy zachowanie jest rozsądne i „zgodne ze specyfikacją”.


Inne problemy podsumowano

Uważam, że inni już w wystarczającym stopniu wskazali niektóre z innych powszechnie rozważanych obszarów zainteresowania:

  • Wstrzykiwanie SQL (przyjmowanie informacji, które wydają się być informacjami dostarczonymi przez użytkownika, i uwzględnianie ich w tekście SQL zamiast dostarczania ich przez zmienne powiązania. Używanie zmiennych powiązania nie jest wymagane, jest to tylko jedno wygodne podejście do udaremnienia za pomocą wstrzykiwania SQL. Istnieją inne sposoby radzenia sobie z tym:

  • plan optymalizacyjny wykorzystujący skanowanie indeksów zamiast przeszukiwania indeksów, możliwa potrzeba wyrażenia lub funkcji do ucieczki symboli wieloznacznych (możliwy indeks wyrażenia lub funkcji)

  • użycie wartości literalnych zamiast zmiennych powiązań wpływa na skalowalność


Wniosek

Lubię podejście Joela Spolsky'ego. To sprytne. I to działa.

Ale gdy tylko to zobaczyłem, natychmiast zobaczyłem potencjalny problem z tym i nie jest moją naturą pozwolić mu się ześlizgnąć. Nie chcę krytykować wysiłków innych. Wiem, że wielu programistów traktuje swoją pracę bardzo osobiście, ponieważ tyle w nią inwestują i tak bardzo się o nią troszczą. Proszę więc zrozumieć, że to nie jest osobisty atak. To, co tu identyfikuję, to rodzaj problemu, który pojawia się w produkcji, a nie podczas testowania.

Tak, odszedłem daleko od pierwotnego pytania. Ale gdzie jeszcze zostawić tę notatkę dotyczącą tego, co uważam za ważny problem z „wybraną” odpowiedzią na pytanie?

spencer7593
źródło
czy możesz dać nam znać, jeśli używasz lub lubisz sparametryzowane zapytania? w tym konkretnym przypadku, czy poprawne jest przeskakiwanie zasady „używania sparametryzowanych zapytań” i dezynfekcji przy użyciu oryginalnego języka? DZIĘKUJE
Luis Siquot
2
@Luis: tak, wolę używać zmiennych powiązań w instrukcjach SQL i unikam zmiennych powiązań tylko wtedy, gdy ich użycie powoduje problemy z wydajnością. moim wzorcem normatywnym dla pierwotnego problemu byłoby dynamiczne utworzenie instrukcji SQL z wymaganą liczbą symboli zastępczych na liście IN, a następnie powiązanie każdej wartości z jednym z symboli zastępczych. Zobacz odpowiedź Marka Bracketta, która jest odpowiedzią, którą głosowałem (i 231 innych osób).
spencer7593
133

Możesz przekazać parametr jako ciąg

Więc masz ciąg

DECLARE @tags

SET @tags = ruby|rails|scruffy|rubyonrails

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

Następnie wystarczy przekazać ciąg jako 1 parametr.

Oto funkcja podziału, której używam.

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
źródło
2
Dzięki takiemu podejściu możesz również dołączyć do funkcji tabeli.
Michael Haren,
Korzystam z rozwiązania podobnego do tego w Oracle. Nie trzeba go ponownie analizować, tak jak niektóre inne rozwiązania.
Leigh Riffel,
9
Jest to podejście oparte wyłącznie na bazie danych, inne wymagają pracy w kodzie poza bazą danych.
David Basarab,
Czy to do skanowania tabeli, czy może skorzystać z indeksów itp.?
Pure.Krome
lepiej byłoby użyć CROSS APPLY względem funkcji tabeli SQL (przynajmniej w 2005 roku), która zasadniczo łączy się ze zwróconą tabelą
czosnek Adolfa
66

Słyszałem, jak Jeff / Joel rozmawia o tym dzisiaj w podcastie ( odcinek 34 , 2008-12-16 (MP3, 31 MB), 1 godz. 03 min 38 sek. - 1 godz. 06 min 45 sek.) I pomyślałem, że przypomniałem sobie przepełnienie stosu używał LINQ do SQL , ale może został porzucony. To samo dotyczy LINQ to SQL.

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

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

Otóż ​​to. I tak, LINQ spogląda już wystarczająco wstecz, ale Containsklauzula wydaje mi się dodatkowa wstecz. Kiedy musiałem wykonać podobne zapytanie dla projektu w pracy, naturalnie próbowałem zrobić to w niewłaściwy sposób, łącząc tablicę lokalną z tabelą SQL Server, stwierdzając, że translator LINQ na SQL byłby wystarczająco inteligentny, aby obsłużyć tłumaczenie jakoś. Nie zrobił tego, ale dostarczył opisowy komunikat o błędzie i skierował mnie do korzystania z Contains .

W każdym razie, jeśli uruchomisz to w wysoce zalecanym LINQPad i uruchomisz tę kwerendę, możesz wyświetlić rzeczywisty kod SQL wygenerowany przez dostawcę SQL LINQ. Pokaże ci każdą wartość sparametryzowaną w INklauzulę.

Peter Meyer
źródło
50

Jeśli dzwonisz z .NET, możesz użyć 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});

Tutaj Dapper myśli, więc nie musisz. Oczywiście w LINQ-SQL możliwe jest coś podobnego :

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
źródło
11
który akurat jest tym, czego używamy na tej stronie, w rzeczywistości zadawane pytanie ( elegancki
Sam Saffron
3
Zauważ, że dapper obsługuje
Marc Gravell
To się
odwraca,
29

Jest to prawdopodobnie w połowie paskudny sposób, użyłem go raz, był raczej skuteczny.

W zależności od celów może się przydać.

  1. Utwórz tabelę tymczasową z jedną kolumną.
  2. INSERT każda wartość wyszukiwania w tej kolumnie.
  3. Zamiast używać an IN, możesz po prostu użyć standardowych JOINreguł. (Elastyczność ++)

Daje to dodatkową elastyczność w zakresie tego, co możesz zrobić, ale bardziej nadaje się do sytuacji, w których masz dużą tabelę do zapytania, z dobrym indeksowaniem i chcesz użyć sparametryzowanej listy więcej niż jeden raz. Oszczędza to konieczności wykonywania go dwa razy i ręcznego wykonywania wszystkich warunków sanitarnych.

Nigdy nie zajmowałem się profilowaniem dokładnie tego, jak szybko było, ale w mojej sytuacji było to potrzebne.

Kent Fredric
źródło
To wcale nie jest paskudne! Co więcej, jest to bardzo czysty sposób IMHO. A jeśli spojrzysz na plan wykonania, zobaczysz, że jest taki sam jak klauzula IN. Zamiast tabeli tymczasowej można również utworzyć stałą tabelę z indeksami, w której parametry są przechowywane razem z SESSIONID.
SQL Police
27

W SQL Server 2016+można użyć STRING_SPLITfunkcji:

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

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

lub:

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

Akceptowane odpowiedź wola oczywiście pracy i jest to jeden do zrobienia, ale to jest anty-wzór.

E. Znajdź wiersze według listy wartości

Jest to zamiennik typowego anty-wzorca, takiego jak tworzenie dynamicznego ciągu SQL w warstwie aplikacji lub Transact-SQL, lub za pomocą operatora LIKE:

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

Dodatek :

Aby poprawić STRING_SPLIToszacowanie wiersza funkcji tabeli, dobrym pomysłem jest zmaterializowanie podzielonych wartości jako tymczasowych zmiennych tabeli / tabeli:

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

Powiązane: Jak przekazać listę wartości do procedury przechowywanej


Oryginalne pytanie ma wymóg SQL Server 2008. Ponieważ to pytanie jest często używane jako duplikat, dodałem tę odpowiedź jako odniesienie.

Łukasz Szozda
źródło
1
Nie testowałem tego doskonale, ale wydaje mi się, że jest to najczystsze rozwiązanie na 2016 rok. Nadal chciałbym móc przekazać szereg int, ale do tego czasu ...
Daniel
24

Mamy funkcję, która tworzy zmienną tabelową, do której możesz dołączyć:

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 

Więc:

@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
źródło
20

Jest to obrzydliwe, ale jeśli masz gwarancję co najmniej jednego, możesz:

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

Posiadanie IN („tag1”, „tag2”, „tag1”, „tag1”, „tag1”) będzie łatwo zoptymalizowane przez SQL Server. Dodatkowo otrzymujesz bezpośrednie wyszukiwanie indeksu

Matt Rogish
źródło
1
Parametry opcjonalne z opcją Null sprawdzają wydajność, ponieważ optymalizator wymaga liczby parametrów używanych do tworzenia wydajnych zapytań. Zapytanie o 5 parametrów może wymagać innego planu zapytań niż jeden na 500 parametrów.
Erik Hart
18

Moim zdaniem najlepszym źródłem rozwiązania tego problemu jest to, co zostało opublikowane na tej stronie:

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

Posługiwać się:

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

KREDYTY DLA: Dinakar Nethi

Paulo Henrique
źródło
Świetna odpowiedź, czyste i modułowe, super szybkie wykonanie, z wyjątkiem wstępnego parsowania CSV do tabeli (jednorazowo, niewielka liczba elementów). Chociaż mógłby użyć prostszego / szybszego charindex () zamiast patindex ()? Charindex () pozwala również na argument „start_location”, który może być w stanie uniknąć przerywania łańcucha wejściowego za każdym razem? Aby odpowiedzieć na oryginalne pytanie, wystarczy dołączyć wynik funkcji.
crokusek
18

Przekazałbym parametr typu tabeli (ponieważ jest to SQL Server 2008 ) i where existswykonałbym połączenie wewnętrzne. Możesz także użyć XML, używając sp_xml_preparedocument, a następnie nawet zindeksować tę tabelę tymczasową.

eulerfx
źródło
Odpowiedź Ph.E zawiera przykładową tabelę temp. Budowania (z csv).
crokusek
12

Prawidłowym sposobem IMHO jest przechowywanie listy w ciągu znaków (ograniczona długością przez obsługę DBMS); jedyną sztuczką jest to, że (w celu uproszczenia przetwarzania) mam separator (w moim przykładzie przecinek) na początku i na końcu łańcucha. Chodzi o „normalizację w locie”, przekształcenie listy w tabelę z jedną kolumną zawierającą jeden wiersz na wartość. To pozwala ci skręcić

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

w

w (wybierz ...)

lub (rozwiązanie, które prawdopodobnie wolałbym) zwykłe łączenie, jeśli dodasz „odrębny”, aby uniknąć problemów ze zduplikowanymi wartościami na liście.

Niestety techniki cięcia sznurka są dość specyficzne dla produktu. Oto wersja 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;

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

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

(Oczywiście „pivot” musi zwrócić tyle wierszy, ile maksymalna liczba elementów, jakie możemy znaleźć na liście)

Jeff Atwood
źródło
11

Jeśli masz program SQL Server 2008 lub nowszy, użyłbym parametru wycenionego w tabeli .

Jeśli masz pecha, aby utknąć w programie SQL Server 2005, możesz dodać taką funkcję CLR ,

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

Którego możesz użyć w ten sposób,

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

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

Myślę, że jest to przypadek, gdy zapytanie statyczne po prostu nie jest dobrym rozwiązaniem. Dynamicznie twórz listę klauzul in, unikaj pojedynczych cudzysłowów i dynamicznie buduj SQL. W tym przypadku prawdopodobnie nie zobaczysz żadnej różnicy z żadną metodą ze względu na małą listę, ale najbardziej wydajną metodą jest naprawdę wysłanie SQL dokładnie tak, jak jest napisany w poście. Myślę, że dobrym zwyczajem jest pisanie go w najbardziej efektywny sposób, zamiast robić to, co czyni najładniejszy kod, lub uważać za złą praktykę dynamiczne budowanie SQL.

Widziałem, że wykonywanie funkcji podziału trwa dłużej niż samo zapytanie w wielu przypadkach, gdy parametry stają się duże. Procedura przechowywana z parametrami o wartościach przechowywanych w tabeli w SQL 2008 to jedyna inna opcja, którą bym rozważył, chociaż prawdopodobnie będzie ona wolniejsza w twoim przypadku. TVP będzie prawdopodobnie szybsze tylko dla dużych list, jeśli szukasz klucza podstawowego TVP, ponieważ SQL i tak zbuduje tymczasową tabelę dla listy (jeśli lista jest duża). Nie będziesz tego pewien, chyba że go przetestujesz.

Widziałem również procedury składowane, które miały 500 parametrów z domyślnymi wartościami null i posiadały GDZIE Kolumna1 IN (@ Param1, @ Param2, @ Param3, ..., @ Param500). Spowodowało to, że SQL zbudował tabelę tymczasową, wykonał sortowanie / odrębne, a następnie wykonał skanowanie tabeli zamiast przeszukiwania indeksu. Zasadniczo to robiłbyś, parametryzując to zapytanie, chociaż na tyle małą skalę, że nie zrobi to zauważalnej różnicy. Zdecydowanie odradzam wpisywanie NULL na twoich listach IN, ponieważ jeśli zostanie to zmienione na NOT IN, nie będzie działać zgodnie z przeznaczeniem. Możesz dynamicznie budować listę parametrów, ale jedyną oczywistą rzeczą, którą zyskasz, jest to, że obiekty unikną za ciebie pojedynczych cudzysłowów. Takie podejście jest również nieco wolniejsze po stronie aplikacji, ponieważ obiekty muszą przeanalizować zapytanie, aby znaleźć parametry.

Ponowne użycie planów wykonania dla procedur przechowywanych lub sparametryzowanych zapytań może dać ci wzrost wydajności, ale zablokuje cię w jednym planie wykonania określonym przez pierwsze wykonywane zapytanie. W wielu przypadkach może to być mniej niż idealne dla kolejnych zapytań. W twoim przypadku ponowne użycie planów wykonania będzie prawdopodobnie plusem, ale może nie mieć żadnej różnicy, ponieważ przykład jest naprawdę prostym zapytaniem.

Fiszki:

W twoim przypadku cokolwiek robisz, czy to parametryzacja ze stałą liczbą elementów na liście (null, jeśli nie jest używana), dynamiczne budowanie zapytania z parametrami lub bez parametrów, lub stosowanie procedur przechowywanych z parametrami o wartościach przechowywanych w tabeli nie zrobi dużej różnicy . Jednak moje ogólne zalecenia są następujące:

Twoja sprawa / proste zapytania z kilkoma parametrami:

Dynamiczny SQL, może z parametrami, jeśli testy wykazują lepszą wydajność.

Zapytania z planami wykonania wielokrotnego użytku, wywoływane wielokrotnie po prostu przez zmianę parametrów lub jeśli zapytanie jest skomplikowane:

SQL z parametrami dynamicznymi.

Zapytania z dużymi listami:

Procedura składowana z parametrami wycenionymi w tabeli. Jeśli lista może się różnić o wiele, użyj Z RECOMPILE procedury składowanej lub po prostu użyj dynamicznego SQL bez parametrów, aby wygenerować nowy plan wykonania dla każdego zapytania.

Scott
źródło
Co rozumiesz przez „procedurę przechowywaną” tutaj? Czy możesz podać przykład?
struhtanov
9

Być może możemy użyć XML tutaj:

    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
źródło
1
CTEi @xmożna je wyeliminować / wstawić do podselekcji, jeśli zostanie to wykonane bardzo ostrożnie, jak pokazano w tym artykule .
robert4,
9

Podchodzę do tego domyślnie, przekazując funkcję o wartości tabeli (która zwraca tabelę z ciągu) do warunku IN.

Oto kod dla UDF (mam go gdzieś z Stack Overflow, nie mogę teraz znaleźć źródła)

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
  )

Po uzyskaniu tego kod będzie tak prosty:

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

Jeśli nie masz absurdalnie długiego łańcucha, powinno to dobrze działać z indeksem tabeli.

W razie potrzeby możesz wstawić go do tabeli tymczasowej, zindeksować, a następnie uruchomić łączenie ...

Eli Ekstein
źródło
8

Innym możliwym rozwiązaniem jest zamiast przekazywania zmiennej liczby argumentów do procedury składowanej, przekazanie pojedynczego ciągu zawierającego nazwy, których szukasz, ale uczynienie ich unikalnymi poprzez otoczenie ich znakiem „<>”. Następnie użyj PATINDEX, aby znaleźć nazwy:

SELECT * 
FROM Tags 
WHERE PATINDEX('%<' + Name + '>%','<jo>,<john>,<scruffy>,<rubyonrails>') > 0
ArtOfCoding
źródło
8

Skorzystaj z następującej procedury składowanej. Wykorzystuje niestandardową funkcję podziału, którą można znaleźć tutaj .

 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
źródło
8

Jeśli w łańcuchu IN mamy zapisane łańcuchy z przecinkami (,), możemy użyć funkcji charindex, aby uzyskać wartości. Jeśli używasz platformy .NET, możesz mapować za pomocą SqlParameters.

Skrypt 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

Możesz użyć powyższej instrukcji w kodzie .NET i zamapować parametr za pomocą SqlParameter.

Demo skrzypka

EDYCJA: Utwórz tabelę o nazwie SelectedTags, używając następującego skryptu.

Skrypt 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
źródło
Czy możesz pokazać przykład tego działania, w którym nie ma zakodowanej listy możliwych wartości?
John Saunders
@JohnSaunders, edytowałem skrypt bez używania listy zakodowanej na stałe. Proszę zweryfikuj.
Gowdhaman008
3
Jedno ograniczenie z tą opcją. CharIndex zwraca 1, jeśli ciąg zostanie znaleziony. IN zwraca dopasowanie dla dokładnych warunków. CharIndex dla „Stack” zwróci 1 dla terminu „StackOverflow” IN nie. Istnieje drobna poprawka do tej odpowiedzi za pomocą PatIndex powyżej, która zawiera nazwy z „<'% name%'>”, która eliminuje to ograniczenie. Jednak kreatywne rozwiązanie tego problemu.
Richard Vivian
7

W przypadku zmiennej liczby takich argumentów jedyny znany mi sposób to albo jawne wygenerowanie kodu SQL, albo zrobienie czegoś, co wymaga zapełnienia tabeli tymczasowej wybranymi elementami i połączenia jej z tabelą tymczasową.

ConcernedOfTunbridgeWells
źródło
7

W ColdFusion po prostu wykonujemy:

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

Oto technika, która odtwarza tabelę lokalną do użycia w ciągu zapytania. Wykonanie tego w ten sposób eliminuje wszystkie problemy z analizą.

Ciąg może być zbudowany w dowolnym języku. W tym przykładzie użyłem SQL, ponieważ był to oryginalny problem, który próbowałem rozwiązać. Potrzebowałem czystego sposobu na przekazywanie danych tabeli w locie w ciągu, który zostanie wykonany później.

Korzystanie z typu zdefiniowanego przez użytkownika jest opcjonalne. Tworzenie typu jest tworzone tylko raz i może być wykonane z wyprzedzeniem. W przeciwnym razie po prostu dodaj pełny typ tabeli do deklaracji w ciągu.

Ogólny wzorzec jest łatwy do rozszerzenia i może być używany do przekazywania bardziej złożonych tabel.

-- 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)
Rockfish
źródło
7

W SQL Server 2016+ inną możliwością jest użycie tej OPENJSONfunkcji.

Takie podejście jest blogowane w OPENJSON - jednym z najlepszych sposobów wybierania wierszy według listy identyfikatorów .

W pełni działający przykład poniżej

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
źródło
7

Oto kolejna alternatywa. Po prostu przekaż listę rozdzielaną przecinkami jako parametr ciągu do procedury składowanej i:

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

I funkcja:

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
Metafora
źródło
6

Mam odpowiedź, która nie wymaga UDF, XML, ponieważ IN akceptuje instrukcję select, np. SELECT * FROM Test gdzie Data IN (SELECT Value FROM TABLE)

Naprawdę potrzebujesz tylko sposobu, aby przekonwertować ciąg znaków na tabelę.

Można to zrobić za pomocą rekurencyjnego CTE lub zapytania z tabelą liczb (lub Master..spt_value)

Oto wersja 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);
Uruchom punkt
źródło
6

Używam bardziej zwięzłej wersji najczęściej głosowanej odpowiedzi :

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

Dwukrotnie przechodzi przez parametry znacznika; ale to nie ma znaczenia przez większość czasu (to nie będzie twoje wąskie gardło; jeśli tak, to rozwiń pętlę).

Jeśli naprawdę interesujesz się wydajnością i nie chcesz dwa razy powtarzać pętli, oto mniej piękna wersja:

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
źródło
5

Oto kolejna odpowiedź na ten problem.

(nowa wersja opublikowana 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;
    }

Twoje zdrowie.

Darek
źródło
4

Jedynym zwycięskim ruchem jest nie grać.

Nie ma dla ciebie nieskończonej zmienności. Tylko skończona zmienność.

W SQL masz taką klauzulę:

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

W kodzie C # robisz coś takiego:

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

Zasadniczo więc, jeśli liczba wynosi 0, nie ma filtra i wszystko przechodzi. Jeśli liczba jest większa niż 0, wówczas wartość musi znajdować się na liście, ale lista została uzupełniona do pięciu z niemożliwymi wartościami (aby SQL nadal miał sens)

Czasami kulawe rozwiązanie jest jedynym, które faktycznie działa.

Jason Henriksen
źródło