Tham số hóa mệnh đề SQL IN


1041

Làm cách nào để tôi tham số hóa một truy vấn có chứa một INmệnh đề với số lượng đối số thay đổi, như đối số này?

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

Trong truy vấn này, số lượng đối số có thể là từ 1 đến 5.

Tôi không muốn sử dụng một thủ tục được lưu trữ dành riêng cho điều này (hoặc XML), nhưng nếu có một cách thanh lịch cụ thể nào đó đối với SQL Server 2008 , tôi sẵn sàng làm điều đó.



Câu trả lời:


316

Đây là một kỹ thuật nhanh và bẩn mà tôi đã sử dụng:

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

Vì vậy, đây là mã 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);
}

Hai hãy cẩn thận:

  • Hiệu suất là khủng khiếp. LIKE "%...%"truy vấn không được lập chỉ mục.
  • Đảm bảo bạn không có bất kỳ |thẻ trống, trống hoặc không hoặc điều này sẽ không hoạt động

Có nhiều cách khác để thực hiện điều này mà một số người có thể coi là sạch hơn, vì vậy hãy tiếp tục đọc.


119
Đó sẽ là hella chậm
Matt Rogish

13
Vâng, đây là quét bảng. Tuyệt vời cho 10 hàng, tệ hại cho 100.000.
Will Hartung

17
Hãy chắc chắn rằng bạn kiểm tra các thẻ có đường ống trong đó.
Joel Coehoorn

17
Điều này thậm chí không trả lời câu hỏi. Cấp, thật dễ dàng để xem nơi để thêm các tham số, nhưng làm thế nào bạn có thể chấp nhận giải pháp này nếu nó thậm chí không bận tâm đến việc tham số hóa truy vấn? Nó chỉ trông đơn giản hơn @Mark Brackett vì nó không được tham số hóa.
tvanfosson

21
Điều gì xảy ra nếu thẻ của bạn là 'ruby | rails'. Nó sẽ phù hợp, mà sẽ sai. Khi bạn đưa ra các giải pháp như vậy, bạn cần đảm bảo các thẻ không chứa đường ống hoặc lọc rõ ràng chúng: select * từ Thẻ trong đó '| ruby ​​| rails | Scruffy | rubyonrails |' như '% |' + Tên + '|%' VÀ tên không giống như '%!%'
AK

729

Bạn có thể tham số hóa từng giá trị, vì vậy một cái gì đó như:

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

Mà sẽ cung cấp cho bạn:

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"

Không, điều này không mở cho SQL tiêm . Văn bản được chèn duy nhất vào CommandText không dựa trên đầu vào của người dùng. Nó chỉ dựa trên tiền tố "@tag" được mã hóa cứng và chỉ mục của một mảng. Chỉ mục sẽ luôn là số nguyên, không do người dùng tạo và an toàn.

Các giá trị nhập vào của người dùng vẫn được nhồi vào các tham số, do đó không có lỗ hổng nào ở đó.

Biên tập:

Đặt các mối quan tâm sang một bên, lưu ý rằng việc xây dựng văn bản lệnh để chứa một số lượng tham số (như trên) cản trở khả năng của máy chủ SQL để tận dụng các truy vấn được lưu trong bộ nhớ cache. Kết quả cuối cùng là bạn gần như chắc chắn mất giá trị của việc sử dụng các tham số ở vị trí đầu tiên (trái ngược với việc chỉ chèn các chuỗi vị ngữ vào chính SQL).

Không phải các gói truy vấn được lưu trong bộ nhớ cache không có giá trị, nhưng IMO truy vấn này gần như không đủ phức tạp để thấy nhiều lợi ích từ nó. Mặc dù chi phí biên dịch có thể đạt tới (hoặc thậm chí vượt quá) chi phí thực hiện, bạn vẫn đang nói chuyện trong một phần nghìn giây.

Nếu bạn có đủ RAM, tôi hy vọng SQL Server có thể sẽ lưu trữ một kế hoạch cho số lượng tham số chung. Tôi cho rằng bạn luôn có thể thêm năm tham số và để các thẻ không xác định là NULL - kế hoạch truy vấn phải giống nhau, nhưng nó có vẻ khá xấu đối với tôi và tôi không chắc rằng nó đáng để tối ưu hóa vi mô (mặc dù, trên Stack Overflow - rất có thể nó có giá trị).

Ngoài ra, SQL Server 7 trở lên sẽ tự động tham số hóa các truy vấn , do đó, việc sử dụng các tham số không thực sự cần thiết từ quan điểm hiệu suất - tuy nhiên, điều quan trọng là từ quan điểm bảo mật - đặc biệt là với dữ liệu người dùng nhập vào như thế này.


2
Về cơ bản giống như câu trả lời của tôi cho câu hỏi "có liên quan" và rõ ràng là giải pháp tốt nhất vì nó mang tính xây dựng và hiệu quả hơn là diễn giải (khó hơn nhiều).
tvanfosson

49
Đây là cách LINQ to SQL thực hiện nó, BTW
Mark Cidade

3
@Pure: Toàn bộ vấn đề này là để tránh SQL Injection, điều mà bạn sẽ dễ bị tổn thương nếu bạn sử dụng SQL động.
Ray

4
@God of Data - Có, tôi cho rằng nếu bạn cần hơn 2100 thẻ, bạn sẽ cần một giải pháp khác. Nhưng Basarb chỉ có thể đạt 2100 nếu độ dài thẻ trung bình là <3 ký tự (vì bạn cũng cần một dấu phân cách). msdn.microsoft.com/en-us/l Library / ms143432.aspx
Mark Brackett

