views:

81

answers:

5

I am in middle of upgrading from a poorly designed legacy database to a new database. In the old database there is tableA with fields Id and Commodities. Id is the primary key and contains an int and Commodities contains a comma delimited list.

TableA:

id   | commodities
1135 | fish,eggs,meat    
1127 | flour,oil  

In the new database, I want tableB to be in the form id, commodity where each commodity is a single item from the comma delimited list in tableA.

TableB:

id   | commodity
1135 | fish  
1135 | eggs   
1135 | meat  
1127 | flour  
1127 | oil    

I have a function, functionA, that when given an id, a list, and a delimiter, returns a table with an id and item field. How can I use this function to turn the two fields from tableA into tableB?

(Note: I had trouble figuring out what to title this question. Please feel free to edit the title to make it more accurately reflect the question!)

Here is the function code:

ALTER  FUNCTION dbo.functionA
(
@id int,
@List VARCHAR(6000),
@Delim varchar(5)
)
RETURNS
@ParsedList TABLE
(
id int, 
item VARCHAR(6000)
)
AS
BEGIN
DECLARE @item VARCHAR(6000), @Pos INT
SET @List = LTRIM(RTRIM(@List))+ @Delim
SET @Pos = CHARINDEX(@Delim, @List, 1)
WHILE @Pos > 0
BEGIN
SET @item = LTRIM(RTRIM(LEFT(@List, @Pos - 1)))
IF @item <> ''
BEGIN
INSERT INTO @ParsedList (id, item)
VALUES (@id, CAST(@item AS VARCHAR(6000)))
END
SET @List = RIGHT(@List, LEN(@List) - @Pos)
SET @Pos = CHARINDEX(@Delim, @List, 1)
END
RETURN
END
+2  A: 

You write a SQL batch that loops through table A and inserts into table b the results of your function call.

Conrad Frix
There seems like there should be a set based way to do this... I would hate for the only solution to be a batch... There must be another way...
kralco626
Well according to that article you need a loop or that tally table. So i guess just use the batch... The set based idiology seems to fail here...
kralco626
this can be done without a loop, in a single `INSERT`, see my answer.
KM
+3  A: 

You need a way to split and process the string in TSQL, there are many ways to do this. This article covers the PROs and CONs of just about every method:

Arrays and Lists in SQL Server 2000 and Earlier

You need to create a split function. This is how a split function can be used:

SELECT
    *
    FROM YourTable                               y
    INNER JOIN dbo.yourSplitFunction(@Parameter) s ON y.ID=s.Value

[I prefer the number table approach to split a string in TSQL](Arrays and Lists in SQL Server 2000 and Earlier) but there are numerous ways to split strings in SQL Server, see the previous link, which explains the PROs and CONs of each.

For the Numbers Table method to work, you need to do this one time table setup, which will create a table Numbers that contains rows from 1 to 10,000:

SELECT TOP 10000 IDENTITY(int,1,1) AS Number
    INTO Numbers
    FROM sys.objects s1
    CROSS JOIN sys.objects s2
ALTER TABLE Numbers ADD CONSTRAINT PK_Numbers PRIMARY KEY CLUSTERED (Number)

Once the Numbers table is set up, create this split function:

CREATE FUNCTION inline_split_me (@SplitOn char(1),@param varchar(7998)) RETURNS TABLE AS
   RETURN(SELECT substring(@SplitOn + @param + ',', Number + 1,
                    charindex(@SplitOn, @SplitOn + @param + @SplitOn, Number + 1) - Number - 1)
                 AS Value
          FROM   Numbers
          WHERE  Number <= len(@SplitOn + @param + @SplitOn) - 1
            AND  substring(@SplitOn + @param + @SplitOn, Number, 1) = @SplitOn)

GO 

You can now easily split a CSV string into a table and join on it:

select * from dbo.inline_split_me(';','1;22;333;4444;;') where LEN(Value)>0

OUTPUT:

Value
----------------------
1
22
333
4444

(4 row(s) affected)

to make you new table use this:

--set up tables:
create table TableA (id int, commodities varchar(8000))
INSERT TableA VALUES (1135,'fish,eggs,meat')
INSERT TableA VALUES (1127,'flour,oil')

Create table TableB (id int, commodities varchar(8000))

--populate TableB
INSERT TableB
    (id, commodities)
SELECT
    a.id,c.value
    FROM TableA    a
        CROSS APPLY dbo.inline_split_me(',',a.commodities) c

 --show tableB contents:
select * from TableB

OUTPUT:

id          commodities
----------- -------------
1135        fish
1135        eggs
1135        meat
1127        flour
1127        oil

(5 row(s) affected)

EDIT after Conrad Frix comment about SQL Server 2000 not supporting CROSS APPLY

this will do the same:

INSERT TableB
        (id, commodities)
    SELECT 
        a.id,NullIf(SubString(',' + a.commodities + ',' , number , CharIndex(',' , ',' + a.commodities + ',' , number) - number) , '')
        FROM TableA            a
            INNER JOIN Numbers n ON 1=1
        WHERE SubString(',' + a.commodities + ',' , number - 1, 1) = ',' 
        AND CharIndex(',' , ',' + a.commodities + ',' , number) - number > 0
        AND number <= Len(',' + a.commodities + ',') 

and is based on the code from the link in the answer by @Rup. It basically removes the function call and does the split in the main query (using a similar Numbers table split), so no need for a CROSS APPLY.

KM
CROSS APPLY won't work in 2000
Conrad Frix
@Conrad Frix, I've updated my answer with a single statement `INSERT` that incorporates the Numbers table split, so no `CROSS APPLY` is needed (and certainly no looping), which is based on the code in [the link in the answer by @Rup](http://stackoverflow.com/questions/3214972/sql-break-up-one-row-into-many-normalization/3215431#3215431)
KM
+ 1 for no loops.
Conrad Frix
+2  A: 

Here's the link I posted as a comment:

http://www.sqlteam.com/article/parsing-csv-values-into-multiple-rows

Rup
+1, good link, code works on SQL Server 2000
KM
If you're the author why not Copy/Paste so stackers can see it in case something happens downstream?
Kristopher Ives
+1  A: 

Call me lazy, but I'd pull the combined rows out of the database, split them, then reinsert the split rows. This kind of thing seems kind of unnatural for SQL...

Justin K
+1  A: 

SSIS has a pretty handy Unpivot transform if that's available to you.

Daniel P
SSIS is a 2005 feature.
Conrad Frix