Extract comma separated values from comma separated GUIDs - sql

I have a column in table T1 named Categories, which contains GUIDs in XML. I am able to extract the GUIDs in a comma-separated form using the below query.
SELECT
Row, ID, Name, City,
Category = STUFF(
(
SELECT ',' + t.c.value('.', 'nvarchar(max)')
FROM dbo.T1 t1
OUTER APPLY t1.Categories.nodes('root/string') as t(c)
WHERE t1.ID = t2.ID FOR XML PATH('')
), 1, 1, ''
)
FROM
dbo.T1 t2
I have another table T2, which contains the names of the Categories. I now want to use these comma-separated GUIDs to go and fetch their corresponding Name from T2.
What changes do I need to make in my SELECT statement to write a LEFT OUTER JOIN which takes this comma-separated GUIDs and returns comma-separated names from T2.
T2 looks something like this:

I would join the category name table before concatenating the values to avoid another iteration of splitting and concatenating.
Sample data
create table xmlData
(
id int,
data xml
);
insert into xmlData (id, data) values
(1,'
<root>
<guid>5d8547aa-e1e7-4f69-88a2-655879531582</guid>
<guid>78555c5d-e39f-48f3-a148-30161b0fb995</guid>
</root>
'),
(2,'
<root>
<guid>5d8547aa-e1e7-4f69-88a2-655879531582</guid>
<guid>f58177f6-63c8-4985-baa8-2db05248f13f</guid>
</root>
'),
(3,'
<root>
<guid>5d8547aa-e1e7-4f69-88a2-655879531582</guid>
<guid>d8f9b789-6d60-4688-9d91-c0f8b1df5319</guid>
</root>
');
create table categoryName
(
guid uniqueidentifier,
name nvarchar(20)
);
insert into categoryName (guid, name) values
('5d8547aa-e1e7-4f69-88a2-655879531582', 'Alpha'),
('78555c5d-e39f-48f3-a148-30161b0fb995', 'Beta'),
('f58177f6-63c8-4985-baa8-2db05248f13f', 'Gamma'),
('d8f9b789-6d60-4688-9d91-c0f8b1df5319', 'Delta');
Solution
Two versions because the SQL Server version is not specified in the question tags... The string_agg() function is available starting from SQL Server 2017.
With string_agg()
select xd.id,
string_agg(cn.name, ',') as 'category_names'
from xmlData xd
cross apply xd.data.nodes('root/guid') g(guid)
join categoryName cn
on cn.guid = g.guid.value('.', 'nvarchar(36)')
group by xd.id
order by xd.id;
Without string_agg()
select xd.id,
stuff( ( select ',' + cn.name
from xmlData xd2
cross apply xd.data.nodes('root/guid') g(guid)
join categoryName cn
on cn.guid = g.guid.value('.', 'nvarchar(36)')
where xd2.id = xd.id
for xml path('') ), 1, 1, '' ) as 'category_names'
from xmlData xd
order by xd.id;
Result
id category_names
-- --------------
1 Alpha,Beta
2 Alpha,Gamma
3 Alpha,Delta
Fiddle to see things in action.

Related

Combine multiple rows into one by coalescing one column's value as CSV from two tables

I'll divide this into three parts:
What I have:
I have two tables Table1 and Table2.
Table1
ObjectName
Status
A
Active
C
Active
Table2
ParentObjectType
ChildObjectType
X
A
Y
C
Z
A
M
C
What I want:
I want to write a stored procedure that gives a result that looks something like this:
ObjectName
Status
ParentObjectName
A
Active
X, Z
C
Active
Y, M
What I have tried: I tried using the STUFF function and I'm getting a weird result.
Here's the query:
SELECT
ObjectName,
Status,
STUFF((SELECT '; ' + table2.ParentObjectType
FROM table1
INNER JOIN table2 ON table1.[ObjectName] = table2.[ChildObjectType]
FOR XML PATH('')), 1, 1, '') [ParentObjectName]
FROM
table1
Output
ObjectName
Status
ParentObjectName
A
Active
X, Z, Y, M
C
Active
X, Z, Y, M
Any help here is highly appreciated as I'm light handed on SQL and this is driving me nuts!
Demo: Fiddle
You are missing WHERE condition in your Subquery for a parent table.
Also I assume this is a typo. In Table2 you have column ChildObjectType but in your link you are linking over ˛table2.[ChildObjectName]
SELECT
ObjectName,
Status,
STUFF((SELECT '; ' + table2.ParentObjectType
FROM table1
INNER JOIN table2 ON table1.[ObjectName] = table2.[ChildObjectName]
WHERE Table1.ObjectName = src.ObjectName
FOR XML PATH('')), 1, 1, '') [ParentObjectName]
FROM
table1 src
Note: You can use STRING_AGG starting from SQL Server 2017 (14.x) and later
This helped me realize I didn't have this saved in my snippets, thanks! Being careful thatFOR XML PATH will return XML Encoded text, so "&" becomes "&", see below for an example that shows you can add , TYPE to your FOR XML statement; This returns an xml datatype, that you can query the text out of with value('.',....
I personally tend to favor subqueries below the FROM, so this also shows an alternative style for joining the data, via a WHERE clause inside the APPLY refernce:
DECLARE #tt1 TABLE ( ObjectName VARCHAR(10), StatusValue VARCHAR(20) )
INSERT INTO #tt1
SELECT 'A','Active'
UNION ALL SELECT 'C','Active'
UNION ALL SELECT 'D&E','Active'
DECLARE #tt2 TABLE ( A VARCHAR(100), B VARCHAR(100) )
INSERT INTO #tt2 SELECT 'X','A'
INSERT INTO #tt2 SELECT 'Y','C'
INSERT INTO #tt2 SELECT 'Z','A'
INSERT INTO #tt2 SELECT 'M','C'
INSERT INTO #tt2 SELECT 'E&F','D&E' --sample "&" that should NOT render "&"
INSERT INTO #tt2 SELECT '"G"','D&E'
INSERT INTO #tt2 SELECT 'F>G','C' --sample ">" that should NOT render ">"
SELECT
tt1.*,
f1.*
FROM
(SELECT ObjectName,StatusValue FROM #tt1) tt1
OUTER APPLY (SELECT
COALESCE(STUFF(
(SELECT ',' + CAST(tt2.A AS VARCHAR(10))
FROM
#tt2 tt2 WHERE tt2.B = tt1.ObjectName FOR XML PATH(''), TYPE ).value('.','nvarchar(max)'), 1,1,''),'') [csv1] ) f1
I'm assuming that you are on a SQL server version that does not have string aggregating functions?

Replace columns separated by string with id from another table - SQL Server

I have following 2 tables in SQL Server
Category table:
Category
--------------------------
Delivery;Gauges;Book;Table
Category id:
id name
-----------------
13183 Delivery
88781 Gauges
88782 Book
12512 Table
Intended result is to have category table replaced with category id, as:
Category
-----------------------
13183;88781;88782;12512
I approached this by first separating category columns into separate columns using :
ltrim(rtrim(xDim.value('/x[1]','varchar(max)')))
ltrim(rtrim(xDim.value('/x[2]','varchar(max)')))
and so on. Then used left join and replace on each new column. Isn't there an easier way to do this? I searched on the net and stackoverflow but can't seem to find anything similar.
You can try to make a function to split your string value by a character.
CREATE FUNCTION Split_fun
( #Words nvarchar(MAX)
, #splitStr varchar(50)
)
RETURNS #Result_Table TABLE
(
[word] nvarchar(max) NULL
)
BEGIN
Declare #TempStr nvarchar(MAX)
WHILE (CHARINDEX(#splitStr,#Words)>0)
BEGIN
Set #TempStr=SUBSTRING(#Words,1,CHARINDEX(#splitStr,#Words)-1)
Insert into #Result_Table (word) Values (#TempStr)
Set #Words = REPLACE(#Words,#TempStr+#splitStr,'')
END/*End While*/
IF(LEN(RTRIM(LTRIM(#Words)))>0 And CHARINDEX(#splitStr,RTRIM(LTRIM(#Words)))=0)
Begin
Set #TempStr=#Words
Insert into #Result_Table (word) Values (#TempStr)
End
RETURN
END
you can use this function to make a result set by ';'.
do self-join with Category id table.
final you can use FOR XML connect all string by ; to get your expectation result.
;with cte as (
SELECT id
FROM T CROSS APPLY Split_fun(Category,';') v
JOIN T1 on v.word = t1.Category
)
select STUFF((
select distinct ';'+ cast(id as varchar(10))
FROM cte
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '')
sqlfiddle

SQL Query using inner join

CategoryTable
Code Name
1 Food
2 Non-Food
Existing Table Consists list of category, as for example, I have two only Food and Non-Food
As challenge, I am assigning tenants with category or categories (multiple assignment, as there are tenants which are categorized as food and non-food). I i used to insert Tenant and Code to a new table creating this output
TenantAssignTable
Tenant Code
Tenant1 1,2
Tenant2 1
What I need to do, is to load the TenantAssingTable to gridview consisting the Name of the CategoryCode too like this
Desired Output
Tenant CCode Name
Tenant1 1,2 Food,Non-Food
Tenant2 1 Food
I used inner join in my code, but this is limited as I have a string of combined code in Code column.
Select a.tenant, a.ccode, b.name
from TenantAssignTable a inner join CategoryTable b
on a.CCode = b.code
Is there anyway to achieve this kind of output? I know that this is unusual in SQL coding but this is what is challenge as what the desired output is concerned and needs which is to have a multiple assignment of category to a single tenant.
Thanks in advance!
Think simple;
You can with LIKE and XML PATH
DECLARE #CategoryTable TABLE (Code VARCHAR(50), Name VARCHAR(50))
INSERT INTO #CategoryTable
VALUES
('1', 'Food'),
('2', 'Non-Food')
DECLARE #TenantAssignTable TABLE (Tenant VARCHAR(50), Code VARCHAR(50))
INSERT INTO #TenantAssignTable
VALUES
('Tenant1', '1,2'),
('Tenant2', '1')
SELECT
T.Tenant ,
T.Code,
STUFF(
(SELECT
',' + C.Name
FROM
#CategoryTable C
WHERE
',' + REPLACE(T.Code, ' ', '') + ',' LIKE '%,' + C.Code + ',%'
FOR XML PATH('')
), 1, 1, '') A
FROM
#TenantAssignTable T
Result:
Tenant Code A
--------------- ------------ ---------------
Tenant1 1,2 Food,Non-Food
Tenant2 1 Food
You can use some XML transformations:
DECLARE #x xml
SELECT #x = (
SELECT CAST('<t name="'+a.tenant +'"><a>'+REPLACE(a.code,',','</a><a>') +'</a></t>' as xml)
FROM TenantAssignTable a
FOR XML PATH('')
)
;WITH cte AS (
SELECT t.v.value('../#name','nvarchar(max)') as Tenant,
t.v.value('.','int') as CCode,
ct.Name
FROM #x.nodes('/t/a') as t(v)
INNER JOIN CategoryTable ct
ON ct.Code = t.v.value('.','int')
)
SELECT DISTINCT
c.Tenant,
STUFF((SELECT ','+CAST(CCode as nvarchar(10))
FROM cte
WHERE c.Tenant = Tenant
FOR XML PATH('')
),1,1,'') as CCode,
STUFF((SELECT ','+Name
FROM cte
WHERE c.Tenant = Tenant
FOR XML PATH('')
),1,1,'') as Name
FROM cte c
Output:
Tenant CCode Name
Tenant1 1,2 Food,Non-Food
Tenant2 1 Food
The first part (defining #x variable) will bring your table to this kind of XML:
<t name="Tenant1">
<a>1</a>
<a>2</a>
</t>
<t name="Tenant2">
<a>1</a>
</t>
Then in CTE part we join XML with table of categories. And after all get data from CTE with the help of FOR XML PATH.
Create Function as below which return Table from separated Value
CREATE FUNCTION [dbo].[fnSplit]
(
#String NVARCHAR(4000),
#Delimiter NCHAR(1)
)
RETURNS TABLE
AS
RETURN
(
WITH Split(stpos,endpos)
AS(
SELECT 0 AS stpos, CHARINDEX(#Delimiter,#String) AS endpos
UNION ALL
SELECT endpos+1, CHARINDEX(#Delimiter,#String,endpos+1)
FROM Split
WHERE endpos > 0
)
SELECT 'Id' = ROW_NUMBER() OVER (ORDER BY (SELECT 1)),
'Data' = SUBSTRING(#String,stpos,COALESCE(NULLIF(endpos,0),LEN(#String)+1)-stpos)
FROM Split
)
Create Function as below which return comma separated Name
CREATE FUNCTION [dbo].[GetCommaSeperatedCategory]
(
#Codes VARCHAR(50)
)
RETURNS VARCHAR(5000)
AS
BEGIN
-- Declare the return variable here
DECLARE #Categories VARCHAR(5000)
SELECT #Categories= STUFF
(
(SELECT ',' + convert(varchar(10), Name, 120)
FROM Category
WHERE Code IN (SELECT Id FROM [dbo].[fnSplit] (#Codes,',') )
ORDER BY Code
FOR XML PATH (''))
, 1, 1, '')
RETURN #Categories
END
AND Last:
SELECT
Tenant,
Code,
(SELECT [dbo].[GetCommaSeperatedCategory] (Code)) AS Name
FROM TblTenant

Splitting a variable length column in SQL server safely

I have a column (varchar400) in the following form in an SQL table :
Info
UserID=1123456,ItemID=6685642
The column is created via our point of sale application, and so I cannot do the normal thing of simply splitting it into two columns as this would cause an obscene amount of work. My problem is that this column is used to store attributes of products in our database, and so while I am only concerned with UserID and ItemID, there may be superfluous information stored here, for example :
Info
IrrelevantID=666,UserID=123124,AnotherIrrelevantID=1232342,ItemID=1213124.
What I want to retrieve is simply two columns, with no error given if neither of these attributes exists in the Info column. :
UserID ItemID
123124 1213124
Would it be possible to do this effectively, with error checking, given that the length of the IDs are all variable, but all of the attributes are comma-separated and follow a uniform style (i.e "UserID=number").
Can anyone tell me the best way of dealing with my problem ?
Thanks a lot.
Try this
declare #infotable table (info varchar(4000))
insert into #infotable
select 'IrrelevantID=666,UserID=123124,AnotherIrrelevantID=1232342,ItemID=1213124.'
union all
select 'UserID=1123456,ItemID=6685642'
-- convert info column to xml type
; with cte as
(
select cast('<info ' + REPLACE(REPLACE(REPLACE(info,',', '" '),'=','="'),'.','') + '" />' as XML) info,
ROW_NUMBER() over (order by info) id
from #infotable
)
select userId, ItemId from
(
select T.N.value('local-name(.)', 'varchar(max)') as Name,
T.N.value('.', 'varchar(max)') as Value, id
from cte cross apply info.nodes('//#*') as T(N)
) v
pivot (max(value) for Name in ([UserID], [ItemId])) p
SQL DEMO
You can try this split function: http://www.sommarskog.se/arrays-in-sql-2005.html
Assuming ItemID=1213124. is terminated with a dot.
Declare #t Table (a varchar(400))
insert into #t values ('IrrelevantID=666,UserID=123124,AnotherIrrelevantID=1232342,ItemID=1213124.')
insert into #t values ('IrrelevantID=333,UserID=222222,AnotherIrrelevantID=0,ItemID=111.')
Select
STUFF(
Stuff(a,1,CHARINDEX(',UserID=',a) + Len(',UserID=')-1 ,'' )
,CharIndex
(',',
Stuff(a,1,CHARINDEX(',UserID=',a) + Len(',UserID=')-1 ,'' )
)
,400,'') as UserID
,
STUFF(
Stuff(a,1,CHARINDEX(',ItemID=',a) + Len(',ItemID=')-1 ,'' )
,CharIndex
('.',
Stuff(a,1,CHARINDEX(',ItemID=',a) + Len(',ItemID=')-1,'' )
)
,400,'') as ItemID
from #t

SQL Server query with multiple values in one column relating to another column

Situation: This table holds the relation information between a Documents table and an Users table. Certain Users need to review or approve documents (Type). I would like to have it to where I could get all of the reviewers on one line if needed. So if three users review Document 1, then a row would have 346, 394, 519 as the value, since those are the reviewers
Table: xDocumentsUsers
DocID..UserID....Type...
1........386......approver
1........346......reviewer
1........394......reviewer..
1........519......reviewer..
4........408......reviewer..
5........408......reviewer..
6........408......reviewer..
7........386......approver..
7........111......readdone..
7........346......reviewer..
8........386......approver..
8........346......reviewer..
9........386......approver..
9........346......reviewer..
10.......386......approver..
11.......386......approver..
11......346......reviewer..
12......386......approver..
12......346......reviewer..
13......386......approver..
13......346......reviewer..
14......386......approver..
14......346......reviewer..
15......386......approver
So desired result would be...
DocID..UserID................Type...
1........386....................approver
1........346,394,519......reviewer.
4........408....................reviewer..
5........408....................reviewer..
6........408....................reviewer..
7........386....................approver..
7........111....................readdone..
7........346....................reviewer..
8........386....................approver..
8........346....................reviewer..
9........386....................approver..
9........346....................reviewer..
10......386....................approver..
11......386....................approver..
11......346....................reviewer..
12......386....................approver..
12......346....................reviewer..
13......386....................approver..
13......346....................reviewer..
14......386....................approver..
14......346....................reviewer..
15......386....................approver
The FOR XML PATH is a great solution. You need to be aware, though, that it will convert any special characters in the inner SELECTs result set into their xml equivalent - i.e., & will become & in the XML result set. You can easily revert back to the original character by using the REPLACE function around the inner result set. To borrow from astander's previous example, it would look like (note that the SELECT as the 1st argument to the REPLACE function is enclosed in ():
--Concat
SELECT t.ID,
REPLACE((SELECT tIn.Val + ','
FROM #Table tIn
WHERE tIn.ID = t.ID
FOR XML PATH('')), '&', '&'))
FROM #Table t
GROUP BY t.ID
Have a look at
Emulating MySQL’s GROUP_CONCAT() Function in SQL Server 2005
Is there a way to create a SQL Server function to “join” multiple rows from a subquery into a single delimited field?
A simple example is
DECLARE #Table TABLE(
ID INT,
Val VARCHAR(50)
)
INSERT INTO #Table (ID,Val) SELECT 1, 'A'
INSERT INTO #Table (ID,Val) SELECT 1, 'B'
INSERT INTO #Table (ID,Val) SELECT 1, 'C'
INSERT INTO #Table (ID,Val) SELECT 2, 'B'
INSERT INTO #Table (ID,Val) SELECT 2, 'C'
--Concat
SELECT t.ID,
(
SELECT tIn.Val + ','
FROM #Table tIn
WHERE tIn.ID = t.ID
FOR XML PATH('')
)
FROM #Table t
GROUP BY t.ID
Does this help?
SELECT DocID
, [Type]
, (SELECT CAST(UserID + ', ' AS VARCHAR(MAX))
FROM [xDocumentsUsers]
WHERE (UserID = x1.UserID)
FOR XML PATH ('')
) AS [UserIDs]
FROM [xDocumentsUsers] AS x1