2
@bonCodigo - các giá trị được chọn của bạn nằm trong một mảng; bạn chỉ cần lặp qua mảng và thêm một tham số (được thêm vào với chỉ mục) cho mỗi cái.
Mark Brackett

249

Đối với SQL Server 2008, bạn có thể sử dụng tham số có giá trị bảng . Đó là một chút công việc, nhưng nó được cho là sạch hơn phương pháp khác của tôi .

Đầu tiên, bạn phải tạo một loại

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

Sau đó, mã ADO.NET của bạn trông như thế này:

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

41
chúng tôi đã thử nghiệm điều này và các tham số có giá trị bảng là DOG chậm. Nghĩa đen là thực hiện 5 truy vấn nhanh hơn so với thực hiện một TVP.
Jeff Atwood

4
@JeffAtwood - Bạn đã thử chia sẻ lại truy vấn thành một cái gì đó như thế SELECT * FROM tags WHERE tags.name IN (SELECT name from @tvp);nào chưa? Về lý thuyết, đây thực sự nên là cách tiếp cận nhanh nhất. Bạn có thể sử dụng các chỉ mục có liên quan (ví dụ: một chỉ mục về tên thẻ INCLUDEsẽ là lý tưởng) và SQL Server sẽ thực hiện một vài lần tìm cách lấy tất cả các thẻ và số lượng của chúng. Kế hoạch trông như thế nào?
Nick Chammas

9
Tôi cũng đã thử nghiệm điều này và nó NHANH CHÓNG NHƯ VẬY (so với việc xây dựng một chuỗi IN lớn). Tôi đã gặp một số vấn đề khi cài đặt tham số mặc dù tôi liên tục nhận được "Không thể chuyển đổi giá trị tham số từ Int32 [] sang IEnumerable`1." Dù sao, đã giải quyết điều đó và đây là một mẫu tôi đã tạo pastebin.com/qHP05CXc
Fredrik Johansson

6
@FredrikJohansson - Trong số 130 lượt upvote, bạn có thể là lần chạy duy nhất thực sự cố gắng chạy này! Tôi đã mắc lỗi khi đọc các tài liệu và bạn thực sự cần một <SqlDataRecord> của IEn chứ không phải bất kỳ IEnumerable nào. Mã đã được cập nhật.
Mark Brackett

3
@MarkBrackett Tuyệt vời với bản cập nhật! Chính xác, mã này thực sự đã tiết kiệm được một ngày cho tôi vì tôi đang truy tìm chỉ mục tìm kiếm Lucene và đôi khi nó trả về hơn 50.000 lần truy cập cần được nhân đôi với máy chủ SQL - Vì vậy, tôi tạo một mảng int [] (document / Các khóa SQL) và sau đó mã ở trên xuất hiện. Toàn bộ OP hiện chỉ mất chưa đến 200ms :)
Fredrik Johansson

188

Câu hỏi ban đầu là "Làm cách nào để tôi tham số hóa truy vấn ..."

Hãy để tôi nói ngay tại đây, rằng đây không phải là một câu trả lời cho câu hỏi ban đầu. Đã có một số minh chứng về điều đó trong các câu trả lời tốt khác.

Như đã nói, hãy tiếp tục và đánh dấu câu trả lời này, đánh giá thấp nó, đánh dấu nó không phải là một câu trả lời ... làm bất cứ điều gì bạn tin là đúng.

Xem câu trả lời từ Mark Brackett để biết câu trả lời ưa thích mà tôi (và 231 người khác) nêu lên. Cách tiếp cận được đưa ra trong câu trả lời của anh ấy cho phép 1) sử dụng hiệu quả các biến liên kết và 2) cho các vị từ có thể mở rộng được.

Câu trả lời được chọn

Điều tôi muốn giải quyết ở đây là cách tiếp cận được đưa ra trong câu trả lời của Joel Spolsky, câu trả lời "được chọn" là câu trả lời đúng.

Cách tiếp cận của Joel Spolsky là thông minh. Và nó hoạt động hợp lý, nó sẽ thể hiện hành vi có thể dự đoán và hiệu suất có thể dự đoán được, với các giá trị "bình thường" và với các trường hợp cạnh chuẩn, chẳng hạn như NULL và chuỗi rỗng. Và nó có thể là đủ cho một ứng dụng cụ thể.

Nhưng về mặt khái quát hóa phương pháp này, chúng ta cũng hãy xem xét các trường hợp góc tối hơn, như khi Namecột chứa ký tự đại diện (như được nhận biết bởi vị từ THÍCH.) Ký tự đại diện tôi thấy thường được sử dụng là %(ký hiệu phần trăm.). Vì vậy, bây giờ hãy giải quyết vấn đề đó ở đây và sau đó tiếp tục các trường hợp khác.

Một số vấn đề với% character

Xem xét một giá trị Tên của 'pe%ter'. (Đối với các ví dụ ở đây, tôi sử dụng giá trị chuỗi bằng chữ thay cho tên cột.) Một hàng có giá trị Tên là '' pe% ter 'sẽ được trả về bởi một truy vấn có dạng:

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

Nhưng hàng tương tự sẽ không được trả về nếu thứ tự của các cụm từ tìm kiếm bị đảo ngược:

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

Hành vi chúng tôi quan sát là loại kỳ lạ. Thay đổi thứ tự của các cụm từ tìm kiếm trong danh sách sẽ thay đổi tập kết quả.

Nó gần như đi mà không nói rằng chúng ta có thể không muốn pe%terkết hợp bơ đậu phộng, bất kể anh ấy thích nó như thế nào.

Trường hợp góc tối

