0

I have a table and that table stores the values of the users as:

ID     USERID      Items        
1       54           1,2,3,4,5,6
2       55           1,2,5,6,7,8
3       56           1,2,3,4,5,6
4       57           1,2,3,4,5,6
5       58           1,2,3,4,5,6
6       59           1,2,3,4,5,6
7       54           1,2,3,4,5,6

so i am trying to remove the values 1,2,7 from items column for every userId

i can write a code inphp which can run and loop over and find a item in the column list and remove but that is what is expected, better to get that into doing as sql

i can do basic functions of sql but my knowledge with advanced is not much, how can i remove these items

thinking of as a function where i can use @declare = 'items as comma seperated to be removed' and write some kind of loop which if it finds will remove it

Well Indeed, i forgot to mention, There is another table called users which has users details in it based upon Userid, firstname,lastName so apparently it will loop over the users table andginside that it will loop over this to make corrections

  • Why you implemented the table like that? If you made a M-N relation everything would be simpler. Have you some requirements about implementing your table that way? – Wallkan Sep 02 '16 at 15:58
  • this is a old table which was created long back and it is being used in the system for a while now, so probably it cannot be changed now, but all we can do is modify it –  Sep 02 '16 at 16:01

3 Answers3

2

If you want a more robust solution (for not only 1,2 and 7 but any wished sequences) use this solution:

First, Create a split function. Just copy/paste this (credits to this answer) code:

CREATE FUNCTION Split (@InputString varchar(8000),
@Delimiter varchar(50))

RETURNS @Items TABLE (
  Item varchar(8000)
)

AS
BEGIN
  IF @Delimiter = ' '
  BEGIN
    SET @Delimiter = ','
    SET @InputString = REPLACE(@InputString, ' ', @Delimiter)
  END

  IF (@Delimiter IS NULL
    OR @Delimiter = '')
    SET @Delimiter = ','

  DECLARE @Item varchar(8000)
  DECLARE @ItemList varchar(8000)
  DECLARE @DelimIndex int

  SET @ItemList = @InputString
  SET @DelimIndex = CHARINDEX(@Delimiter, @ItemList, 0)
  WHILE (@DelimIndex != 0)
  BEGIN
    SET @Item = SUBSTRING(@ItemList, 0, @DelimIndex)
    INSERT INTO @Items
      VALUES (@Item)

    -- Set @ItemList = @ItemList minus one less item
    SET @ItemList = SUBSTRING(@ItemList, @DelimIndex + 1, LEN(@ItemList) - @DelimIndex)
    SET @DelimIndex = CHARINDEX(@Delimiter, @ItemList, 0)
  END -- End WHILE

  IF @Item IS NOT NULL -- At least one delimiter was encountered in @InputString
  BEGIN
    SET @Item = @ItemList
    INSERT INTO @Items
      VALUES (@Item)
  END

  -- No delimiters were encountered in @InputString, so just return @InputString
  ELSE
    INSERT INTO @Items
      VALUES (@InputString)

  RETURN

END -- End Function
GO

Edit: As per @Sean Lange comment, please consider a better performance split function as described here.

Second, here is the code:

DECLARE @table TABLE (
  ID int,
  UserID int,
  Items varchar(50)
);

INSERT INTO @table (ID, UserID, Items)
  VALUES (1, 54, '1,2,3,4,5,6'), (2, 55, '1,2,5,6,7,8'), (3, 56, '1,2,3,4,5,6'),
  (4, 57, '1,2,3,4,5,6'), (5, 58, '1,2,3,4,5,6'), (6, 59, '1,2,3,4,5,6'), (7, 54, '1,2,3,4,5,6');

DECLARE @toBeRemoved TABLE (
  num varchar(16)
);
INSERT INTO @toBeRemoved (num)
  VALUES ('1'), ('2'), ('7');

DECLARE @forUserIds TABLE (
  userId int
);
INSERT INTO @forUserIds (userId)
  VALUES (54), (55), (56), (57), (58);

