I have a list of products in comma separated fashion and since the item list was replaced with new product items, I am trying to modify this CSV list with new product item list.
Thanks for this question - I've just learned something new. The following code is an adaptation of an article written by Rob Volk on exactly this topic. This is a very clever query! I won't copy all of the content down here. I have adapted it to create the results you're looking for in your example.
CREATE TABLE #nums (n INT)
DECLARE @i INT
SET @i = 1
WHILE @i < 8000
BEGIN
INSERT #nums VALUES(@i)
SET @i = @i + 1
END
CREATE TABLE #tmp (
id INT IDENTITY(1,1) not null,
plist VARCHAR(MAX) null
)
INSERT INTO #tmp
VALUES('10,11,15,17,19'),('22,34,44,25'),('5,6,8,9')
CREATE TABLE #tmpprod (
oldid INT NULL,
newid INT NULL
)
INSERT INTO #tmpprod VALUES(5, 109),(9, 110),(10, 111),(15, 112),(19, 113),(30, 114),(34, 222),(44, 333)
;WITH cte AS (SELECT ID, NULLIF(SUBSTRING(',' + plist + ',' , n , CHARINDEX(',' , ',' + plist + ',' , n) - n) , '') AS prod
FROM #nums, #tmp
WHERE ID <= LEN(',' + plist + ',') AND SUBSTRING(',' + plist + ',' , n - 1, 1) = ','
AND CHARINDEX(',' , ',' + plist + ',' , n) - n > 0)
UPDATE t SET plist = (SELECT CAST(CASE WHEN tp.oldid IS NULL THEN cte.prod ELSE tp.newid END AS VARCHAR) + ','
FROM cte LEFT JOIN #tmpprod tp ON cte.prod = tp.oldid
WHERE cte.id = t.id FOR XML PATH(''))
FROM #tmp t WHERE id = t.id
UPDATE #tmp SET plist = SUBSTRING(plist, 1, LEN(plist) -1)
WHERE LEN(plist) > 0 AND SUBSTRING(plist, LEN(plist), 1) = ','
SELECT * FROM #tmp
DROP TABLE #tmp
DROP TABLE #tmpprod
DROP TABLE #nums
The #nums table is a table of sequential integers, the length of which must be greater than the longest CSV you have in your table. The first 8 lines of the script create this table and populate it. Then I've copied in your code, followed by the meat of this query - the very clever single-query parser, described in more detail in the article pointed to above. The common table expression (WITH cte...) does the parsing, and the update script recompiles the results into CSV and updates #tmp.
Adam Machanic's blog contains this posting of a T-SQL only UDF which can accept T-SQL's wildcards for use in replacement.
http://dataeducation.com/splitting-a-string-of-unlimited-length/
For my own use, I adjusted the varchar sizes to max
. Also note that this UDF performs rather slowly, but if you cannot use the CLR, it may be an option. The minor changes I made to the author's code may limit use of this to SQL Server 2008r2 and later.
CREATE FUNCTION dbo.PatternReplace
(
@InputString VARCHAR(max),
@Pattern VARCHAR(max),
@ReplaceText VARCHAR(max)
)
RETURNS VARCHAR(max)
AS
BEGIN
DECLARE @Result VARCHAR(max) = ''
-- First character in a match
DECLARE @First INT
-- Next character to start search on
DECLARE @Next INT = 1
-- Length of the total string -- 0 if @InputString is NULL
DECLARE @Len INT = COALESCE(LEN(@InputString), 0)
-- End of a pattern
DECLARE @EndPattern INT
WHILE (@Next <= @Len)
BEGIN
SET @First = PATINDEX('%' + @Pattern + '%', SUBSTRING(@InputString, @Next, @Len))
IF COALESCE(@First, 0) = 0 --no match - return
BEGIN
SET @Result = @Result +
CASE --return NULL, just like REPLACE, if inputs are NULL
WHEN @InputString IS NULL
OR @Pattern IS NULL
OR @ReplaceText IS NULL THEN NULL
ELSE SUBSTRING(@InputString, @Next, @Len)
END
BREAK
END
ELSE
BEGIN
-- Concatenate characters before the match to the result
SET @Result = @Result + SUBSTRING(@InputString, @Next, @First - 1)
SET @Next = @Next + @First - 1
SET @EndPattern = 1
-- Find start of end pattern range
WHILE PATINDEX(@Pattern, SUBSTRING(@InputString, @Next, @EndPattern)) = 0
SET @EndPattern = @EndPattern + 1
-- Find end of pattern range
WHILE PATINDEX(@Pattern, SUBSTRING(@InputString, @Next, @EndPattern)) > 0
AND @Len >= (@Next + @EndPattern - 1)
SET @EndPattern = @EndPattern + 1
--Either at the end of the pattern or @Next + @EndPattern = @Len
SET @Result = @Result + @ReplaceText
SET @Next = @Next + @EndPattern - 1
END
END
RETURN(@Result)
END
Convert your comma separated list to XML. Use a numbers table, XQuery and position()
to get the separate ID's with the position they have in the string. Build the comma separated string using the for xml path('')
trick with a left outer join
to #tempprod
and order by position()
.
;with C as
(
select T.id,
N.number as Pos,
X.PList.value('(/i[position()=sql:column("N.Number")])[1]', 'int') as PID
from @tmp as T
cross apply (select cast('<i>'+replace(plist, ',', '</i><i>')+'</i>' as xml)) as X(PList)
inner join master..spt_values as N
on N.number between 1 and X.PList.value('count(/i)', 'int')
where N.type = 'P'
)
select C1.id,
stuff((select ','+cast(coalesce(T.newid, C2.PID) as varchar(10))
from C as C2
left outer join @tmpprod as T
on C2.PID = T.oldid
where C1.id = C2.id
order by C2.Pos
for xml path(''), type).value('.', 'varchar(max)'), 1, 1, '')
from C as C1
group by C1.id
Try on SE-Data
Assuming SQL Server 2005 or better, and assuming order isn't important, then given this split function:
CREATE FUNCTION [dbo].[SplitInts]
(
@List VARCHAR(MAX),
@Delimiter CHAR(1)
)
RETURNS TABLE
AS
RETURN ( SELECT Item FROM ( SELECT Item = x.i.value('(./text())[1]', 'int') 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
);
GO
You can get this result in the following way:
;WITH x AS
(
SELECT id, item, oldid, [newid], rn = ROW_NUMBER() OVER
(PARTITION BY id ORDER BY PATINDEX('%,' + RTRIM(s.Item) + ',%', ',' + t.plist + ','))
FROM #tmp AS t CROSS APPLY dbo.SplitInts(t.plist, ',') AS s
LEFT OUTER JOIN #tmpprod AS p ON p.oldid = s.Item
)
SELECT DISTINCT id, STUFF((SELECT ',' +RTRIM(COALESCE([newid], Item))
FROM x AS x2 WHERE x2.id = x.id
FOR XML PATH(''), TYPE).value('.[1]', 'varchar(max)'), 1, 1, '')
FROM x;
Note that the ROW_NUMBER() / OVER / PARTITION BY / ORDER BY
is only there to try to coerce the optimizer to return the rows in that order. You may observe this behavior today and it can change tomorrow depending on statistics or data changes, optimizer changes (service packs, CUs, upgrade, etc.) or other variables.
Long story short: if you're depending on that order, just send the set back to the client, and have the client construct the comma-delimited list. It's probably where this functionality belongs anyway.