(Có, tôi sẽ đồng ý rằng đây là trường hợp tối nghĩa. Có lẽ một trường hợp không có khả năng được kiểm tra. Chúng tôi không mong đợi ký tự đại diện trong giá trị cột. Chúng tôi có thể giả định rằng ứng dụng ngăn chặn giá trị đó được lưu trữ. Nhưng theo kinh nghiệm của tôi, tôi hiếm khi thấy một ràng buộc cơ sở dữ liệu đặc biệt không cho phép các ký tự hoặc mẫu được coi là ký tự đại diện ở phía bên phải của LIKEtoán tử so sánh.

Vá một lỗ

Một cách tiếp cận để vá lỗ hổng này là thoát khỏi %ký tự đại diện. (Đối với bất kỳ ai không quen thuộc với mệnh đề thoát trên toán tử, đây là liên kết đến tài liệu SQL Server .

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

Bây giờ chúng ta có thể khớp với% theo nghĩa đen. Tất nhiên, khi chúng ta có một tên cột, chúng ta sẽ cần phải tự động thoát khỏi ký tự đại diện. Chúng ta có thể sử dụng REPLACEhàm để tìm các lần xuất hiện của %ký tự và chèn ký tự dấu gạch chéo ngược trước mỗi ký tự, như sau:

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

Vì vậy, giải quyết vấn đề với% ký tự đại diện. Hầu hết.

Thoát khỏi lối thoát

Chúng tôi nhận ra rằng giải pháp của chúng tôi đã giới thiệu một vấn đề khác. Nhân vật trốn thoát. Chúng tôi thấy rằng chúng tôi cũng sẽ cần phải thoát khỏi bất kỳ sự xuất hiện của chính nhân vật thoát. Lần này, chúng tôi sử dụng! như nhân vật thoát:

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

Gạch dưới quá

Bây giờ chúng tôi đang triển khai, chúng tôi có thể thêm một thẻ REPLACEđiều khiển ký tự gạch dưới. Và để giải trí, lần này, chúng ta sẽ sử dụng $ làm ký tự thoát.

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

Tôi thích cách tiếp cận này hơn để thoát vì nó hoạt động trong Oracle và MySQL cũng như SQL Server. (Tôi thường sử dụng dấu gạch chéo ngược làm ký tự thoát, vì đó là ký tự chúng ta sử dụng trong các biểu thức thông thường. Nhưng tại sao lại bị ràng buộc bởi quy ước!

Những dấu ngoặc

SQL Server cũng cho phép các ký tự đại diện được coi là chữ bằng cách đặt chúng trong ngoặc []. Vì vậy, chúng tôi chưa hoàn thành việc sửa chữa, ít nhất là cho SQL Server. Vì các cặp ngoặc có ý nghĩa đặc biệt, chúng tôi cũng sẽ cần phải thoát chúng. Nếu chúng ta quản lý để thoát đúng dấu ngoặc, thì ít nhất chúng ta sẽ không phải bận tâm với dấu gạch nối -và carat ^trong dấu ngoặc. Và chúng ta có thể để bất kỳ %và các _ký tự bên trong dấu ngoặc thoát, vì về cơ bản chúng ta đã vô hiệu hóa ý nghĩa đặc biệt của dấu ngoặc.

Tìm cặp dấu ngoặc phù hợp không nên khó. Khó hơn một chút so với việc xử lý các lần xuất hiện của singleton% và _. (Lưu ý rằng việc thoát khỏi tất cả các lần xuất hiện của dấu ngoặc là không đủ, bởi vì dấu ngoặc đơn được coi là bằng chữ và không cần phải thoát. Logic trở nên khó hiểu hơn tôi có thể xử lý mà không cần chạy nhiều trường hợp kiểm tra .)

Biểu hiện nội tuyến trở nên lộn xộn

Biểu thức nội tuyến đó trong SQL ngày càng dài hơn và xấu hơn. Có lẽ chúng ta có thể làm cho nó hoạt động, nhưng thiên đàng giúp đỡ linh hồn tội nghiệp phía sau và phải giải mã nó. Với nhiều người hâm mộ tôi dành cho các biểu thức nội tuyến, tôi có xu hướng không sử dụng nó ở đây, chủ yếu vì tôi không muốn phải để lại một bình luận giải thích lý do cho sự lộn xộn và xin lỗi về nó.

Một chức năng ở đâu?

Được rồi, vì vậy, nếu chúng ta không xử lý đó như là một biểu thức nội tuyến trong SQL, thì sự thay thế gần nhất mà chúng ta có là một hàm do người dùng định nghĩa. Và chúng ta biết rằng sẽ không tăng tốc mọi thứ (trừ khi chúng ta có thể định nghĩa một chỉ mục trên nó, giống như chúng ta có thể với Oracle.) Nếu chúng ta phải tạo một hàm, chúng ta có thể làm điều đó tốt hơn trong mã gọi SQL tuyên bố.

Và chức năng đó có thể có một số khác biệt trong hành vi, phụ thuộc vào DBMS và phiên bản. (Một tiếng hét cho tất cả các nhà phát triển Java của bạn rất quan tâm đến việc có thể sử dụng bất kỳ công cụ cơ sở dữ liệu nào có thể hoán đổi cho nhau.)

Kiến thức tên miền

Chúng tôi có thể có kiến ​​thức chuyên môn về miền cho cột, (nghĩa là tập hợp các giá trị được phép thi hành cho cột. Chúng tôi có thể biết một tiên nghiệm rằng các giá trị được lưu trữ trong cột sẽ không bao giờ chứa dấu phần trăm, dấu gạch dưới hoặc dấu ngoặc trong trường hợp đó, chúng tôi chỉ bao gồm một nhận xét nhanh rằng những trường hợp đó được bảo hiểm.

Các giá trị được lưu trữ trong cột có thể cho phép% hoặc _ ký tự, nhưng một ràng buộc có thể yêu cầu các giá trị đó được thoát, có thể sử dụng một ký tự được xác định, sao cho các giá trị là so sánh "an toàn". Một lần nữa, một nhận xét nhanh về tập hợp các giá trị được phép và đặc biệt là nhân vật nào được sử dụng làm nhân vật thoát hiểm và đi theo cách tiếp cận của Joel Spolsky.

Nhưng, không có kiến ​​thức chuyên môn và đảm bảo, điều quan trọng nhất là chúng tôi phải xem xét xử lý các trường hợp góc tối nghĩa đó và xem xét liệu hành vi đó có hợp lý và "theo thông số kỹ thuật" hay không.


Các vấn đề khác tóm tắt

Tôi tin rằng những người khác đã chỉ ra đầy đủ một số lĩnh vực quan tâm thường được xem xét khác:

  • Việc tiêm SQL (lấy thông tin có vẻ là thông tin do người dùng cung cấp và bao gồm thông tin trong văn bản SQL thay vì cung cấp chúng thông qua các biến liên kết. Sử dụng biến liên kết không bắt buộc, đó chỉ là một cách tiếp cận thuận tiện để ngăn chặn việc tiêm SQL. cách để đối phó với nó:

  • kế hoạch tối ưu hóa bằng cách sử dụng quét chỉ mục thay vì tìm kiếm chỉ mục, có thể cần một biểu thức hoặc hàm để thoát ký tự đại diện (chỉ mục có thể trên biểu thức hoặc hàm)

  • sử dụng các giá trị bằng chữ thay cho các biến liên kết ảnh hưởng đến khả năng mở rộng


Phần kết luận

Tôi thích cách tiếp cận của Joel Spolsky. Thật thông minh. Và nó hoạt động.

Nhưng ngay khi tôi nhìn thấy nó, tôi lập tức thấy một vấn đề tiềm ẩn với nó, và đó không phải là bản chất của tôi để cho nó trượt. Tôi không có ý chỉ trích những nỗ lực của người khác. Tôi biết nhiều nhà phát triển thực hiện công việc của họ rất cá nhân, vì họ đầu tư rất nhiều vào nó và họ quan tâm rất nhiều về nó. Vì vậy, hãy hiểu, đây không phải là một cuộc tấn công cá nhân. Những gì tôi xác định ở đây là loại vấn đề nảy sinh trong sản xuất hơn là thử nghiệm.

Vâng, tôi đã đi xa từ câu hỏi ban đầu. Nhưng nơi nào khác để lại ghi chú này liên quan đến những gì tôi coi là một vấn đề quan trọng với câu trả lời "được chọn" cho một câu hỏi?


bạn có thể vui lòng cho chúng tôi biết nếu bạn sử dụng hoặc thích các truy vấn được tham số hóa không? trong trường hợp cụ thể này có đúng không khi nhảy qua quy tắc 'sử dụng truy vấn được tham số hóa' và vệ sinh với ngôn ngữ gốc?
CẢM

2
@Luis: có, tôi thích sử dụng các biến liên kết trong các câu lệnh SQL và sẽ chỉ tránh các biến liên kết khi sử dụng chúng gây ra vấn đề về hiệu năng. mẫu quy phạm của tôi cho vấn đề ban đầu sẽ là tự động tạo câu lệnh SQL với số lượng giữ chỗ cần thiết trong danh sách IN, sau đó liên kết từng giá trị với một trong các trình giữ chỗ. Xem câu trả lời từ Mark Brackett, đó là câu trả lời mà tôi (và 231 người khác) nêu lên.
spencer7593

133

Bạn có thể truyền tham số dưới dạng chuỗi

Vì vậy, bạn có chuỗi

DECLARE @tags

SET @tags = ruby|rails|scruffy|rubyonrails

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

Sau đó, tất cả những gì bạn phải làm là truyền chuỗi dưới dạng 1 tham số.

Đây là chức năng phân chia tôi sử dụng.

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

2
Bạn cũng có thể tham gia vào chức năng bảng với phương pháp này.
Michael Haren

Tôi sử dụng một giải pháp tương tự như thế này trong Oracle. Nó không phải được phân tích lại như một số giải pháp khác làm.
Leigh Riffel

9
Đây là một cách tiếp cận cơ sở dữ liệu thuần túy, yêu cầu khác làm việc trong mã bên ngoài cơ sở dữ liệu.
David Basarab

Điều này để quét bảng hoặc nó có thể tận dụng các chỉ mục, vv?
Pure.Krom

tốt hơn là sử dụng CROSS ỨNG DỤNG với chức năng bảng SQL (ít nhất là vào năm 2005 trở đi), về cơ bản là tham gia vào bảng được trả lại
adolf tỏi

66

Tôi đã nghe Jeff / Joel nói về điều này trên podcast ngày hôm nay ( tập 34 , 2008-12-16 (MP3, 31 MB), 1 giờ 03 phút 38 giây - 1 giờ 06 phút 45 giây) và tôi nghĩ rằng tôi đã nhớ lại Stack Overflow đã sử dụng LINQ to SQL , nhưng có lẽ nó đã bị bỏ. Đây là điều tương tự trong LINQ to SQL.

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

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

Đó là nó. Và, vâng, LINQ đã nhìn về phía sau đủ, nhưng Containsmệnh đề có vẻ ngược lại với tôi. Khi tôi phải thực hiện một truy vấn tương tự cho một dự án tại nơi làm việc, tôi tự nhiên đã cố gắng làm điều này sai cách bằng cách thực hiện nối giữa mảng cục bộ và bảng SQL Server, hình dung trình dịch LINQ sang SQL sẽ đủ thông minh để xử lý dịch bằng cách nào đó. Không, nhưng nó đã cung cấp một thông báo lỗi mang tính mô tả và hướng tôi đến việc sử dụng Chứa .

Dù sao, nếu bạn chạy nó trong LINQPad được đề xuất cao và chạy truy vấn này, bạn có thể xem SQL thực tế mà nhà cung cấp LINQ SQL tạo ra. Nó sẽ hiển thị cho bạn từng giá trị được tham số hóa thành một INmệnh đề.


50

Nếu bạn đang gọi từ .NET, bạn có thể sử dụng 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});

Ở đây Dapper thực hiện suy nghĩ, vì vậy bạn không cần phải làm vậy. Dĩ nhiên, một cái gì đó tương tự có thể xảy ra với LINQ to SQL :

string[] names = new string[] {"ruby","rails","scruffy","rubyonrails"};
var tags = from tag in dataContext.Tags
           where names.Contains(tag.Name)
           orderby tag.Count descending
           select tag;

11
đó là những gì chúng tôi sử dụng trên trang này, cho câu hỏi thực tế được hỏi (dapper) i.stack.imgur.com/RBAjL.png
Sam Saffron


Điều này sẽ xảy ra nếu tên dài
cs0815

29

Đây có thể là một nửa cách làm khó chịu, tôi đã sử dụng nó một lần, khá hiệu quả.

Tùy thuộc vào mục tiêu của bạn, nó có thể được sử dụng.

  1. Tạo một bảng tạm thời với một cột.
  2. INSERT mỗi giá trị tra cứu vào cột đó.
  3. Thay vì sử dụng một IN, sau đó bạn có thể chỉ cần sử dụng các JOINquy tắc tiêu chuẩn của bạn . (Tính linh hoạt ++)

Điều này có thêm một chút linh hoạt trong những gì bạn có thể làm, nhưng nó phù hợp hơn cho các tình huống bạn có một bảng lớn để truy vấn, với chỉ mục tốt và bạn muốn sử dụng danh sách tham số nhiều lần. Tiết kiệm phải thực hiện hai lần và thực hiện tất cả các vệ sinh bằng tay.

Tôi không bao giờ có được hồ sơ chính xác nhanh như thế nào , nhưng trong tình huống của tôi, nó là cần thiết.


Điều này không khó chịu chút nào! Thậm chí, đó là IMHO một cách rất sạch sẽ. Và nếu bạn nhìn vào kế hoạch thực hiện, bạn sẽ thấy rằng nó giống như mệnh đề IN. Thay vì bảng tạm thời, bạn cũng có thể tạo một bảng cố định với các chỉ mục, nơi bạn lưu trữ các tham số cùng với SESSIONID.
Cảnh sát SQL

27

Trong SQL Server 2016+bạn có thể sử dụng STRING_SPLITchức năng:

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

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

hoặc là:

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;

Bản thử trực tiếp

Các câu trả lời được chấp nhận ý chí của công việc khóa học và nó là một trong những con đường để đi, nhưng nó là chống mẫu.

E. Tìm hàng theo danh sách các giá trị

Đây là sự thay thế cho các kiểu chống phổ biến như tạo một chuỗi SQL động trong lớp ứng dụng hoặc Transact-SQL hoặc bằng cách sử dụng toán tử LIKE:

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

Phụ lục :

Để cải thiện STRING_SPLITước lượng hàng của hàm bảng, một ý tưởng tốt là cụ thể hóa các giá trị được chia thành biến bảng / bảng tạm thời:

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 - Demo trực tiếp

Liên quan: Cách chuyển danh sách các giá trị vào thủ tục lưu trữ


Câu hỏi ban đầu có yêu cầu SQL Server 2008. Vì câu hỏi này thường được sử dụng dưới dạng trùng lặp, tôi đã thêm câu trả lời này làm tài liệu tham khảo.


1
Tôi chưa thử nghiệm hoàn hảo điều này, nhưng tôi cảm thấy đây là giải pháp 2016+ sạch nhất. Tôi vẫn muốn có thể vượt qua một mảng int, nhưng cho đến lúc đó ...
Daniel

24

Chúng tôi có chức năng tạo một biến bảng mà bạn có thể tham gia:

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 

Vì thế:

@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

20

Đây là tổng, nhưng nếu bạn được đảm bảo có ít nhất một, bạn có thể làm:

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

Có IN ('tag1', 'tag2', 'tag1', 'tag1', 'tag1') sẽ dễ dàng được SQL Server tối ưu hóa. Thêm vào đó, bạn có được chỉ số tìm kiếm trực tiếp


1
Các tham số tùy chọn với Null kiểm tra hiệu suất làm hỏng, vì trình tối ưu hóa yêu cầu số lượng tham số được sử dụng để tạo các truy vấn hiệu quả. Một truy vấn cho 5 tham số có thể cần một kế hoạch truy vấn khác với một tham số cho 500 tham số.
Erik Hart

18

Theo tôi, nguồn tốt nhất để giải quyết vấn đề này, là những gì đã được đăng trên trang web này:

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

Sử dụng:

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

TÍN DỤNG CHO: Dinakar Nethi


Câu trả lời tuyệt vời, sạch sẽ và mô-đun, thực thi siêu nhanh ngoại trừ phân tích cú pháp CSV ban đầu vào một bảng (một lần, số lượng phần tử nhỏ). Mặc dù có thể sử dụng charindex () đơn giản / nhanh hơn thay vì patindex ()? Charindex () cũng cho phép đối số 'start_location' có thể tránh được việc cắt chuỗi đầu vào mỗi lần lặp? Để trả lời câu hỏi ban đầu chỉ có thể tham gia với kết quả chức năng.
crokusek

18

Tôi sẽ chuyển một tham số loại bảng (vì đó là SQL Server 2008 ) và thực hiện where existshoặc tham gia bên trong. Bạn cũng có thể sử dụng XML, sử dụng sp_xml_preparedocumentvà thậm chí lập chỉ mục bảng tạm thời đó.


Câu trả lời của Ph.E có một bảng tạm thời xây dựng ví dụ (từ csv).
crokusek

12

Cách thích hợp IMHO là lưu trữ danh sách trong một chuỗi ký tự (giới hạn về độ dài bởi những gì DBMS hỗ trợ); mẹo duy nhất là (để đơn giản hóa việc xử lý) tôi có một dấu phân cách (dấu phẩy trong ví dụ của tôi) ở đầu và cuối chuỗi. Ý tưởng là "bình thường hóa nhanh chóng", biến danh sách thành bảng một cột chứa một hàng cho mỗi giá trị. Điều này cho phép bạn biến

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

vào một

trong (chọn ...)

hoặc (giải pháp tôi có thể thích) tham gia thường xuyên, nếu bạn chỉ cần thêm một "khác biệt" để tránh các vấn đề với các giá trị trùng lặp trong danh sách.

Thật không may, các kỹ thuật để cắt một chuỗi khá cụ thể theo sản phẩm. Đây là phiên bản 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;

Phiên bản 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, ',', ''))));