SELECT
  ID,
  UserID,
  Items,
  LEFT([Sub].[NewItems], LEN([Sub].[NewItems]) - 1) AS 'NewItems'
FROM (SELECT DISTINCT
  ST2.*,
  (SELECT
    ST1.Item + ',' AS [text()]
  FROM (SELECT
    *
  FROM @table a
  CROSS APPLY dbo.Split(Items, ',')
  WHERE Item NOT IN (SELECT
    num
  FROM @toBeRemoved)) ST1
  WHERE ST1.ID = ST2.ID
  ORDER BY ST1.ID
  FOR xml PATH (''))
  [NewItems],
  ROW_NUMBER() OVER (PARTITION BY ID ORDER BY id) AS RowNo
FROM (SELECT
  *
FROM @table a
CROSS APPLY dbo.Split(Items, ',')
WHERE Item NOT IN (SELECT
  num
FROM @toBeRemoved)) ST2) [Sub]
WHERE RowNo = 1
AND UserID IN (SELECT
  userId
FROM @forUserIds);

Notice the @toBeRemoved table and @forUserIds tables. These are exactly the tables you can change as you wish and dynamically modify. The result is dependent on these tables. The @table is just for testing purposes. Replace it by your actual table.

Edit2: Here is the sql update statement instead of select. I changed the @table to YourTable. Just use your table name instead.

UPDATE YourTable
SET Items = LEFT([Sub].[NewItems], LEN([Sub].[NewItems]) - 1)
FROM (SELECT DISTINCT
  ST2.*,
  (SELECT
    ST1.Item + ',' AS [text()]
  FROM (SELECT
    *
  FROM YourTable a
  CROSS APPLY dbo.Split(Items, ',')
  WHERE Item NOT IN (SELECT
    num
  FROM @toBeRemoved)) ST1
  WHERE ST1.ID = ST2.ID
  ORDER BY ST1.ID
  FOR xml PATH (''))
  [NewItems],
  ROW_NUMBER() OVER (PARTITION BY ID ORDER BY id) AS RowNo
FROM (SELECT
  *
FROM YourTable a
CROSS APPLY dbo.Split(Items, ',')
WHERE Item NOT IN (SELECT
  num
FROM @toBeRemoved)) ST2) [Sub]
WHERE [Sub].RowNo = 1
AND [Sub].UserID IN (SELECT
  userId
FROM @forUserIds)
AND [Sub].ID = YourTable.ID;

SELECT
  *
FROM YourTable
Community
  • 1
  • 1
