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
Related
I have two tables, let's call them Users and Fruit.
Users
ID Name Fruit
-------------------
1 Bob 1,3
2 Jack 3
Fruit
ID Name
-------------
1 Apple
2 Orange
3 Grape
How does one join those two tables to fill a datatable with the users choice of fruit names?
Would I need to write a stored procedure with a loop?
I am rather new to SQL Server and would be glad for any help or to be pointed in the right direction.
Created Physical tables with sample data
CREATE TABLE TempUsers
( ID INT,
Name VARCHAR(100),
Fruit VARCHAR(100)
)
INSERT INTO TempUsers
SELECT 1,'Bob' ,'1,3' UNION ALL
SELECT 2,'Jack','3'
CREATE TABLE TempFruit
( ID INT,
Name VARCHAR(100))
INSERT INTO TempFruit
SELECT 1,'Apple' UNION ALL
SELECT 2,'Orange'UNION ALL
SELECT 3,'Grape'
Create A Table-valued-Function to retrive the fruit names as comma separated
CREATE FUNCTION [dbo].[udf_GetFruitNames]
(
#vc_String nvarchar(max)
)
RETURNS #OutTable TABLE
(
Reqdata nvarchar(max)
)
AS
BEGIN
DECLARE #Temp AS TABLE
(
DATA nvarchar(max)
)
INSERT INTO #Temp
SELECT #vc_String;
DECLARE #Temp1 AS TABLE
(
DATA nvarchar(max)
)
INSERT INTO #Temp1
SELECT
STUFF((SELECT DISTINCT ','+ Name FROM
(
SELECT ID,
Name
FROm TempFruit
WHERE ID IN ( SELECT
CAST(Split.a.value('.', 'nvarchar(1000)') AS INT) AS FruitId
FROM
( SELECT
CAST( '<S>'+ REPLACE(DATA,',','</S><S>')+'</S>' AS XML) AS FruitId
FROM #Temp f
)AS A
CROSS APPLY FruitId.nodes('S') AS Split(a))
) As dt FOR XML PATH ('')),1,1,'') As FruitName
INSERT INTO #OutTable
SELECT * FROM #Temp1
RETURN
END
Sql query
SELECT ID
,Name
,uf.Reqdata AS FruitNames
FROM TempUsers u
CROSS APPLY [dbo].[udf_GetFruitNames](u.Fruit) AS uf
Or
SELECT ID
,Name
,(SELECT Reqdata FROM [dbo].[udf_GetFruitNames](u.Fruit) ) AS FruitNames
FROM TempUsers u
Result
ID Name FruitNames
---------------------
1 Bob Apple,Grape
2 Jack Grape
First of all, you need to redesign your tables. There is need for junction table, which will hold which user is connected to what fruit. It is a N:N raletionship.
So, you should create such table:
FruitUser
UserId FruitId
1 1
1 3
2 3
UserId is FK to Users table, FruitId is FK to Fruits table and both of these columns form a compoiste primary key. That's a standard approach.
Then you can use simple join to get results:
select * from users u
join FruitUser fu on u.id = fu.userid
join Fruit f on f.id = fu.fruitId
Sample Data
DECLARE #Users AS TABLE(ID INt, Name VARCHAR(100),fruit VARCHAR(100))
INSERT INTO #Users
SELECT 1,'Bob' ,'1,3' UNION ALL
SELECT 2,'Jack','3'
DECLARE #Fruit AS TABLE(ID INt, Name VARCHAR(100))
INSERT INTO #Fruit
SELECT 1,'Apple' UNION ALL
SELECT 2,'Orange'UNION ALL
SELECT 3,'Grape'
Sql Script
;WITH CTE
AS
(
SELECT UserId,
UserName ,
CAST(Split.a.value('.', 'nvarchar(1000)') AS INT) AS FruitId
FROM
( SELECT u.ID AS UserId,
u.Name AS UserName ,
CAST( '<S>'+ REPLACE(fruit,',','</S><S>')+'</S>' AS XML) AS FruitId
FROM #Fruit f
INNER JOIN #Users u
ON u.ID=f.ID
)AS A
CROSS APPLY FruitId.nodes('S') AS Split(a)
)
SELECT Userid,
UserName,
FruitId,
ft.name AS FruitName
FROM CTE c
LEFT JOIN (SELECT * FROM #Fruit) AS Ft
ON ft.ID=c.FruitId
Result
Userid UserName FruitId FruitName
------------------------------------------
1 Bob 1 Apple
1 Bob 3 Grape
2 Jack 3 Grape
For SQL Server 2014 where you can't use STRING_SPLIT , you can split the varchar using XML like following.
;WITH cte
AS (SELECT id,
name,
fruitid
FROM (SELECT *,
Cast('<X>' + Replace(F.fruit, ',', '</X><X>')
+ '</X>' AS XML) AS xmlfilter
FROM users F)F1
CROSS apply (SELECT fdata.d.value('.', 'varchar(50)') AS FruitId
FROM f1.xmlfilter.nodes('X') AS fdata(d)) O)
SELECT *
FROM cte C
INNER JOIN fruit F
ON F.id = Cast(C.fruitid AS INT)
DEMO
You can achieve the desire result without changing anything with this query
SELECT u.Id, u.Name, f.name
FROM Users u
inner join Fruit f on f.ID IN (SELECT cast(value as int)FROM STRING_SPLIT(u.fruit, ','));
Since you have sql server 2014 you have various options like CLR, XML and number functions. Best one is CLR but it's complex. So you can use this XML code.
select * from
(SELECT ID, [name],LTRIM(RTRIM(m.n.value('.[1]','varchar(8000)'))) AS fruitid
FROM
(SELECT ID,[name],CAST('<XMLRoot><RowData>' + REPLACE(fruit,',','</RowData><RowData>') + '</RowData></XMLRoot>' AS XML) AS x FROM {User table})t
CROSS APPLY x.nodes('/XMLRoot/RowData')m(n)) u
inner join {fruit table} f on f.id = u.fruitid
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
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
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.
I am not sure if this would be called pivoting.
Data in my SQL 2005 table [CustromerRoles] is as such:
CustId RoleId
2 4
2 3
3 4
4 1
4 2
[Roles] table:
RoleId Role
1 Admin
2 Manager
3 Support
4 Assistant
I want to create a view such that:
SELECT * FROM [MYVIEW] will give me the data below:
The 1 & 0's will be bits so that I can display a grid with checkboxes on my UI display.
CustId Admin Manager Support Assistant
2 0 0 1 1
3 0 0 0 1
4 1 1 0 0
So far I have no idea how to go about doing this.
Have you read the documentation on PIVOT in Microsoft SQL Server 2005?
SELECT CustId,
[1] AS Admin,
[2] AS Manager,
[3] AS Support,
[4] AS Assistant
FROM (SELECT c.CustId, r.RoleId
FROM CustomerRoles c JOIN Roles r USING (RoleId)) AS s
PIVOT (
COUNT(CustId)
FOR RoleId IN ([1], [2], [3], [4])
) AS pvt
ORDER BY CustId;
I haven't tested the above, but just based it on the doc. This may get you started.
There doesn't seem to be a way to generate the columns dynamically. You have to hard-code them.
Try this:
SELECT
CustId,
SUM(ISNULL(Admin,0)) AS Admin,
SUM(ISNULL(Manager,0)) AS Manager,
SUM(ISNULL(Support,0)) AS Support,
SUM(ISNULL(Assistant,0)) AS Assistant
FROM
(
SELECT cr.CustId, cr.RoleId, Role, 1 AS a
FROM CustromerRoles cr
INNER JOIN Roles r ON cr.RoleId = r.RoleId
) up
PIVOT (MAX(a) FOR Role IN (Admin, Manager, Support, Assistant)) AS pvt
GROUP BY CustId
Tested. It gives the same result you want.
PIVOT has the disadvantage that the columns must be known, because you have to provide the ids in the query. You can work around this by using dynamic SQL, i.e. generating the PIVOT query dynamically based on separate query results from the Roles table, in your case, then executing the result. This can easily be done in a stored procedure.
Example:
CREATE TABLE #CustomerRole ([CustId] int, [RoleId] int);
INSERT INTO #CustomerRole values (2, 4);
INSERT INTO #CustomerRole values (2, 3);
INSERT INTO #CustomerRole values (3, 4);
INSERT INTO #CustomerRole values (4, 1);
INSERT INTO #CustomerRole values (4, 2);
CREATE TABLE #Role ([Id] int, [Role] varchar(20));
INSERT INTO #Role values (1, 'Admin');
INSERT INTO #Role values (2, 'Manager');
INSERT INTO #Role values (3, 'Support');
INSERT INTO #Role values (4, 'Assistant');
DECLARE #RoleList nvarchar(MAX)
SELECT #RoleList = COALESCE(#RoleList + ',[' + [Role] + ']',
'[' + [Role] + ']')
FROM #Role;
DECLARE #SQL Nvarchar(max);
SET #SQL = 'SELECT
[CustId] ' +
ISNULL(', ' + #RoleList , '') + '
FROM #CustomerRole custrole
inner join #Role as r
on r.[Id] = custrole.[RoleId]
PIVOT (count([Id]) for [Role] IN
(' + ISNULL(#RoleList, '[No role]') +
')) as pvt;'
EXEC sp_executesql #SQL;
drop table #Role;
drop table #CustomerRole;