và phiên bản 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, ',', ''));

(Tất nhiên, "trục" phải trả lại nhiều hàng bằng số lượng mục tối đa chúng ta có thể tìm thấy trong danh sách)


11

Nếu bạn đã có SQL Server 2008 trở lên, tôi sẽ sử dụng Tham số có giá trị bảng .

Nếu bạn không may bị mắc kẹt trên SQL Server 2005, bạn có thể thêm chức năng CLR như thế này,

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

Mà bạn có thể sử dụng như thế này,

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

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

10

Tôi nghĩ rằng đây là một trường hợp khi một truy vấn tĩnh không phải là cách để đi. Tự động xây dựng danh sách cho mệnh đề trong của bạn, thoát các trích dẫn đơn của bạn và tự động xây dựng SQL. Trong trường hợp này, bạn có thể sẽ không thấy nhiều sự khác biệt với bất kỳ phương thức nào do danh sách nhỏ, nhưng phương pháp hiệu quả nhất thực sự là gửi SQL chính xác như được viết trong bài đăng của bạn. Tôi nghĩ rằng đó là một thói quen tốt để viết nó theo cách hiệu quả nhất, thay vì làm những gì tạo ra mã đẹp nhất, hoặc coi đó là cách thực hành tồi để xây dựng SQL một cách linh hoạt.

