There are several ways of doing it. Changing the data model may also be a better option if you can do it.
1. Comma separated parameters
If you want to stick with the comma separated string, you can find many string split functions online using CLR, CTE, XML, ... I am not going to put them all here and I won't benchmark them. But you must know that they all have their own issues. For more information you can look at this post from Aaron Bertrand: Split strings the right way – or the next best way
Sample query (using XML conversion):
DECLARE @c1 nvarchar(100) = N'10,15,13,14';
DECLARE @delimiter nvarchar(1) = N',';
SELECT v1 = LTRIM(RTRIM(vals.node.value('(./text())[1]', 'nvarchar(4000)')))
FROM (
SELECT x = CAST('<root><data>' + REPLACE(@c1, @delimiter, '</data><data>') + '</data></root>' AS XML).query('.')
) v
CROSS APPLY x.nodes('/root/data') vals(node);
Output:
v1
10
15
13
14
Main query:
The idea is to convert it to a table. It can then be used in a correlated sub query:
DECLARE @c1 nvarchar(100) = N'10,15,13,14';
DECLARE @c2 nvarchar(100) = N'C21, B21';
DECLARE @delimiter nvarchar(1) = N',';
SELECT *
FROM FinalStatus f
WHERE EXISTS (
SELECT 1
FROM (
SELECT CAST('<root><data>' + REPLACE(@c1, @delimiter, '</data><data>') + '</data></root>' AS XML) AS x
)t
CROSS APPLY x.nodes('/root/data') vals(node)
WHERE LTRIM(RTRIM(vals.node.value('.[1]', 'nvarchar(4000)'))) = f.Col1
)
OR EXISTS (
SELECT 1
FROM (
SELECT CAST('<root><data>' + REPLACE(@c2, @delimiter, '</data><data>') + '</data></root>' AS XML) AS x
)t
CROSS APPLY x.nodes('/root/data') vals(node)
WHERE LTRIM(RTRIM(vals.node.value('.[1]', 'nvarchar(4000)'))) = f.Col2
);
Only the WHERE
clause has to be updated in your procedure. Parameters remains of varchar type with comma separated values.
Output:
ID Col1 Col2 Col3 LastUpdatedDate
1 10 ABC21 Msg1 2016-07-20 09:06:19.380 => match c1
2 10 ABC21 Msg2 2016-07-20 09:06:19.390 => match c1
3 11 C21 Some Msg1 2016-07-20 09:06:19.390 => match c2
2. XML parameter(s)
If you can change the parameter' types, the XML data type can be used. It can be easily deserialized by the procedure and the query.
This query is pretty similar to the previous one. However it gives a better control over invalid values and special characters since there is no conversion or search and replace.
DECLARE @c xml = N'
<root>
<c1>10</c1><c1>15</c1><c1>13</c1><c1>14</c1>
<c2>C21</c2><c2>B21</c2>
</root>';
DECLARE @delimiter nvarchar(1) = N',';
SELECT *
FROM FinalStatus f
WHERE EXISTS (
SELECT 1
FROM (
SELECT x = @c.query('.')
) v
CROSS APPLY x.nodes('/root/c1') val1(node)
WHERE LTRIM(RTRIM(val1.node.value('.[1]', 'nvarchar(4000)'))) = f.Col1
)
OR EXISTS (
SELECT 1
FROM (
SELECT x = @c.query('.')
) v
CROSS APPLY x.nodes('/root/c2') val1(node)
WHERE LTRIM(RTRIM(val1.node.value('.[1]', 'nvarchar(4000)'))) = f.Col2
);
The parameter' types have to be changed to xml. I used only one variable with c1
and c2
nodes but 1 variable for each node (c1, c2, ...) and different node names can also be used:
DECLARE @c1 xml = N'<root><data>10</data><data>15</data><data>13</data><data>14</data></root>';
DECLARE @c2 xml = N'<root><data>C21</data><data>B21</data></root>';
DECLARE @c3 xml = N'...';
The correct path and variables must be updated in the nodes
part of the CROSS APPLY
.
Using .Net, Powershell or other languages, an array or other types can easily be converted to xml and used as a parameter of the procedure.
...
3. User-Defined Table Types
Another option would be to create a Table Value Type that can be used by the Stored Procedure parameters.
Table Type
CREATE TYPE [dbo].[TableTypeCols] AS TABLE
(
[col] varchar(15)
);
Stored Procedure with Table Type parameter(s)
You then update the Stored Procedure using this newly created type:
CREATE OR ALTER PROCEDURE [dbo].[FindResult]
@c1 [dbo].[TableTypeCols] READONLY
, @c2 [dbo].[TableTypeCols] READONLY
AS
SELECT * -- fs.[...], fs.[...], ...
FROM [dbo].[FinalStatus] fs
WHERE
EXISTS (
SELECT 1 FROM @c1 c1 WHERE c1.col = fs.Col1
)
OR EXISTS (
SELECT 1 FROM @c2 c2 WHERE c2.col = fs.Col2
);
GO;
READONLY
is mandatory in the parameter declaration.
Stored Procedure call with Table Type parameter(s)
With SQL, you can call it the same way using a table variable:
DECLARE @tc1 [dbo].[TableTypeCols];
DECLARE @tc2 [dbo].[TableTypeCols];
INSERT INTO @tc1(col) VALUES('10'), ('15'), ('13'), ('14');
INSERT INTO @tc2(col) VALUES('C21'), ('B21');
EXEC dbo.FindResult @c1 = @tc1, @c2 = @tc2;
Output:
ID Col1 Col2 Col3 LastUpdatedDate
1 10 ABC21 Msg1 2016-07-20 09:06:19.380 => match c1
2 10 ABC21 Msg2 2016-07-20 09:06:19.390 => match c1
3 11 C21 Some Msg1 2016-07-20 09:06:19.390 => match c2
Please note that if you are planning on using more than a couple rows for each parameters, you should run some performance test with real data and make sure it works properly on your system.
.Net C# Call
The procedure can the be call from you .Net code
// Create & Fill Parameter 1
DataTable dt1 = new DataTable();
dt1.Columns.Add("col", typeof (string));
DataRow row = dt1.NewRow();
row["col"] = ("10");
dt1.Rows.Add(row);
/*
... add more row ...
*/
// Create & Fill Parameter 2
DataTable dt2 = new DataTable();
dt2.Columns.Add("col", typeof (string));
DataRow row = dt2.NewRow();
row["col"] = ("C21");
dt2.Rows.Add(row);
/*
... add more row ...
*/
// Output dataset
DataSet ds = new DataSet("SQLDatabase");
using (SqlConnection conn = new SqlConnection(...))
{
// Stored Procedure with table parameters
SqlCommand sqlComm = new SqlCommand("dbo.FindResult", conn);
sqlComm.CommandType = CommandType.StoredProcedure;
// Parameter 1
SqlParameter param = new SqlParameter("@c1", SqlDbType.Structured)
{
TypeName = "dbo.TableTypeCols",
Value = dt1
};
sqlComm.Parameters.Add(param);
// Parameter 2
SqlParameter param = new SqlParameter("@c2", SqlDbType.Structured)
{
TypeName = "dbo.TableTypeCols",
Value = dt2
};
sqlComm.Parameters.Add(param);
// Call Stored Procedure
SqlDataAdapter da = new SqlDataAdapter();
da.SelectCommand = sqlComm;
da.Fill(ds);
}
SqlDbType.Structured
is mandatory.
Please note that I am not a .Net expert and there are other or better ways to write and use this code.
4. Json Type Parameters (Since SQL Server 2016)
With minimal changes, you can declare a NVARCHAR(MAX) parameters with json string.
json parameters
DECLARE @C1 NVARCHAR(MAX);
SET @jsonC1 =
N'[
{ "col" : "10"}, { "col" : "15"}, { "col" : "13"}, { "col" : "14"}
]';
DECLARE @C2 NVARCHAR(MAX);
SET @jsonC2 =
N'[
{ "col" : "C21"}, { "col" : "B21"}
]';
Query
SELECT *
FROM dbo.FinalStatus f
WHERE
EXISTS (
SELECT 1 FROM OPENJSON(@C1) WITH(col nvarchar(15)) AS c1 WHERE c1.col = f.Col1
)
OR
EXISTS (
SELECT 1 FROM OPENJSON(@C2) WITH(col nvarchar(15)) AS c2 WHERE c2.col = f.Col2
);
Please note that OPENJSON requires SQL Server 2016.
Best Answer
Personally I would choose to pass a list of id's in as a table parameter to the stored procedure this would then allow you to do a set-based update instead of a row by row one which is less efficient.
I have never personally used the EF but a good artical on performing the above using ADO is below (ignore the fact it says it is for SQL 2008 as it also works on 2005). The same strategy would work better for you in this situation but you may need to adapt the implementation based on the fact you are using the Entity Framework.
http://www.mssqltips.com/sqlservertip/2112/table-value-parameters-in-sql-server-2008-and-net-c/
EDIT
As you rightly pointed out I am wrong about the fact this works on 2005 - sorry about that!
However, I have some alternate suggestions.
As SQL Server 2005 does support table variables (just not as parameters to stored procedures as you pointed out) you could parse the delimited string and insert the id's into a table variable. You could then use the table variable to perform a set-based update.
Alternatively the link below provides a different take on the same problem by persisting the values to a table first thereby avoiding serialization and de-serialization of the id values:
http://weblogs.sqlteam.com/jeffs/archive/2007/06/26/passing-an-array-or-table-parameter-to-a-stored-procedure.aspx
I hope this helps you.