Parameterize an SQL IN clause

How do I parameterize a query containing an IN clause with a variable number of arguments, like this one?

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

In this query, the number of arguments could be anywhere from 1 to 5.

I would prefer not to use a dedicated stored procedure for this (or XML), but if there is some elegant way specific to SQL Server 2008, I am open to that.


Here's a quick-and-dirty technique I have used:

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

So here's the C# code:

string[] tags = new string[] { "ruby", "rails", "scruffy", "rubyonrails" };
const string cmdText = "select * from tags where '|' + @tags + '|' like '%|' + Name + '|%'";

using (SqlCommand cmd = new SqlCommand(cmdText)) {
   cmd.Parameters.AddWithValue("@tags", string.Join("|", tags);
}

Two caveats:

  • The performance is terrible. LIKE "%...%" queries are not indexed.
  • Make sure you don't have any | , blank, or null tags or this won't work
  • There are other ways to accomplish this that some people may consider cleaner, so please keep reading.


    You can parameterize each value, so something like:

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

    Which will give you:

    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"
    

    No, this is not open to SQL injection. The only injected text into CommandText is not based on user input. It's solely based on the hardcoded "@tag" prefix, and the index of an array. The index will always be an integer, is not user generated, and is safe.

    The user inputted values are still stuffed into parameters, so there is no vulnerability there.

    Edit:

    Injection concerns aside, take care to note that constructing the command text to accomodate a variable number of parameters (as above) impede's SQL server's ability to take advantage of cached queries. The net result is that you almost certainly lose the value of using parameters in the first place (as opposed to merely inserting the predicate strings into the SQL itself).

    Not that cached query plans aren't valuable, but IMO this query isn't nearly complicated enough to see much benefit from it. While the compilation costs may approach (or even exceed) the execution costs, you're still talking milliseconds.

    If you have enough RAM, I'd expect SQL Server would probably cache a plan for the common counts of parameters as well. I suppose you could always add five parameters, and let the unspecified tags be NULL - the query plan should be the same, but it seems pretty ugly to me and I'm not sure that it'd worth the micro-optimization (although, on Stack Overflow - it may very well be worth it).

    Also, SQL Server 7 and later will auto-parameterize queries, so using parameters isn't really necessary from a performance standpoint - it is, however, critical from a security standpoint - especially with user inputted data like this.


    For SQL Server 2008, you can use a table valued parameter. It's a bit of work, but it is arguably cleaner than my other method.

    First, you have to create a type

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

    Then, your ADO.NET code looks like this:

    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;
        });
    }
    
    链接地址: http://www.djcxy.com/p/16780.html

    上一篇: 如何使用PHP / SQL构建树视图?

    下一篇: 参数化SQL IN子句