77

Looking to pass a list of User IDs to return a list names. I have a plan to handle the outputed names (with a COALESCE something or other) but trying to find the best way to pass in the list of user IDs. The guts of my sproc will look something like this:

create procedure [dbo].[get_user_names]
@user_id_list, --which would equal a list of incoming ID numbers like (5,44,72,81,126)
@username varchar (30) output
as
select last_name+', '+first_name 
from user_mstr
where user_id in @user_id_list

Passing the values for @user_id_list is my main concern here.

Mureinik
  • 297,002
  • 52
  • 306
  • 350
  • 5
    I wouldn't pass in a list of IDs, I would create a user-defined table type, insert the values into that, and pass that variable to the proc. You could then just do a `JOIN` to the table. – Siyual Feb 24 '17 at 21:29
  • 1
    try looking here: http://stackoverflow.com/questions/7097079/c-sharp-sql-server-passing-a-list-to-a-stored-procedure – Justin C Feb 24 '17 at 21:29
  • i agree with table parameters... secondly i would use a string splitter and as a last resort... dynamic sql. Similar question just posted [here](http://stackoverflow.com/questions/42448333/using-a-string-which-comprises-of-values-in-a-query) – S3S Feb 24 '17 at 21:34
  • 2
    Depending on the programming language and API, you can pass the list in as a table valued parameter (TVP). Other solutions will vary depending on your SQL Server version. See http://www.sommarskog.se/arrays-in-sql.html. – Dan Guzman Feb 24 '17 at 21:38
  • 1
    It depends on how you are calling it. Please edit your question and describe how you are calling this. (from C#? Inside SQL? A reporting tool?). Also please be specific about the version. You can use `string_split` in some versions – Nick.Mc Dec 11 '17 at 05:27

10 Answers10

83

The preferred method for passing an array of values to a stored procedure in SQL server is to use table valued parameters.

First you define the type like this:

CREATE TYPE UserList AS TABLE ( UserID INT );

Then you use that type in the stored procedure:

create procedure [dbo].[get_user_names]
@user_id_list UserList READONLY,
@username varchar (30) output
as
select last_name+', '+first_name 
from user_mstr
where user_id in (SELECT UserID FROM @user_id_list)

So before you call that stored procedure, you fill a table variable:

DECLARE @UL UserList;
INSERT @UL VALUES (5),(44),(72),(81),(126)

And finally call the SP:

EXEC dbo.get_user_names @UL, @username OUTPUT;
Matthew Sontum
  • 1,013
  • 6
  • 8
  • 3
    When I try to run the first line: "CREATE TYPE UserList AS TABLE ( UserID INT );" I get the following error: "Incorrect syntax near the keyword 'AS'. Any suggestions? I'm using SQL Server 2014. – CowboyBebop Apr 02 '19 at 16:48
  • 2
    @CowboyBebop try prefixing UserList with the name of your schema, usually dbo. So the line becomes: CREATE TYPE dbo.UserList AS TABLE ( UserID INT ); – Vlad Tamas Jun 12 '20 at 16:49
  • @CowboyBebop You would have UserList created in the Type Folder. It wouldn't be in the Store Procedure code block. – juanvan Apr 15 '21 at 02:59
  • Why bother with the `TYPE`? If you're going to populate the table like this anyway, why not declare a temp table, populate that, and then pass it? – J. Mini Jun 01 '22 at 14:41
22

Azure DB, Azure Data WH and from SQL Server 2016, you can use STRING_SPLIT to achieve a similar result to what was described by @sparrow.

Recycling code from @sparrow

WHERE user_id IN (SELECT value FROM STRING_SPLIT( @user_id_list, ',')

Simple and effective way of accepting a list of values into a Stored Procedure

Peter Henry
  • 651
  • 5
  • 17
  • 5
    STRING_SPLIT Returns a single-column table whose rows are the substrings. The name of the column is **value**. the correct subquery is `Select value from STRING_SPLIT(@myvar, ',')` (instead of **id**) as https://learn.microsoft.com/en-us/sql/t-sql/functions/string-split-transact-sql?view=sql-server-2017 – Gerardo Grignoli Jul 10 '19 at 12:44
21

As far as I can tell, there are three main contenders: Table-Valued Parameters, delimited list string, and JSON string.

Since 2016, you can use the built-in STRING_SPLIT if you want the delimited route: https://learn.microsoft.com/en-us/sql/t-sql/functions/string-split-transact-sql

That would probably be the easiest/most straightforward/simple approach.

Also since 2016, JSON can be passed as a nvarchar and used with OPENJSON: https://learn.microsoft.com/en-us/sql/t-sql/functions/openjson-transact-sql

That's probably best if you have a more structured data set to pass that may be significantly variable in its schema.

TVPs, it seems, used to be the canonical way to pass more structured parameters, and they are still good if you need that structure, explicitness, and basic value/type checking. They can be a little more cumbersome on the consumer side, though. If you don't have 2016+, this is probably the default/best option.

I think it's a trade off between any of these concrete considerations as well as your preference for being explicit about the structure of your params, meaning even if you have 2016+, you may prefer to explicitly state the type/schema of the parameter rather than pass a string and parse it somehow.

Ambrose Little
  • 2,111
  • 2
  • 15
  • 15
  • 1
    http://groupby.org/conference-session-abstracts/author/amachanic/ STRING_SPLIT is the way to go if you are on 2016 or greater. It clobbers home-grown code in performance. – discosammy Nov 04 '18 at 06:33
  • If you are on 2016 or later, but running in a compatibility mode with an earlier version, you may still not have access to STRING_SPLIT or OPENJSON (unfortunately this was my scenario, despite being on 2019 at this point). For how to check, see: https://learn.microsoft.com/en-us/sql/relational-databases/databases/view-or-change-the-compatibility-level-of-a-database?view=sql-server-ver15 (Compatibility level 130 or greater is required to use either of those methods) – David Thompson Jan 28 '21 at 21:29
  • no sql injection risk with STRING_SPLIT? – JoelFan Oct 26 '21 at 16:10
  • 3
    @JoelFan, no, this is not generating dynamic sql that is evaluated. STRNG_SPLIT results in a single-col table with each split out value as one row. – Ambrose Little Dec 08 '21 at 16:25
7

You can try this:

create procedure [dbo].[get_user_names]
    @user_id_list varchar(2000), -- You can use any max length

    @username varchar (30) output
    as
    select last_name+', '+first_name 
    from user_mstr
    where user_id in (Select ID from dbo.SplitString( @user_id_list, ',') )

And here is the user defined function for SplitString:

Create FUNCTION [dbo].[SplitString]
(    
      @Input NVARCHAR(MAX),
      @Character CHAR(1)
)
RETURNS @Output TABLE (
      Item NVARCHAR(1000)
)
AS
BEGIN
      DECLARE @StartIndex INT, @EndIndex INT

      SET @StartIndex = 1
      IF SUBSTRING(@Input, LEN(@Input) - 1, LEN(@Input)) <> @Character
      BEGIN
            SET @Input = @Input + @Character
      END

      WHILE CHARINDEX(@Character, @Input) > 0
      BEGIN
            SET @EndIndex = CHARINDEX(@Character, @Input)

            INSERT INTO @Output(Item)
            SELECT SUBSTRING(@Input, @StartIndex, @EndIndex - 1)

            SET @Input = SUBSTRING(@Input, @EndIndex + 1, LEN(@Input))
      END

      RETURN
END
Sparrow
  • 2,548
  • 1
  • 24
  • 28
  • `dbo.SplitString` is not internal function. – ahmed abdelqader Feb 24 '17 at 21:34
  • 1
    That's probably the slowest you can get for that type of function. Check here for some other options: [Split strings the right way – or the next best way - Aaron Bertrand](https://sqlperformance.com/2012/07/t-sql-queries/split-strings) – SqlZim Feb 24 '17 at 21:38
7

I solved this problem through the following:

  1. In C # I built a String variable.

string userId="";

  1. I put my list's item in this variable. I separated the ','.

for example: in C#

userId= "5,44,72,81,126";

and Send to SQL-Server

 SqlParameter param = cmd.Parameters.AddWithValue("@user_id_list",userId);
  1. I Create Separated Function in SQL-server For Convert my Received List (that it's type is NVARCHAR(Max)) to Table.
CREATE FUNCTION dbo.SplitInts  
(  
   @List      VARCHAR(MAX),  
   @Delimiter VARCHAR(255)  
)  
RETURNS TABLE  
AS  
  RETURN ( SELECT Item = CONVERT(INT, Item) FROM  
      ( SELECT Item = x.i.value('(./text())[1]', 'varchar(max)')  
        FROM ( SELECT [XML] = CONVERT(XML, '<i>'  
        + REPLACE(@List, @Delimiter, '</i><i>') + '</i>').query('.')  
          ) AS a CROSS APPLY [XML].nodes('i') AS x(i) ) AS y  
      WHERE Item IS NOT NULL  
  );
  1. In the main Store Procedure, using the command below, I use the entry list.

SELECT user_id = Item FROM dbo.SplitInts(@user_id_list, ',');

BehrouzMoslem
  • 9,053
  • 3
  • 27
  • 34
2

Check the below code this work for me

@ManifestNoList      VARCHAR(MAX)


WHERE
    (
        ManifestNo IN (SELECT value FROM dbo.SplitString(@ManifestNoList, ','))
    )
Chamath Jeevan
  • 5,072
  • 1
  • 24
  • 27
2

this is perfect working for me . this perfect example i hope solved many users problem.

Step 1 Creare reference table in sql like this

 Create TYPE dbo.tblNames
AS TABLE
(
    [Name] nvarchar(max)
);
go

create TYPE dbo.tblNamesWithCols
AS TABLE
(
    [Name] nvarchar(max)
);
go

Step 2 create store procedure with reference table parameters like this

    create proc syTest
    @VarTbleNameList AS dbo.tblNames READONLY,
    @VarTbleNameColsList AS dbo.tblNamesWithCols READONLY,
    @VarWhereQuery nvarchar(max)
    as
    begin
    ......
......  End

**Calling Store Procedure with parameters **

DECLARE @VarTbleList AS dbo.tblNames
INSERT INTO @VarTbleList
VALUES ( 'tblEmployes' )
INSERT INTO @VarTbleList
VALUES ( 'tblDepartments' )
INSERT INTO @VarTbleList
VALUES ( 'tblCities' )

DECLARE @VarTbleColList AS dbo.tblNamesWithCols
INSERT INTO @VarTbleColList
VALUES ( 'tblEmployes.EmployeId as empId;' )
INSERT INTO @VarTbleColList
VALUES ( 'tblEmployes.EmployeName as empName;' )
INSERT INTO @VarTbleColList
VALUES ( 'tblDepartments.DepartmentName as deptName;'  )
INSERT INTO @VarTbleColList
VALUES ( 'tblDepartments.DepartmentId as deptId;' )

EXECUTE  syTest @VarTbleList , @VarTbleColList , @VarWhereQuery ='test'
1

You can use this simple 'inline' method to construct a string_list_type parameter (works in SQL Server 2014):

declare @p1 dbo.string_list_type
insert into @p1 values(N'myFirstString')
insert into @p1 values(N'mySecondString')

Example use when executing a stored proc:

exec MyStoredProc @MyParam=@p1
Chris Halcrow
  • 28,994
  • 18
  • 176
  • 206
1

The proper way is to create a user defined data type:

CREATE TYPE [dbo].[IntArray] AS TABLE
(
    [ID] [INT] NULL
)

Then you can use this custom data type:

CREATE OR ALTER PROCEDURE [dbo].[sp_GetUserNames]
(
    @userIds [IntArray] READONLY
)
AS
BEGIN
    SET NOCOUNT ON;

    SELECT 
        "Name" = u.LastName + ', ' + u.FirstName
    FROM dbo.User u
    JOIN @userIds uid ON u.Id = uid.Id;
END

Usage:

@DECLARE @result TABLE
(
    Name NVARCHAR(max)
);

@DECLARE @ids [IntArray] = SELECT x.userId FROM dbo.sometable x;

SET @result = EXECUTE [dbo].[sp_GetUserNames] @userIds = @ids;

SELECT * FROM @result;
MovGP0
  • 7,267
  • 3
  • 49
  • 42
0

Maybe you could use:

select last_name+', '+first_name 
from user_mstr
where ',' + @user_id_list + ',' like '%,' + convert(nvarchar, user_id) + ',%'
Skippy
  • 1,595
  • 1
  • 9
  • 13