Tôi đã thấy các hàm phân tách mất nhiều thời gian để thực thi hơn chính truy vấn trong nhiều trường hợp các tham số trở nên lớn. Một thủ tục được lưu trữ với các tham số có giá trị bảng trong SQL 2008 là lựa chọn duy nhất tôi sẽ xem xét, mặc dù điều này có thể sẽ chậm hơn trong trường hợp của bạn. TVP có thể sẽ chỉ nhanh hơn cho các danh sách lớn nếu bạn đang tìm kiếm trên khóa chính của TVP, vì dù sao SQL cũng sẽ xây dựng một bảng tạm thời cho danh sách (nếu danh sách lớn). Bạn sẽ không biết chắc chắn trừ khi bạn kiểm tra nó.

Tôi cũng đã thấy các thủ tục được lưu trữ có 500 tham số với giá trị mặc định là null và có WHERE Cột1 IN (@ Param1, @ Param2, @ Param3, ..., @ Param500). Điều này khiến SQL xây dựng bảng tạm thời, thực hiện sắp xếp / phân biệt và sau đó thực hiện quét bảng thay vì tìm kiếm chỉ mục. Đó thực chất là những gì bạn sẽ làm bằng cách tham số hóa truy vấn đó, mặc dù ở quy mô đủ nhỏ để nó không tạo ra sự khác biệt đáng chú ý. Tôi thực sự khuyên bạn không nên có NULL trong danh sách IN của mình, vì nếu điều đó được thay đổi thành KHÔNG IN thì nó sẽ không hoạt động như dự định. Bạn có thể tự động xây dựng danh sách tham số, nhưng điều rõ ràng duy nhất bạn sẽ đạt được là các đối tượng sẽ thoát khỏi dấu ngoặc đơn cho bạn. Cách tiếp cận đó cũng chậm hơn một chút ở cuối ứng dụng vì các đối tượng phải phân tích truy vấn để tìm các tham số.

