How to group by comma delimited column in mssql? - sql

I have tables with
T1(user)
Id Name CourseIds
1 Joel 1,2,3
2 Jeff 2,3,4
T2(courses)
CourseId CourseName
1 C#
2 Javascript
3 SQL
4 VB
I have to join the two tables and find out the count of each courses - learned by a user (group by) like
(result table)
Courses Count
C# 1
Javascript 2
SQL 2
VB 1
I have tried stackoverflow answers related to the question but they din't help. Please help me.

First, you should fix your data structure. Storing lists of ids in a comma delimited list is bad for many reasons:
Storing numbers as strings is bad.
Storing multiple values in a single field is bad.
Not being able to declare foreign key relationships is bad.
Such lists cannot take advantage of indexes.
Junction tables are a much better alternative.
Sometimes we are stuck with other peoples really bad decisions. If this is the case, you can do what you want, although not efficiently:
select c.coursename,
(select count(*)
from user u
where ',' + u.courseids + ',' like '%,' + cast(c.id as varchar(255)) + ',%'
) as cnt
from courses c;
But really, rather than putting together arcane string operations, it is better to fix the data structure.

With cte(courseids,si,ei)
As(
Select courseids,
1,
charindex(',', courseids)
From user
Union all
Select courseids,
Cast( 1 +ei as int),
Charindex(',',courseids,1+ei)
From cte
Where ei >0
)
Select substring(courseids,si, case when ei>0 then ei- si else Len(courseids) end) as courseid into #t
From cte;
Select count(1) as count, (select coursename from courses where courseid = t.courseid) as courses
From #t t
Group by courseid;
Drop table #t;

