Wednesday, February 28, 2007

Passing a Variable to an IN List

Every once in a while there is a need to do something like this:

SELECT person_id, person_name

FROM MyUsers

WHERE person_id IN (@search_list);


And @search_list contains some form of a delimited list. However, this is not a supported syntax and will fail. Here is one solution to this problem:

-- Create the test table

CREATE TABLE MyUsers (

 person_id INT PRIMARY KEY,

 person_name VARCHAR(35));

 

-- Insert sample data

INSERT INTO MyUsers VALUES (1327, 'Joe');

INSERT INTO MyUsers VALUES (1342, 'John F.');

INSERT INTO MyUsers VALUES (1411, 'Mary');

INSERT INTO MyUsers VALUES (1345, 'Nancy');

INSERT INTO MyUsers VALUES (1366, 'Greg');

INSERT INTO MyUsers VALUES (1367, 'Jeff');

INSERT INTO MyUsers VALUES (1368, 'Chris');

INSERT INTO MyUsers VALUES (1369, 'John M.');

INSERT INTO MyUsers VALUES (1370, 'Peggy');

INSERT INTO MyUsers VALUES (1371, 'Samuel');

INSERT INTO MyUsers VALUES (1372, 'Tony');

INSERT INTO MyUsers VALUES (1373, 'Lisa');

INSERT INTO MyUsers VALUES (1374, 'Tom');

INSERT INTO MyUsers VALUES (1375, 'Dave');

INSERT INTO MyUsers VALUES (1376, 'Peter');

INSERT INTO MyUsers VALUES (1377, 'Jason');

INSERT INTO MyUsers VALUES (1378, 'Justin');

INSERT INTO MyUsers VALUES (1379, 'Oscar');

 

DECLARE @search_list VARCHAR(100);

 

DECLARE @delimiter CHAR(1);

 

SELECT @search_list = '1327,1342,1411',

       @delimiter = ',';

 

-- Get the users based on the delimited variable list

SELECT person_id, person_name

FROM MyUsers

WHERE person_id IN

    (SELECT SUBSTRING(string, 2, CHARINDEX(@delimiter, string, 2) - 2)

      FROM (SELECT SUBSTRING(list, n, LEN(list))

            FROM (SELECT @delimiter + @search_list + @delimiter) AS L(list),

                (SELECT ROW_NUMBER() OVER (ORDER BY person_id)

                  FROM MyUsers) AS Nums(n)

            WHERE n <= LEN(list)) AS D(string)

      WHERE LEN(string) > 1

        AND SUBSTRING(string, 1, 1) = @delimiter)

ORDER BY person_id;


-- Results

person_id person_name
-------- ------------------------------
1327 Joe
1342 John F.
1411 Mary


Notes:

- This algorithm is based on walking the delimited string (could be in a table column or a variable) and parsing it into a table format that can feed the IN list.

- The use of ROW_NUMBER assumes you are using SQL Server 2005. However, you can accomplish the same if you have SQL Server 2000. What this subquery does (SELECT ROW_NUMBER() OVER (ORDER BY person_id) FROM MyUsers) is simply generating a table with numbers. In SQL Server 2000 you can create a temp table and generate sequential numbers. The only requirement is that you have more numbers than the length of the string to parse (in this case the length of '1327,1342,1411'). These numbers are used as an index in the walk process.

5 comments:

Phil said...

This probably is not as efficient but

select * from myUsers
where
@delimeter + @searchlist + @delimeter LIKE ('%' + @delimeter + CAST(person_id AS VarChar) + @delimeter + '%')

Mohammad said...

Hi Plamen,
In this case is much easier to use dynamic SQL instead of splitting/parsing approach.
Dynamic SQL like this:

DECLARE @search_list VARCHAR(100),
@SQLcommand VARCHAR(MAX);

SELECT @search_list = '1327,1342,1411',
@SQLcommand = 'SELECT person_id, person_name
FROM MyUsers
WHERE person_id IN ('+ @search_list +');';
EXEC (@SQLcommand);

Also I have created a very simple set-based technique for splitting a string with number table, see:

CREATE FUNCTION dbo.splitter (@S VARCHAR(MAX), @D CHAR(1)) RETURNS TABLE AS
RETURN (SELECT CASE WHEN CHARINDEX(@D, @S + @D, nbr) - nbr = 0 THEN ''
ELSE SUBSTRING(@S, nbr, CHARINDEX(@D, @S + @D, nbr) - nbr)
END AS Word, nbr
FROM Nums
WHERE nbr <= LEN(@S)
AND SUBSTRING(@D + @S, nbr, 1) =@D
);

Plamen Ratchev said...

Hi Mohammad,

The dynamic SQL solution has many issues, please read Erland Sommarskog's article on the topic:
http://www.sommarskog.se/arrays-in-sql-2000.html#dynamic-sql

Mohammad said...

Okay,
I have read some of this article; also the pattern matching approach is simple and short by seems it not efficient. I am right?

DECLARE @search_list VARCHAR(100) ='1327,1342,1411'

SELECT person_id, person_name
FROM MyUsers
WHERE ',' + @search_list + ',' LIKE '%,' + CAST(person_id AS VARCHAR)+ ',%'
ORDER BY person_id;

Plamen Ratchev said...

Correct, the pattern match is inefficient and also unreliable (depends on text format).