Ivan Sivak
  • 7,178
  • 3
  • 36
  • 42
  • 1
    That splitter is really awful for performance. There is no need to use loops for splitting delimited strings. And multi-statement table valued functions are horrible on their own. Here are several better options. http://sqlperformance.com/2012/07/t-sql-queries/split-strings Nonetheless a plus 1 from me as the rest of this works as expected – Sean Lange Sep 02 '16 at 18:30
  • very cool, but i am confused a bit, we i need to create a complete code of @table and inserts and this portion `DECLARE @table TABLE ID int,UserID int,Items varchar(50_); INSERT INTO @table (ID, UserID, Items)VALUES (1, 54, '1,2,3,4,5,6'), (2, 55, '1,2,5,6,7,8'), (3, 56, '1,2,3,4,5,6'),(4, 57, '1,2,3,4,5,6'), (5, 58, '1,2,3,4,5,6'), (6, 59, '1,2,3,4,5,6'), (7, 54, '1,2,3,4,5,6');DECLARE @toBeRemoved TABLE (num varchar(16)); INSERT INTO @toBeRemoved (num)VALUES ('1'), ('2'), ('7'); DECLARE @forUserIds TABLE (userId int);INSERT INTO @forUserIds (userId)VALUES (54), (55), (56), (57), (58);~ –  Sep 02 '16 at 19:11
  • because my table has more columns too so is that inserts necessary –  Sep 02 '16 at 19:14
  • if i have more values how do i send them to the toberemoved, is this an spx, or function or what? –  Sep 02 '16 at 19:16
  • anyways, i manged to make it work, now i want to delete the original column called as items and rename the new column newlist as items and reflect that in table permanatly –  Sep 02 '16 at 19:54
  • @Jack Sorry for late comment, yes, the `@table` is here just for demo purposes (as I needed some test data to test the function). If you need to remove the old items values just perform sql `update` from select. Should be easy. In terms of `@toBeRemoved` table - the same as `@table`. You can replace it with your actual users table. Again it was just for demo data. – Ivan Sivak Sep 02 '16 at 20:00
  • i managed to do the changes i required, now the only action item which u told me to do an update is bit difficult for me because i am basically a front end guy so much with db, can you update your answer to include my last comment thanks –  Sep 02 '16 at 20:07
  • update somehow created a mess, now it goes in a loop and never comes back. –  Sep 02 '16 at 20:31
  • just 50 rows, no more one table has 4000 rows which is the users table and other table called as yourtable has 40 –  Sep 02 '16 at 20:34
  • @Jack That should be fine. The `update` itself works fine for me. Does it throw any error message or it just runs and never ends? – Ivan Sivak Sep 02 '16 at 20:38
  • it just runs and never ends, i think select was good with the exception we can write update at the bottom after select –  Sep 02 '16 at 20:45
  • @Jack I can't reproduce this. Works well on my machine. Perhaps give it some time. The update statement is OK. The last `SELECT * FROM YourTable` doesn't have to be there..I added it just to see the result after the update finishes but it's not necessary there. – Ivan Sivak Sep 02 '16 at 20:55
  • i changed this to select mode for test purpose and it started giving me Exception of type 'System.OutOfMemoryException' was thrown. error –  Sep 06 '16 at 18:57
  • @Jack Do you still get the exception? System.OutOfMemoryException sounds odd. Perhaps we could try a different split function with a better performance. – Ivan Sivak Sep 07 '16 at 04:53
  • yes it does throw that, what it last night is, it actually took off the entire database and i had to restart the sql server to bring it back, some kind of uncommitted thread or something, not sure what bu it went off yesterday completely –  Sep 07 '16 at 17:27
  • @Jack Is there any possibility you could run the sql on smaller number of records? e.g. not 4000 but lets say 50. – Ivan Sivak Sep 07 '16 at 19:27
  • i will try to do that, let me backupmy database –  Sep 08 '16 at 14:05
1

You can do this as:

update t
    set items = stuff(stuff(new_items, len(new_items) - 1, 1, ''), 1, 1, '') 
    from (select t.*,
                 replace(replace(replace(',1,', ',' + items + ',', ''
                                        ), ',2,', ''
                                ), ',7,', ''
                        ) as new_items
          from t
         ) t;

new_items is the list of items with 1, 2, and 7 removed, but still starting and ending with a comma. The stuff() function is used to remove the leading and trailing comma.

Gordon Linoff
  • 1,242,037
  • 58
  • 646
  • 786
  • will `stuff()` will work if it does not have a leading or trailing comma, and what if i have more than 3 values, i have to use more replace, correct –  Sep 02 '16 at 16:26
  • @Jack . . . This is putting leading and trailing commas on the list and then removing them. If you have more items, you would need another nested replace for each item. The above example shows the code for two. – Gordon Linoff Sep 03 '16 at 22:21
0

Will something like this work...

declare @CommaSeparatedValues varchar = '1,'
update TableName
set Items = REPLACE(Items, @CommaSeparatedValues, '')
  • but how we wrap in a function and also it needs to be userid dependent, i mean i have another table which has list of users like userid,firstname,lastname and indeed that is required too for this to make it happen –  Sep 02 '16 at 16:18
  • You didn't specify a length for your varchar and the default for a variable is 1. And what about values like 11, – Sean Lange Sep 02 '16 at 16:25
  • yes, u are correct, basically `varchar` is 255, but that `varchar` can also have a value of `training_id, training,training_summary` –  Sep 02 '16 at 16:47
  • Typo issue.. it should be varchar(AnyLength). For other tables or filters you can use Joins and WHERE clause. – Santosh Mishra Sep 02 '16 at 19:10