Tek-Tips is the largest IT community on the Internet today!

Members share and learn making Tek-Tips Forums the best source of peer-reviewed technical information on the Internet!

  • Congratulations Chris Miller on being selected by the Tek-Tips community for having the most helpful posts in the forums last week. Way to Go!

Passing a list of values to a Stored Procedure (Part II)

T-SQL Hints and Tips

Passing a list of values to a Stored Procedure (Part II)

by  ESquared  Posted    (Edited  )
The SQL FAQ, [link http://tek-tips.com/faqs.cfm?fid=3979]Passing a list of values to a Stored Procedure[/link] (Part I), has a good discussion of various ways to get the job done.

[green]Update July 2007 - see [link http://www.tek-tips.com/faqs.cfm?fid=6684]Passing a list of values to a Stored Procedure (Part III)[/link] for two more options. It discusses handling extremely long lists and also offers a faster function than the one given below.[/green]

This Part II exists because I wanted to offer an additional method that I believe is more efficient and easier to use, with clearer examples.

Here is a quick rundown of Part I:

[li]Option 1 - Dynamic SQL[/li]
Not so great, as it says. Try to avoid dynamic SQL whenever possible!

[li]Option 2 - Table-valued Function[/li]
A good idea, but in my opinion, the referenced function on MSDN was poorly implemented. It's more complicated than it needs to be in format and coding, the attached article is very long, the instructions for using it are very short, and the comments, well, I'll shut up now. [green][Update: the article is long gone from MSDN.][/green]

[li]Option 3 - Temp Table
A temp table disappears (or can't be 'seen' in the scope of the calling context) the moment the creating procedure exits. So to use this option one must:
[ol][li]Create a temp table,[/li]
[li]run the stored procedure in the same scope,[/li]
[li]use its output,[/li]
[li]then finally drop or truncate the temp table, especially if you want to use the sp again.[/li][/ol]
And that is a little bit awkward for frequent use.[/li]


Passing a list of values to a Stored Procedure (Part II)

[li]Option 4 - A More Efficient Table Function[/li]

The goal is to be able to do something similar to the following:

Code:
SELECT ... WHERE CustomerID IN @List

If you've tried it, you know by now that it doesn't work. If your list is the text string '2,5,7,42' then the above statement is essentially saying

Code:
[blue]SELECT[/blue] [gray]...[/gray] [blue]WHERE[/blue] CustomerID [blue]IN[/blue] [gray]([/gray][red]'2,5,7,42'[/red][gray])[/gray]

[color #008080]--which is not the same thing as[/color]

[blue]SELECT[/blue] [gray]...[/gray] [blue]WHERE[/blue] CustomerID [blue]IN[/blue] [gray]([/gray]2[gray],[/gray]5[gray],[/gray]7[gray],[/gray]42[gray])[/gray]

So the query engine gets to customer 42 and tries to match it to your string. First it has to convert the integer column value to a string, so it does this: 42 to [red]'42'[/red]. Then it compares this string to the string of four customer numbers:
Code:
[red]'42'[/red] = [red]'2,5,7,42'[/red] ? -- No
The row is not included.

I put the code in the colors that Query Analyzer uses so you can quickly see the difference between how it interprets the two statements. The first has the list as a string (red), the second has them as separate number literals (black). The parser would be happy with integer variables separated by commas, but not a string.

So what do you do? Keep reading: I've got a function for you to easily get the job done.

Several other similar UDFs I've seen on the web have done special handling for spaces, trimming whitespace from input strings before parsing, and from output strings after parsing. Some have also ignored a lone trailing delimiter. I prefer to let the function user make such decisions. You of course can add your own modifications to make the function suit your needs, and I've included some ideas at the end on how to do this.

It is named Split in honor of the Visual Basic built-in function which has the same functionality. Usage examples are given after the function.

Code:
CREATE Function Split(
   @InputText Varchar(4000), [color #008080]-- The text to be split into rows[/color]
   @Delimiter Varchar(10)) [color #008080]-- The delimiter that separates tokens.
                           -- Can be multiple characters, or empty[/color]

RETURNS @Array TABLE (
   TokenID Int PRIMARY KEY IDENTITY(1,1), [color #008080]--Comment out this line if
                                          -- you don't want the
                                          -- identity column[/color]
   Value Varchar(4000))

AS

[b][color #008080]-----------------------------------------------------------
-- Function Split                                        --
--    ò Returns a Varchar rowset from a delimited string --
-----------------------------------------------------------[/color][/b]

BEGIN

   DECLARE
      @Pos Int,        [color #008080]-- Start of token or character[/color]
      @End Int,        [color #008080]-- End of token[/color]
      @TextLength Int, [color #008080]-- Length of input text[/color]
      @DelimLength Int [color #008080]-- Length of delimiter[/color]

[color #008080]-- Len ignores trailing spaces, thus the use of DataLength.
-- Note: if you switch to NVarchar input and output, you'll need to divide by 2.[/color]
   SET @TextLength = DataLength(@InputText)

[color #008080]-- Exit function if no text is passed in[/color]
   IF @TextLength = 0 RETURN

   SET @Pos = 1
   SET @DelimLength = DataLength(@Delimiter)

   IF @DelimLength = 0 BEGIN [color #008080]-- Each character in its own row[/color]
      WHILE @Pos <= @TextLength BEGIN
         INSERT @Array (Value) VALUES (SubString(@InputText,@Pos,1))
         SET @Pos = @Pos + 1
      END
   END
   ELSE BEGIN
      [color #008080]-- Tack on delimiter to 'see' the last token[/color]
      SET @InputText = @InputText + @Delimiter
      [color #008080]-- Find the end character of the first token[/color]
      SET @End = CharIndex(@Delimiter, @InputText)
      WHILE @End > 0 BEGIN
         [color #008080]-- End > 0, a delimiter was found: there is a(nother) token[/color]
         INSERT @Array (Value) VALUES (SubString(@InputText, @Pos, @End - @Pos))
         [color #008080]-- Set next search to start after the previous token[/color]
         SET @Pos = @End + @DelimLength
         [color #008080]-- Find the end character of the next token[/color]
         SET @End = CharIndex(@Delimiter, @InputText, @Pos)
      END
   END
   
   RETURN

END

GO

[b][color #008080]-----------------------------------------------------------
-- Usage Example 1: Simple SELECTs                       --
-----------------------------------------------------------[/color][/b]

   SELECT TokenID, Value
      FROM dbo.Split('This function brought to you by Erik E',' ')


   SELECT TokenID, Value
      FROM dbo.Split('abcdefghijklmnopqrstuvwxyz','')


[b][color #008080]-----------------------------------------------------------
-- For the next two examples, assume existence of        --
--    the table 'Customers'                              --
--    ò with primary key field CustomerID and name field --
--      FullName                                         --
--    ò and the variable @CustomerList contains a comma- --
--      separated list of desired Customer IDs.          --
--                                                       --
-- These examples will almost assuredly not run as is!   --
--                                                       --
-----------------------------------------------------------
-- Usage Example 2: JOIN                                 --
-----------------------------------------------------------[/color][/b]

   SELECT CustomerID, FullName
      FROM Customers C
         INNER JOIN dbo.Split(@CustomerList,',') S
         ON C.CustomerID = S.Value  [color #008080]-- Implicit conversion to int[/color]
      ORDER BY S.TokenID


[b][color #008080]-----------------------------------------------------------
-- Usage Example 3: WHERE IN                             --
--    ò The above join syntax *may* offer superior       --
--      performance on very large tables.                --
--    ò But I've also included this WHERE IN syntax so   --
--      you can see how it is done.                      --
--    ò This method does not allow you to order by       --
--      TokenID.                                         --
-----------------------------------------------------------[/color][/b]

   SELECT CustomerID, FullName
      FROM Customers
      WHERE CustomerID IN (
         SELECT Value FROM dbo.Split(@CustomerList,',')
      )
   

[b][color #008080]-----------------------------------------------------------
-- Usage Example 4: JOIN (With Setup)                    --
--    ò If you would like a more detailed example of how --
--      to use this function, here is code that does it  --
--      including setting up a temporary Customers table --
--      full of fake data.                               --
--    ò It uses the same JOIN as example 2 above.        --
-----------------------------------------------------------[/color][/b]
   
   CREATE TABLE #Customers (
      CustomerID Int PRIMARY KEY,
      FullName Varchar(60))
   
   INSERT INTO #Customers
      SELECT 1, 'Joe's Landscaping And Goat Leasing' UNION
      SELECT 2, 'Hereford And Calves' UNION
      SELECT 3, 'Multiversal Pictures' UNION
      SELECT 4, 'Remote Control Peanut Butter Spreaders ''R'' Us' UNION
      SELECT 5, 'Rent-A-Spy' UNION
      SELECT 6, 'Whale Dairy Products, a Limited Liability Corporation'
   
   DECLARE @CustomerList Varchar(20)
   SET @CustomerList = '5,2,4'
   
   SELECT CustomerID, FullName
      FROM #Customers C
         INNER JOIN dbo.Split(@CustomerList,',') S
         ON C.CustomerID = S.Value
      ORDER BY S.TokenID
   
   DROP TABLE #Customers


[b][color #008080]-----------------------------------------------------------
-- Function Modification Ideas                           --
--    ò I'll leave it to you to figure out how to        --
--      incorporate the suggestions into the function.   --
-----------------------------------------------------------[/color][/b]

[color #008080]-- Ignore extra blanks[/color]
[blue]SET @InputText = LTrim(RTrim(@InputText))[/blue]
INSERT @Array (Value) VALUES ([blue]LTrim(RTrim([/blue]SubString(@InputText, @Pos, @End - @Pos)[blue]))[/blue])

[color #008080]-- Ignore a trailing delimiter[/color]
[blue]IF Right(@InputText, @DelimLength) <> @Delimiter[/blue] SET @InputText = @InputText + @Delimiter

[color #008080]-- Do not return empty rows[/color]
[blue]IF @End - @Pos > 0[/blue] INSERT @Array (Value) VALUES (SubString(@InputText, @Pos, @End - @Pos))

[color #008080]-- Return a different data type
-- (See example 2, as conversion can also be implicit)[/color]
CREATE Function Split[blue]Int[/blue](
   Value [blue]int[/blue]
INSERT @Array (Value) VALUES ([blue]Convert(int,[/blue] SubString(@InputText, @Pos, @End - @Pos)[blue])[/blue])

--- Many thanks to [link http://www.tek-tips.com/userinfo.cfm?member=donutman]donutman[/link] for his ideas and comments on this FAQ ---
Register to rate this FAQ  : BAD 1 2 3 4 5 6 7 8 9 10 GOOD
Please Note: 1 is Bad, 10 is Good :-)

Part and Inventory Search

Back
Top