late to answer but you can proceed like this also
DECLARE #Table1 TABLE
(Id int, Name varchar(4), CourseIds varchar(5))
;
INSERT INTO #Table1
(Id, Name, CourseIds)
VALUES
(1, 'Joel', '1,2,3'),
(2, 'Jeff', '2,3,4')
;
DECLARE #Table2 TABLE
(CourseId int, CourseName varchar(10))
;
INSERT INTO #Table2
(CourseId, CourseName)
VALUES
(1, 'C#'),
(2, 'Javascript'),
(3, 'SQL'),
(4, 'VB')
;
declare #str varchar(max)
;with cte as (
SELECT Id,Name,
Split.a.value('.', 'VARCHAR(100)') AS Courseid
FROM (SELECT Id,Name,
CAST ('<M>' + REPLACE([CourseIds], ',', '</M><M>') + '</M>' AS XML) AS String
FROM #Table1) AS A CROSS APPLY String.nodes ('/M') AS Split(a))
select TT.CourseName,COUNT(C.Courseid) AS Courseid from cte C
INNER JOIN #Table2 TT
ON TT.CourseId = C.Courseid
GROUP BY TT.CourseName

Related

Remove e-mail address in a String SQL

I have a table with a field "E-mailTo".
This field contains a string of e-mail addresses.
Example
user1#domain.com; user2#domain.com; user3#domain.com; user4#domain.com; user5#domain.com;
user6#domain.com; user3#domain.com;
user7#domain.com; user4#domain.com;
I have a 2nd table with a list of e-mail addresses that need to be removed from all E-mailTo strings.
Example
user3#domain.com
user4#domain.com
Please can anyone help me achieve this?
Thank you
you could try using a replace with join
select replace(a.my_string, my_table2.col_mail_to_exclude,'')
from my_table1
inner join my_table2 on my_table1.my_string like concat('%', my_table2.col_mail_to_exclude, '%')
As others have suggested, it is better to go for normalized design, instead of storing emails as multivalued data.
But, for your needs, You can use recursive CTE to achieve this.
DECLARE #table1 table(id int, Emailto VARCHAR(8000))
INSERT INTO #table1 values
(1, 'user1#domain.com; user2#domain.com; user3#domain.com; user4#domain.com; user5#domain.com')
,(2, 'user6#domain.com; user3#domain.com;')
,(3,'user7#domain.com; user4#domain.com;')
DECLARE #table2 table(EmailToremove VARCHAR(8000))
INSERT INTO #table2 values
('user3#domain.com'),('user4#domain.com');
;WITH cte_table2rank AS
(
select ROW_NUMBER() OVER(ORDER BY EmailToRemove) as rnk, EmailToRemove
FROM #table2
), CTE_Cleanser AS
(
select id, EmailTo, 1 as rnk from #table1
UNION ALL
select c2.id, REPLACE(c2.EmailTo,c1.emailtoRemove,'') as Emailto, c2.rnk+1 as rnk
FROM cte_table2rank as C1
inner join CTE_Cleanser as c2
on c2.rnk = c1.rnk
)
SELECT id,Emailto FROM cte_cleanser as oc
where rnk = (SELECT max(rnk) FROM CTE_Cleanser where id = oc.id)
order by id
id
Emailto
1
user1#domain.com; user2#domain.com; ; ; user5#domain.com
2
user6#domain.com; ;
3
user7#domain.com; ;

T-SQL loop through two tables

I have been unable to find a working solution for below dilemma.
I am using SQL Server 2016 and have the 2 tables shown below in a database.
Users table:
Id Name
----------
1 Lisa
2 Paul
3 John
4 Mike
5 Tom
Role table:
Id UserId Role
------------------------
1 3 Manager
2 2,4,5 Developer
3 1 Designer
I am looking for T-SQL code that loops through the Role table, extracts UserIds and retrieves associated name for each Id from the Users table.
So the looped result would look like this:
John
Paul,Mike,Tom
Lisa
FOR SQL SERVER 2017
SELECT R1.Id,STRING_AGG(U.Name , ','),R1.Role
FROM Users U
INNER JOIN
(
SELECT R.Id,S.value AS UserId,R.Role
FROM Role R
CROSS APPLY STRING_SPLIT (UserID, ',') S
) R1
ON U.Id=R1.UserId
GROUP BY R1.ID,R1.Role
ORDER BY R1.ID;
OR
FOR SQL SERVER 2016
WITH CTE AS
(
SELECT R2.ID,U.Name,R2.UserId,R2.Role
FROM Users U
INNER JOIN
(
SELECT R.Id,S.value AS UserId,R.Role
FROM Role R
CROSS APPLY STRING_SPLIT (UserID, ',') S
)R2
ON U.id=R2.UserId
)
SELECT DISTINCT R1.Id,
STUFF((
SELECT ',' + name
FROM CTE R3
WHERE R1.Role = R3.Role
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '') AS NAME
,R1.Role
FROM CTE AS R1;
OR
For Old versions
With CTE AS
(
SELECT r.id,
u.name,
r.Role
FROM Users u
INNER JOIN Role r
ON ',' + CAST(r.Userid AS NVARCHAR(20)) + ',' like '%,' + CAST(u.id AS NVARCHAR(20)) + ',%'
)
SELECT DISTINCT id,
STUFF((
SELECT ',' + name
FROM CTE md
WHERE T.Role = md.Role
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '') AS NAME,
Role
FROM
CTE AS T
ORDER BY id
Output
id NAME Role
1 John Manager
2 Paul,Mike,Tom Developer
3 Lisa Designer
Demo
http://sqlfiddle.com/#!18/04a2d/69
There are several problems going on here. The first issue is that you're storing your values in a delimited format. Next, because you're storing your values in a delimited format, the values are being stored as a varchar. This has problems as well, as, as I would guess that the value of your column Id in the table Users is an int; meaning an implicit cast is needed and ruining any SARGability.
So, the solution is to fix the problem, in my view. Because you have a many to many relationship, you'll need an extra table. Let's design the tables as you have them right now, anyway:
CREATE TABLE Users (Id int, Name varchar(100));
CREATE TABLE Role (Id int, UserId varchar(100), [Role] varchar(100));
INSERT INTO Users
VALUES (1,'Lisa'),
(2,'Paul'),
(3,'John'),
(4,'Mike'),
(5,'Tom');
INSERT INTO Roles
VALUES(1,'3','Manager'),
(2,'2,4,5','Developer'),
(3,'1','Designer');
Now, instead we need a new table:
CREATE TABLE UserRoles (Id int, UserID int, RoleID int);
Now, we can insert the proper rows into the database. As you're using SQL Server 2016, we can use STRING_SPLIT:
INSERT INTO UserRoles (UserID, RoleID)
SELECT SS.value, R.Id
FROM Roles R
CROSS APPLY STRING_SPLIT (UserID, ',') SS;
After this, if you want, you could drop your existing column using the following, however, I see no harm in leaving it at the moment:
ALTER TABLE Roles DROP COLUMN UserID;
Now, we can query the data correctly:
SELECT *
FROM Users U
JOIN UserRoles UR ON U.ID = UR.UserID
JOIN Roles R ON UR.RoleID = R.Id;
If you want to then delimit this data, you can use STUFF, but don't store it back; I've explained how to correct your data for a reason! :)
SELECT [Role],
STUFF((SELECT ',' + [Name]
FROM Users U
JOIN UserRoles UR ON U.Id = UR.UserID
WHERE UR.RoleID = R.Id
FOR XML PATH ('')),1,1,'') AS Users
FROM Roles R;
If you were using SQL Server 2017, you'd be able to use STRING_AGG
Clean up script:
DROP TABLE UserRoles;
DROP TABLE Users;
DROP TABLE Roles;
Try this solution:
declare #users table (Id int, Name varchar(100))
declare #role table (Id int, UserId varchar(100), [Role] varchar(100))
insert into #users values
(1, 'Lisa'),
(2, 'Paul'),
(3, 'John'),
(4, 'Mike'),
(5, 'Tom')
insert into #role values
(1, '3', 'Manager'),
(2, '2,4,5', 'Developer'),
(3, '1', 'Designer')
select * from #role [r]
join #users [u] on
CHARINDEX(',' + cast([u].Id as varchar(3)) + ',', ',' + [r].UserId + ',', 1) > 0
I joined both tables based on occurence Id in UserId. To make it possible and avoid matches like: 2 is matched to 12, I decided to match only IDs surrounded by commas. That's why I wrapped in commas Id in a query and also wrapped UserId in commas, to match IDs at the end and the beginning of userId.
This query should give you satisfying result, but to match your desired output exatcly, you have to wrap this query in a CTE and perform group by with string concatenation:
;with cte as (
select [r].Id, [r].Role, [u].Name from #role [r]
join #users [u] on
CHARINDEX(',' + cast([u].Id as varchar(3)) + ',', ',' + [r].UserId + ',', 1) > 0
)
select Id,
(select Name + ',' from cte where Id = [c].Id for xml path('')) [Name],
--I believe this should work in your case, if so, just pick one column from these two
string_agg(Name + ',') [Name2],
Role
from cte [c]
group by Id, Role

Join by concatenated field (in CSV format)

UPDATE: I am fully aware that this is a poor RDBMS practice, but the question is not asking whether it is and how I can re-train the DBAs who created this architecture. The question is how I can work around the situation that I have on hands. I appreciate the help of the community and must admit that this is an interesting problem indeed.
In SQL Server 2017, I have a lookup table containing codes and a transactions table with CSV-formated codes:
CREATE TABLE #t(cd VARCHAR(100))
CREATE TABLE #cd (id INT, cd VARCHAR (1000))
INSERT INTO #t SELECT 'c1'
INSERT INTO #t SELECT 'c1,c2'
INSERT INTO #t SELECT 'c1,c2,c3'
INSERT INTO #cd SELECT 10, 'c1'
INSERT INTO #cd SELECT 20, 'c2'
INSERT INTO #cd SELECT 30, 'c3'
So, the lookup is
id cd
10 c1
10 c1
20 c2
30 c3
and, the transactions table has:
cd
c1
c1,c2
c1,c2,c3
I need to replace the codes to their respective IDs, while keeping these in CSV format.
I would like to avoid the cursor because it is too slow. Is there a way to parse the codes, do the JOIN, and recombine the IDs somehow efficiently? I suppose COALESCE may be of use, but need help applying it. Perhaps, there is already a function in t-SQL that does this types of lookups.
The output needs to another column in transactions table:
id
10
10,20
10,20,30
You can first strip out comma into a list and then join and get correct ids for codes and then add them back with commas. I used a row_number upfront to get a unique thing to join back on in my query.
See live demo
CREATE TABLE #t(cd VARCHAR(100))
CREATE TABLE #cd (id INT, cd VARCHAR (1000))
INSERT INTO #t SELECT 'c1'
INSERT INTO #t SELECT 'c1,c2'
INSERT INTO #t SELECT 'c1,c2,c3'
INSERT INTO #cd SELECT 10, 'c1'
INSERT INTO #cd SELECT 20, 'c2'
INSERT INTO #cd SELECT 30, 'c3'
; WITH X AS
(
SELECT
C.id,P1.rn
FROM
(
SELECT *, row_number() over( order by (select 1)) rn,
cast('<X>'+replace(P.cd,',','</X><X>')+'</X>' as XML) AS xmlitems FROM #t P
)P1
CROSS APPLY
(
SELECT fdata.D.value('.','varchar(100)') AS splitdata
FROM P1.xmlitems.nodes('X') AS fdata(D)) O
LEFT JOIN #cd C
ON C.cd= LTRIM(RTRIM(O.splitdata ))
)
SELECT
rn,
id= STUFF((
SELECT ',' + cast(id as varchar(100)) FROM X AS x2
WHERE x2.rn = x.rn
ORDER BY rn FOR XML PATH,
TYPE).value(N'.[1]',N'varchar(max)'), 1, 1, '')
FROM
X
GROUP BY rn
Note: With SQL server 2017 you can also you SPLIT_STRING() function
and STRING_AGG() functions
SQL SERVER 2017 code:
select
id=STRING_AGG(id,',')
from
(
select V=value, rn
from
(
select
rn=row_number() over( order by (select 1)),
cd
from #T
)T
cross apply STRING_SPLIT(cd, ',')
) T
left join #cd C
on cd= v
group by rn

SQL Server stored procedure looping through a comma delimited cell

I am trying to figure out how to go about getting the values of a comma separated string that's present in one of my cells.
This is the query I current am trying to figure out in my stored procedure:
SELECT
uT.id,
uT.permissions
FROM
usersTbl AS uT
INNER JOIN
usersPermissions AS uP
/*Need to loop here I think?*/
WHERE
uT.active = 'true'
AND
uT.email = 'bbarker#thepriceisright.com'
The usersPermissions table looks like this:
And so a row in the usersTbl table looks like this for permissions:
1,3
I need to find a way to loop through that cell and get each number and place the name ****, in my returned results for the usersTbl.permissions.
So instead of returning this:
Name | id | permissions | age |
------------------------------------
Bbarker | 5987 | 1,3 | 87 |
It needs to returns this:
Name | id | permissions | age |
------------------------------------
Bbarker | 5987 | Read,Upload | 87 |
Really just replacing 1,3 with Read,Upload.
Any help would be great from a SQL GURU!
Reworked query
SELECT
*
FROM
usersTbl AS uT
INNER JOIN
usersPermissionsTbl AS uPT
ON
uPT.userId = uT.id
INNER JOIN
usersPermissions AS uP
ON
uPT.permissionId = uP.id
WHERE
uT.active='true'
AND
uT.email='bBarker#thepriceisright.com'
I agree with all of the comments... but strictly trying to do what you want, here's a way with a splitter function
declare #usersTbl table ([Name] varchar(64), id int, [permissions] varchar(64), age int)
insert into #usersTbl
values
('Bbarker',5987,'1,3',87)
declare #usersTblpermissions table (id int, [type] varchar(64))
insert into #usersTblpermissions
values
(1,'Read'),
(2,'Write'),
(3,'Upload'),
(4,'Admin')
;with cte as(
select
u.[Name]
,u.id as UID
,p.id
,p.type
,u.age
from #usersTbl u
cross apply dbo.DelimitedSplit8K([permissions],',') x
inner join #usersTblpermissions p on p.id = x.Item)
select distinct
[Name]
,UID
,age
,STUFF((
SELECT ',' + t2.type
FROM cte t2
WHERE t.UID = t2.UID
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '')
from cte t
Jeff Moden Splitter
CREATE FUNCTION [dbo].[DelimitedSplit8K] (#pString VARCHAR(8000), #pDelimiter CHAR(1))
--WARNING!!! DO NOT USE MAX DATA-TYPES HERE! IT WILL KILL PERFORMANCE!
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
/* "Inline" CTE Driven "Tally Table" produces values from 1 up to 10,000...
enough to cover VARCHAR(8000)*/
WITH E1(N) AS (
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
), --10E+1 or 10 rows
E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
cteTally(N) AS (--==== This provides the "base" CTE and limits the number of rows right up front
-- for both a performance gain and prevention of accidental "overruns"
SELECT TOP (ISNULL(DATALENGTH(#pString),0)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
),
cteStart(N1) AS (--==== This returns N+1 (starting position of each "element" just once for each delimiter)
SELECT 1 UNION ALL
SELECT t.N+1 FROM cteTally t WHERE SUBSTRING(#pString,t.N,1) = #pDelimiter
),
cteLen(N1,L1) AS(--==== Return start and length (for use in substring)
SELECT s.N1,
ISNULL(NULLIF(CHARINDEX(#pDelimiter,#pString,s.N1),0)-s.N1,8000)
FROM cteStart s
)
--===== Do the actual split. The ISNULL/NULLIF combo handles the length for the final element when no delimiter is found.
SELECT ItemNumber = ROW_NUMBER() OVER(ORDER BY l.N1),
Item = SUBSTRING(#pString, l.N1, l.L1)
FROM cteLen l
;
GO
First, you should read Is storing a delimited list in a database column really that bad?, where you will see a lot of reasons why the answer to this question is Absolutely yes!
Second, you should add a table for user permissions since this is clearly a many to many relationship.
Your tables might look something like this (pseudo code):
usersTbl
(
Id int primary key
-- other user related columns
)
usersPermissionsTbl
(
UserId int, -- Foreign key to usersTbl
PermissionId int, -- Foreign key to permissionsTbl
Primary key (UserId, PermissionId)
)
permissionsTbl
(
Id int primary key,
Name varchar(20)
)
Once you have your tables correct, it's quite easy to get a list of comma separated values from the permissions table.
Adapting scsimon's sample data script to a correct many to many relationship:
declare #users table ([Name] varchar(64), id int, age int)
insert into #users values
('Bbarker',5987,87)
declare #permissions table (id int, [type] varchar(64))
insert into #permissions values
(1,'Read'),
(2,'Write'),
(3,'Upload'),
(4,'Admin')
declare #usersPermissions as table (userId int, permissionId int)
insert into #usersPermissions values (5987, 1), (5987, 3)
Now the query looks like this:
SELECT u.Name,
u.Id,
STUFF(
(
SELECT ','+ [type]
FROM #permissions p
INNER JOIN #usersPermissions up ON p.id = up.permissionId
WHERE up.userId = u.Id
FOR XML PATH('')
)
, 1, 1, '') As Permissions,
u.Age
FROM #Users As u
And the results:
Name Id Permissions Age
Bbarker 5987 Read,Upload 87
You can see a live demo on rextester.
I concur with much of the advice being presented to you in the other responses. The structure you're starting with is not going to be fun to maintain and work with. However, your situation may mean you are stuck with it so maybe some of the tools below will help you.
You can parse the delimiter with charindex() as others demonstrated here- MSSQL - How to split a string using a comma as a separator
... and even better here (several functions are provided) - Split function equivalent in T-SQL?
If you still want to do it with raw inline SQL and are committed to a loop, then pair the string manipulation with a CURSOR. Cursors have their own controversies BTW. The code below will work if your permission syntax remains consistent, which it probably doesn't.
They used charindex(',',columnName) and fed the location into the left() and right() functions along with some additional string evaluation to pull values out. You should be able to piece those together with a cursor
Your query might look like this...
--creating my temp structure
declare #userPermissions table (id int, [type] varchar(16))
insert into #userPermissions (id, [type]) values (1, 'Read')
insert into #userPermissions (id, [type]) values (2, 'Write')
insert into #userPermissions (id, [type]) values (3, 'Upload')
insert into #userPermissions (id, [type]) values (4, 'Admin')
declare #usersTbl table ([Name] varchar(16), id int, [permissions] varchar(8), age int)
insert into #usersTbl ([Name], id, [permissions], age) values ('Bbarker', 5987, '1,3', 87)
insert into #usersTbl ([Name], id, [permissions], age) values ('Mmouse', 5988, '2,4', 88)
--example query
select
ut.[Name]
, (select [type] from #userPermissions where [id] = left(ut.[permissions], charindex(',', ut.[permissions])-1) )
+ ','
+ (select [type] from #userPermissions where [id] = right(ut.[permissions], len(ut.[permissions])-charindex(',', ut.[permissions])) )
from #usersTbl ut

How to insert multiple values in SQL Server table?

I don't exactly know how to phrase the question, but an example would work. So I have this table
Users
Id Name
1 Tim
2 Jon
3 Matt
There is another table
Tags
TagId TagName
1 Test
2 Other
3 Dummy
4 More
In a temp table I have structure like this
TmpUserTags
User Tags
Tim Test
Jon Other, Test
Matt Dummy, More, Other
So, what I need to do is from this temp table, insert record in table UserTags with corresponding Ids, for the above given example, the result would be
UserTags
User TagId
1 1
2 2
2 1
3 3
3 4
3 2
So, this is the end result I want, to be inserted in UserTags. But since for each row in TmpUserTags each user can have many tags, separated by comma, I don't know what would be the best way to do it. I can probably use a while loop (or rather a cursor) to loop through all the rows in TmpUserTags, and then, for each row, split the tags by comma, find their Id, and insert those in UserTags. But that doesn't seems to be the most optimized way. Can someone please suggest some better way of doing it?
I think the simplest way would be to just join the tags column using LIKE:
CREATE TABLE #Users (ID INT, Name VARCHAR(4));
INSERT #Users (ID, Name)
VALUES (1, 'Tim'), (2, 'Jon'), (3, 'Matt');
CREATE TABLE #Tags (TagID INT, TagName VARCHAR(5));
INSERT #Tags (TagID, TagName)
VALUES (1, 'Test'), (2, 'Other'), (3, 'Dummy'), (4, 'More');
CREATE TABLE #TmpUserTags ([User] VARCHAR(4), Tags VARCHAR(100));
INSERT #tmpUserTags ([User], Tags)
VALUES ('Tim', 'Test'), ('Jon', 'Other,Test'), ('Matt', 'Dummy,More,Other');
SELECT u.ID, t.TagID
FROM #TmpUserTags AS ut
INNER JOIN #Users AS u
ON u.Name = ut.[User]
INNER JOIN #Tags AS t
ON ',' + ut.Tags + ',' LIKE '%,' + t.TagName + ',%';
You could also go down the route of creating a split function to split your comma separated list into rows:
CREATE FUNCTION [dbo].[Split](#StringToSplit NVARCHAR(MAX), #Delimiter NCHAR(1))
RETURNS TABLE
AS
RETURN
(
SELECT ID = ROW_NUMBER() OVER(ORDER BY n.Number),
Position = Number,
Value = SUBSTRING(#StringToSplit, Number, CHARINDEX(#Delimiter, #StringToSplit + #Delimiter, Number) - Number)
FROM ( SELECT TOP (LEN(#StringToSplit) + 1) Number = ROW_NUMBER() OVER(ORDER BY a.object_id)
FROM sys.all_objects a
) n
WHERE SUBSTRING(#Delimiter + #StringToSplit + #Delimiter, n.Number, 1) = #Delimiter
);
Then you can use:
SELECT u.ID, t.TagID
FROM #TmpUserTags AS ut
CROSS APPLY dbo.Split(ut.tags, ',') AS s
INNER JOIN #Users AS u
ON u.Name = ut.[User]
INNER JOIN #Tags AS t
ON t.TagName = s.Value;
Here is XML-version of answer:
SELECT Users.Id as [User],Tags.TagId
FROM
(SELECT A.[User],
LTRIM(Split.a.value('.', 'VARCHAR(100)')) AS Tagname
FROM (SELECT [User],
CAST ('<M>' + REPLACE(Tags, ',', '</M><M>') + '</M>' AS XML) AS String
FROM TmpUserTags) AS A CROSS APPLY String.nodes ('/M') AS Split(a)) ut
LEFT JOIN Users ON Users.Name=ut.[User]
LEFT JOIN Tags ON Tags.TagName=ut.Tagname
No procedures, functions and CTEs.
[Update] If some performance issues are appeared, please read this nice article: http://beyondrelational.com/modules/2/blogs/114/posts/14617/delimited-string-tennis-anyone.aspx
Left join is used to show all rows from the table TmpUserTags even if other tables are w/o some necessary rows (E.g. new User 'Bob' with Tag 'Test2' that isn't described in table Tags)
To me the main question would be how you arrived at the temp table containing the comma delimited column. If it was an import from a file and all the data was comma delimited it would be easy enough to save the file as a csv which will save the user and each tag separately, then create a table in the your database containing the same number of columns as the file has, then bulk insert this table from the file.
drop table #TmpUserTags
GO
create table #TmpUserTags
(
[user] varchar(10),
tag1 varchar(10),
tag2 varchar(10),
tag3 varchar(10)
)
bulk insert #TmpUserTags from '<filepath>' with (fieldterminator=',')
Then union the data to create two columns which should be easy enough to reinterpret as ids.
SELECT [User],Tag1 FROM #TmpUserTags WHERE Tag1 IS NOT NULL
UNION ALL
SELECT [User],Tag2 FROM #TmpUserTags WHERE Tag2 IS NOT NULL
UNION ALL
SELECT [User],Tag3 FROM #TmpUserTags WHERE Tag3 IS NOT NULL
ORDER BY [User]
Of course all this might be conjecture but, like, how did you arrive at the table with the comma delimited values?
Just another way to do it (Fiddle: http://sqlfiddle.com/#!3/7f48f/20):
WITH cteTagMatrix
AS
(
SELECT n.ID,
CASE
WHEN CHARINDEX(',' + t.TagName + ',', REPLACE(',' + tut.Tags + ',', ' ', '')) <> 0 THEN t.TagID
ELSE NULL
END AS TagID
FROM Names n INNER JOIN TmpUserTags tut
ON n.[Name] = tut.[User]
CROSS JOIN Tags t
)
SELECT *
FROM cteTagMatrix
WHERE TagID IS NOT NULL
ORDER BY ID, TagID;)
EDIT: Oops, was a error with my comma logic. Fixed code and fiddle updated.