Việc sử dụng lại các kế hoạch thực hiện cho các thủ tục được lưu trữ hoặc các truy vấn được tham số hóa có thể mang lại cho bạn hiệu suất, nhưng nó sẽ khóa bạn vào một kế hoạch thực hiện được xác định bởi truy vấn đầu tiên được thực hiện. Điều đó có thể ít hơn lý tưởng cho các truy vấn tiếp theo trong nhiều trường hợp. Trong trường hợp của bạn, việc sử dụng lại các kế hoạch thực hiện có thể sẽ là một điểm cộng, nhưng nó có thể không tạo ra bất kỳ sự khác biệt nào vì ví dụ này là một truy vấn thực sự đơn giản.

Vách đá ghi chú:

Đối với trường hợp của bạn, bất cứ điều gì bạn làm, có thể là tham số hóa với một số mục cố định trong danh sách (null nếu không được sử dụng), tự động xây dựng truy vấn có hoặc không có tham số hoặc sử dụng quy trình được lưu trữ với tham số có giá trị bảng sẽ không có nhiều khác biệt . Tuy nhiên, các khuyến nghị chung của tôi như sau:

Trường hợp của bạn / truy vấn đơn giản với một vài tham số:

SQL động, có thể với các tham số nếu kiểm tra cho thấy hiệu suất tốt hơn.

Các truy vấn với các kế hoạch thực hiện có thể sử dụng lại, được gọi nhiều lần bằng cách thay đổi các tham số hoặc nếu truy vấn phức tạp:

SQL với các tham số động.

Truy vấn với danh sách lớn:

Thủ tục lưu trữ với các tham số có giá trị bảng. Nếu danh sách có thể thay đổi theo số lượng lớn, hãy sử dụng VỚI RECOMPILE trên quy trình được lưu trữ hoặc chỉ đơn giản sử dụng SQL động không có tham số để tạo kế hoạch thực hiện mới cho mỗi truy vấn.


Bạn có ý nghĩa gì bởi "thủ tục lưu trữ" ở đây? Bạn có thể gửi một ví dụ?
struhtanov

9

Có thể chúng ta có thể sử dụng XML ở đây:

    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)

1
CTE@xcó thể được loại bỏ / nội tuyến vào phần phụ, nếu được thực hiện rất cẩn thận, như thể hiện trong bài viết này .
robert4

9

Tôi sẽ tiếp cận điều này theo mặc định bằng cách chuyển một hàm có giá trị bảng (trả về một bảng từ một chuỗi) đến điều kiện IN.

Đây là mã cho UDF (Tôi đã nhận được nó từ Stack Overflow ở đâu đó, tôi không thể tìm thấy nguồn ngay bây giờ)

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
  )

Khi bạn đã nhận được mã này, mã của bạn sẽ đơn giản như thế này:

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

Trừ khi bạn có một chuỗi dài kỳ cục, điều này sẽ hoạt động tốt với chỉ mục bảng.

Nếu cần, bạn có thể chèn nó vào bảng tạm thời, lập chỉ mục cho nó, sau đó chạy tham gia ...


8

Một giải pháp khả thi khác là thay vì chuyển một số lượng đối số khác nhau cho một thủ tục được lưu trữ, hãy chuyển một chuỗi chứa tên bạn theo sau, nhưng làm cho chúng trở nên duy nhất bằng cách bao quanh chúng bằng '<>'. Sau đó sử dụng PATINDEX để tìm tên:

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

8

Sử dụng các thủ tục được lưu trữ sau đây. Nó sử dụng một chức năng phân chia tùy chỉnh, có thể được tìm thấy ở đây .

 create stored procedure GetSearchMachingTagNames 
    @PipeDelimitedTagNames varchar(max), 
    @delimiter char(1) 
    as  
    begin
         select * from Tags 
         where Name in (select data from [dbo].[Split](@PipeDelimitedTagNames,@delimiter) 
    end

8

Nếu chúng ta có các chuỗi được lưu trữ bên trong mệnh đề IN với dấu phẩy (,) được phân cách, chúng ta có thể sử dụng hàm charindex để lấy các giá trị. Nếu bạn sử dụng .NET, thì bạn có thể ánh xạ với SqlParameter.

Tập lệnh 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

Bạn có thể sử dụng câu lệnh trên trong mã .NET của mình và ánh xạ tham số với SqlParameter.

Bản demo Fiddler

BIÊN TẬP: Tạo bảng có tên là ChọnTags bằng cách sử dụng tập lệnh sau.

Tập lệnh 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

Bạn có thể đưa ra một ví dụ về hoạt động này khi không có danh sách mã hóa cứng của các giá trị có thể?
John Saunders

@JohnSaunders, tôi đã chỉnh sửa tập lệnh mà không sử dụng bất kỳ danh sách mã hóa cứng nào. Xin vui lòng xác minh.
Gowdhaman008

3
Một hạn chế với tùy chọn này. Char Index trả về 1 nếu chuỗi được tìm thấy. IN trả về một trận đấu cho một điều khoản chính xác. Char Index cho "Stack" sẽ trả về 1 cho cụm từ "StackOverflow" IN sẽ không. Có một phần nhỏ cho câu trả lời này bằng cách sử dụng Pat Index ở trên, bao gồm các tên có '<'% name% '>' vượt qua giới hạn này. Giải pháp sáng tạo cho vấn đề này mặc dù.
Richard Vivian

7

Đối với một số lượng đối số như thế này, cách duy nhất tôi biết là tạo SQL một cách rõ ràng hoặc làm một cái gì đó liên quan đến việc tạo một bảng tạm thời với các mục bạn muốn và tham gia vào bảng tạm thời.


7

Trong ColdFusion chúng ta chỉ cần làm:

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

7

Đây là một kỹ thuật tạo lại một bảng cục bộ sẽ được sử dụng trong chuỗi truy vấn. Làm theo cách này giúp loại bỏ tất cả các vấn đề phân tích cú pháp.

Chuỗi có thể được xây dựng trong bất kỳ ngôn ngữ. Trong ví dụ này tôi đã sử dụng SQL vì đó là vấn đề ban đầu tôi đang cố gắng giải quyết. Tôi cần một cách rõ ràng để truyền dữ liệu bảng một cách nhanh chóng để thực hiện sau.

Sử dụng một loại người dùng xác định là tùy chọn. Tạo kiểu chỉ được tạo một lần và có thể được thực hiện trước thời hạn. Nếu không, chỉ cần thêm một loại bảng đầy đủ để khai báo trong chuỗi.

Mẫu chung dễ mở rộng và có thể được sử dụng để vượt qua các bảng phức tạp hơn.

-- Create a user defined type for the list.
CREATE TYPE [dbo].[StringList] AS TABLE(
    [StringValue] [nvarchar](max) NOT NULL
)

-- Create a sample list using the list table type.
DECLARE @list [dbo].[StringList]; 
INSERT INTO @list VALUES ('one'), ('two'), ('three'), ('four')

-- Build a string in which we recreate the list so we can pass it to exec
-- This can be done in any language since we're just building a string.
DECLARE @str nvarchar(max);
SET @str = 'DECLARE @list [dbo].[StringList]; INSERT INTO @list VALUES '

-- Add all the values we want to the string. This would be a loop in C++.
SELECT @str = @str + '(''' + StringValue + '''),' FROM @list

-- Remove the trailing comma so the query is valid sql.
SET @str = substring(@str, 1, len(@str)-1)

-- Add a select to test the string.
SET @str = @str + '; SELECT * FROM @list;'

-- Execute the string and see we've pass the table correctly.
EXEC(@str)

7

Trong SQL Server 2016+ khả năng khác là sử dụng OPENJSONhàm.

Cách tiếp cận này được viết về OPENJSON - một trong những cách tốt nhất để chọn hàng theo danh sách id .

Một ví dụ đầy đủ làm việc dưới đây

CREATE TABLE dbo.Tags
  (
     Name  VARCHAR(50),
     Count INT
  )

INSERT INTO dbo.Tags
VALUES      ('VB',982), ('ruby',1306), ('rails',1478), ('scruffy',1), ('C#',1784)

GO

CREATE PROC dbo.SomeProc
@Tags VARCHAR(MAX)
AS
SELECT T.*
FROM   dbo.Tags T
WHERE  T.Name IN (SELECT J.Value COLLATE Latin1_General_CI_AS
                  FROM   OPENJSON(CONCAT('[', @Tags, ']')) J)
ORDER  BY T.Count DESC

GO

EXEC dbo.SomeProc @Tags = '"ruby","rails","scruffy","rubyonrails"'

DROP TABLE dbo.Tags 

7

Đây là một thay thế khác. Chỉ cần chuyển một danh sách được phân cách bằng dấu phẩy dưới dạng tham số chuỗi cho thủ tục được lưu trữ và:

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

Và chức năng:

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

6

Tôi có câu trả lời không yêu cầu UDF, XML Vì IN chấp nhận câu lệnh chọn, ví dụ: CHỌN * TỪ Kiểm tra trong đó Dữ liệu IN (CHỌN Giá trị TABLE TABLE)

Bạn thực sự chỉ cần một cách để chuyển đổi chuỗi thành bảng.

Điều này có thể được thực hiện với CTE đệ quy hoặc truy vấn với bảng số (hoặc Master..spt_value)

Đây là phiên bản CTE.

DECLARE @InputString varchar(8000) = 'ruby,rails,scruffy,rubyonrails'

SELECT @InputString = @InputString + ','

;WITH RecursiveCSV(x,y) 
AS 
(
    SELECT 
        x = SUBSTRING(@InputString,0,CHARINDEX(',',@InputString,0)),
        y = SUBSTRING(@InputString,CHARINDEX(',',@InputString,0)+1,LEN(@InputString))
    UNION ALL
    SELECT 
        x = SUBSTRING(y,0,CHARINDEX(',',y,0)),
        y = SUBSTRING(y,CHARINDEX(',',y,0)+1,LEN(y))
    FROM 
        RecursiveCSV 
    WHERE
        SUBSTRING(y,CHARINDEX(',',y,0)+1,LEN(y)) <> '' OR 
        SUBSTRING(y,0,CHARINDEX(',',y,0)) <> ''
)
SELECT
    * 
FROM 
    Tags
WHERE 
    Name IN (select x FROM RecursiveCSV)
OPTION (MAXRECURSION 32767);

6

Tôi sử dụng một phiên bản ngắn gọn hơn của câu trả lời được bình chọn hàng đầu :

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

Nó lặp qua các tham số thẻ hai lần; nhưng điều đó không quan trọng trong hầu hết thời gian (nó sẽ không phải là nút cổ chai của bạn; nếu có, hãy bỏ kiểm soát vòng lặp).

Nếu bạn thực sự quan tâm đến hiệu suất và không muốn lặp lại vòng lặp hai lần, thì đây là một phiên bản kém đẹp hơn:

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

5

Đây là một câu trả lời khác cho vấn đề này.

(phiên bản mới đăng ngày 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;
    }

Chúc mừng.


4

Động thái chiến thắng duy nhất là không chơi.

Không có sự thay đổi vô hạn cho bạn. Chỉ biến thiên hữu hạn.

Trong SQL, bạn có một mệnh đề như thế này:

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

Trong mã C #, bạn làm một cái gì đó như thế này:

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

Vì vậy, về cơ bản nếu số đếm bằng 0 thì không có bộ lọc và mọi thứ đều đi qua. Nếu số đếm cao hơn 0 thì giá trị phải nằm trong danh sách, nhưng danh sách đã được đệm thành năm với các giá trị không thể (để SQL vẫn có ý nghĩa)

Đôi khi giải pháp khập khiễng là giải pháp duy nhất thực sự hoạt động.

Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.