Try to find explaination for [duplicate] - sql-server

Table is:
Id
Name
1
aaa
1
bbb
1
ccc
1
ddd
1
eee
Required output:
Id
abc
1
aaa,bbb,ccc,ddd,eee
Query:
SELECT ID,
abc = STUFF(
(SELECT ',' + name FROM temp1 FOR XML PATH ('')), 1, 1, ''
)
FROM temp1 GROUP BY id
This query is working properly. But I just need the explanation how it works or is there any other or short way to do this.
I am getting very confused to understand this.

Here is how it works:
1. Get XML element string with FOR XML
Adding FOR XML PATH to the end of a query allows you to output the results of the query as XML elements, with the element name contained in the PATH argument. For example, if we were to run the following statement:
SELECT ',' + name
FROM temp1
FOR XML PATH ('')
By passing in a blank string (FOR XML PATH('')), we get the following instead:
,aaa,bbb,ccc,ddd,eee
2. Remove leading comma with STUFF
The STUFF statement literally "stuffs” one string into another, replacing characters within the first string. We, however, are using it simply to remove the first character of the resultant list of values.
SELECT abc = STUFF((
SELECT ',' + NAME
FROM temp1
FOR XML PATH('')
), 1, 1, '')
FROM temp1
The parameters of STUFF are:
The string to be “stuffed” (in our case the full list of name with a
leading comma)
The location to start deleting and inserting characters (1, we’re stuffing into a blank string)
The number of characters to delete (1, being the leading comma)
So we end up with:
aaa,bbb,ccc,ddd,eee
3. Join on id to get full list
Next we just join this on the list of id in the temp table, to get a list of IDs with name:
SELECT ID, abc = STUFF(
(SELECT ',' + name
FROM temp1 t1
WHERE t1.id = t2.id
FOR XML PATH (''))
, 1, 1, '') from temp1 t2
group by id;
And we have our result:
Id
Name
1
aaa,bbb,ccc,ddd,eee

This article covers various ways of concatenating strings in SQL, including an improved version of your code which doesn't XML-encode the concatenated values.
SELECT ID, abc = STUFF
(
(
SELECT ',' + name
FROM temp1 As T2
-- You only want to combine rows for a single ID here:
WHERE T2.ID = T1.ID
ORDER BY name
FOR XML PATH (''), TYPE
).value('.', 'varchar(max)')
, 1, 1, '')
FROM temp1 As T1
GROUP BY id
To understand what's happening, start with the inner query:
SELECT ',' + name
FROM temp1 As T2
WHERE T2.ID = 42 -- Pick a random ID from the table
ORDER BY name
FOR XML PATH (''), TYPE
Because you're specifying FOR XML, you'll get a single row containing an XML fragment representing all of the rows.
Because you haven't specified a column alias for the first column, each row would be wrapped in an XML element with the name specified in brackets after the FOR XML PATH. For example, if you had FOR XML PATH ('X'), you'd get an XML document that looked like:
<X>,aaa</X>
<X>,bbb</X>
...
But, since you haven't specified an element name, you just get a list of values:
,aaa,bbb,...
The .value('.', 'varchar(max)') simply retrieves the value from the resulting XML fragment, without XML-encoding any "special" characters. You now have a string that looks like:
',aaa,bbb,...'
The STUFF function then removes the leading comma, giving you a final result that looks like:
'aaa,bbb,...'
It looks quite confusing at first glance, but it does tend to perform quite well compared to some of the other options.

PATH mode is used in generating XML from a SELECT query
1. SELECT
ID,
Name
FROM temp1
FOR XML PATH;
Ouput:
<row>
<ID>1</ID>
<Name>aaa</Name>
</row>
<row>
<ID>1</ID>
<Name>bbb</Name>
</row>
<row>
<ID>1</ID>
<Name>ccc</Name>
</row>
<row>
<ID>1</ID>
<Name>ddd</Name>
</row>
<row>
<ID>1</ID>
<Name>eee</Name>
</row>
The Output is element-centric XML where each column value in the resulting rowset is wrapped in an row element. Because the SELECT clause does not specify any aliases for the column names, the child element names generated are the same as the corresponding column names in the SELECT clause.
For each row in the rowset a tag is added.
2.
SELECT
ID,
Name
FROM temp1
FOR XML PATH('');
Ouput:
<ID>1</ID>
<Name>aaa</Name>
<ID>1</ID>
<Name>bbb</Name>
<ID>1</ID>
<Name>ccc</Name>
<ID>1</ID>
<Name>ddd</Name>
<ID>1</ID>
<Name>eee</Name>
For Step 2: If you specify a zero-length string, the wrapping element is not produced.
3.
SELECT
Name
FROM temp1
FOR XML PATH('');
Ouput:
<Name>aaa</Name>
<Name>bbb</Name>
<Name>ccc</Name>
<Name>ddd</Name>
<Name>eee</Name>
4. SELECT
',' +Name
FROM temp1
FOR XML PATH('')
Ouput:
,aaa,bbb,ccc,ddd,eee
In Step 4 we are concatenating the values.
5. SELECT ID,
abc = (SELECT
',' +Name
FROM temp1
FOR XML PATH('') )
FROM temp1
Ouput:
1 ,aaa,bbb,ccc,ddd,eee
1 ,aaa,bbb,ccc,ddd,eee
1 ,aaa,bbb,ccc,ddd,eee
1 ,aaa,bbb,ccc,ddd,eee
1 ,aaa,bbb,ccc,ddd,eee
6. SELECT ID,
abc = (SELECT
',' +Name
FROM temp1
FOR XML PATH('') )
FROM temp1 GROUP by iD
Ouput:
ID abc
1 ,aaa,bbb,ccc,ddd,eee
In Step 6 we are grouping the date by ID.
STUFF( source_string, start, length, add_string )
Parameters or Arguments
source_string
The source string to modify.
start
The position in the source_string to delete length characters and then insert add_string.
length
The number of characters to delete from source_string.
add_string
The sequence of characters to insert into the source_string at the start position.
SELECT ID,
abc =
STUFF (
(SELECT
',' +Name
FROM temp1
FOR XML PATH('')), 1, 1, ''
)
FROM temp1 GROUP by iD
Output:
-----------------------------------
| Id | Name |
|---------------------------------|
| 1 | aaa,bbb,ccc,ddd,eee |
-----------------------------------

There is very new functionality in Azure SQL Database and SQL Server (starting with 2017) to handle this exact scenario. I believe this would serve as a native official method for what you are trying to accomplish with the XML/STUFF method. Example:
select id, STRING_AGG(name, ',') as abc
from temp1
group by id
STRING_AGG - https://msdn.microsoft.com/en-us/library/mt790580.aspx
EDIT: When I originally posted this I made mention of SQL Server 2016 as I thought I saw that on a potential feature that was to be included. Either I remembered that incorrectly or something changed, thanks for the suggested edit fixing the version. Also, pretty impressed and wasn't fully aware of the multi-step review process that just pulled me in for a final option.

In for xml path, if we define any value like [ for xml path('ENVLOPE') ] then these tags will be added with each row:
<ENVLOPE>
</ENVLOPE>

SELECT ID,
abc = STUFF(
(SELECT ',' + name FROM temp1 FOR XML PATH ('')), 1, 1, ''
)
FROM temp1 GROUP BY id
Here in the above query STUFF function is used to just remove the first comma (,) from the generated xml string (,aaa,bbb,ccc,ddd,eee) then it will become (aaa,bbb,ccc,ddd,eee).
And FOR XML PATH('') simply converts column data into (,aaa,bbb,ccc,ddd,eee) string but in PATH we are passing '' so it will not create a XML tag.
And at the end we have grouped records using ID column.

I did debugging and finally returned my 'stuffed' query to it it's normal way.
Simply
select * from myTable for xml path('myTable')
gives me contents of the table to write to a log table from a trigger I debug.

Declare #Temp As Table (Id Int,Name Varchar(100))
Insert Into #Temp values(1,'A'),(1,'B'),(1,'C'),(2,'D'),(2,'E'),(3,'F'),(3,'G'),(3,'H'),(4,'I'),(5,'J'),(5,'K')
Select X.ID,
stuff((Select ','+ Z.Name from #Temp Z Where X.Id =Z.Id For XML Path('')),1,1,'')
from #Temp X
Group by X.ID

Related

Split XML field into multiple delimited values - SQL

I have some XML content in a single field; I want to split each xml field in multiple rows.
The XML is something like that:
<env>
<id>id1<\id>
<DailyProperties>
<date>01/01/2022<\date>
<value>1<\value>
<\DailyProperties>
<DailyProperties>
<date>05/05/2022<\date>
<value>2<\value>
<\DailyProperties>
<\env>
I want to put everything in a table as:
ID DATE VALUE
id1 01/01/2022 1
id1 05/05/2022 2
For now I managed to parse the xml value, and I have found something online to get a string into multiple rows (like this), but my string should have some kind of delimiter. I did this:
SELECT
ID,
XMLDATA.X.query('/env/DailyProperties/date').value('.', 'varchar(100)') as r_date,
XMLDATA.X.query('/env/DailyProperties/value').value('.', 'varchar(100)') as r_value
from tableX
outer apply xmlData.nodes('.') as XMLDATA(X)
WHERE ID = 'id1'
but I get all values without a delimiter, as such:
01/10/202202/10/202203/10/202204/10/202205/10/202206/10/202207/10/202208/10/202209/10/202210/10/2022
Or, as in my example:
ID R_DATE R_VALUE
id01 01/01/202205/05/2022 12
I have found out that XQuery has a last() function that return the last value parsed; in my xml example it will return only 05/05/2022, so it should exists something for address the adding of a delimiter. The number of rows could vary, as it could vary the number of days of which I have a value.
Please try the following solution.
I had to fix your XML to make it well-formed.
SQL
DECLARE #tbl TABLE (id INT IDENTITY PRIMARY KEY, xmldata XML);
INSERT INTO #tbl (xmldata) VALUES
(N'<env>
<id>id1</id>
<DailyProperties>
<date>01/01/2022</date>
<value>1</value>
</DailyProperties>
<DailyProperties>
<date>05/05/2022</date>
<value>2</value>
</DailyProperties>
</env>');
SELECT p.value('(id/text())[1]','VARCHAR(20)') AS id
, c.value('(date/text())[1]','VARCHAR(10)') AS [date]
, c.value('(value/text())[1]','INT') AS [value]
FROM #tbl
CROSS APPLY xmldata.nodes('/env') AS t1(p)
OUTER APPLY t1.p.nodes('DailyProperties') AS t2(c);
Output
id
date
value
id1
01/01/2022
1
id1
05/05/2022
2
Yitzhak beat me to it by 2 min. Nonetheless, here's what I have:
--==== XML Data:
DECLARE #xml XML =
'<env>
<id>id1</id>
<DailyProperties>
<date>01/01/2022</date>
<value>1</value>
</DailyProperties>
<DailyProperties>
<date>05/05/2022</date>
<value>2</value>
</DailyProperties>
</env>';
--==== Solution:
SELECT
ID = ff2.xx.value('(text())[1]','varchar(20)'),
[Date] = ff.xx.value('(date/text())[1]', 'date'),
[Value] = ff.xx.value('(value/text())[1]', 'int')
FROM (VALUES(#xml)) AS f(X)
CROSS APPLY f.X.nodes('env/DailyProperties') AS ff(xx)
CROSS APPLY f.X.nodes('env/id') AS ff2(xx);
Returns:
ID Date Value
-------------------- ---------- -----------
id1 2022-01-01 1
id1 2022-05-05 2

Substitute for STRING_AGG pre SQL Server 2016

I need to group a table by a set of values together with all matching row numbers/id:s for each set. This operation must be done within the boundaries of SQL Server 2016.
Let's suppose I have the following table (Places):
ID
Country
City
1
Sweden
Stockholm
2
Norway
Oslo
3
Iceland
Reykjavik
4
Sweden
Stockholm
The result that I'm after (No curly-brackets because Stack Overflow thinks it's code, preventing me from posting):
ID
Json
1,4
"Country":"Sweden","City":"Stockholm"
2
"Country":"Norway ","City":"Oslo"
3
"Country":"Iceland ","City":"Reykjavik"
In SQL Server 2017 the above result can be achieved with:
SELECT STRING_AGG(ID) ID, (SELECT Country, City FOR JSON PATH) Json
FROM Places GROUP BY Country, City
I managed to get a similar result in SQL Server 2016 with the code below. (But with my actual amount of data and columns, this solution is too slow.)
SELECT DISTINCT Country, City INTO #temp FROM Places
SELECT (SELECT ID From Places WHERE Country = P.Country AND City = P.City FOR JSON PATH) ID,
(SELECT Country, City FOR JSON Path) Json FROM #temp P
Is there any more performance-effective way of achieving the result that I'm after?
EDIT: As people suggested me to try "FOR XML Path" I tried the code below. This gives the following error "Places.ID is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause ":
SELECT stuff((select ',' + cast(ID as varchar(max)) for xml path ('')), 1, 1, '') ID,
(SELECT Country, City FOR JSON PATH) Json
FROM Places GROUP BY Country, City
Here's a solution you can try with for xml path
Basically select and group the json columns needed and using an apply, use the for xml path solution to aggregate the correlated ID values; because the outer query needs to refer to the output of the apply it needs to be aggregated also, I chose to use max
select max(x.Ids), (select country,city for json path) as [Json]
from t
outer apply (
select Stuff((select ',' + Convert(varchar(10),t2.Id)
from t t2
where t2.city=t.city and t2.country=t.country
for xml path(''),type).value('(./text())[1]','varchar(10)'),1,1,'') as Ids
)x
group by country,city
Working Fiddle
Here is another possible solution:
Declare #testTable Table (ID int, Country varchar(30), City varchar(30));
Insert Into #testTable (ID, Country, City)
Values (1, 'Sweden', 'Stockholm')
, (2, 'Normway', 'Oslo')
, (3, 'Iceland', 'Reykjavik')
, (4, 'Sweden', 'Stockholm');
Select Distinct
ID = stuff((Select concat(',', tt2.ID)
From #testTable tt2
Where tt2.City = tt.City
And tt2.Country = tt.Country
For xml path (''), Type).value('.', 'varchar(10)'), 1, 1, '')
, json = (Select Country, City For JSON PATH)
From #testTable tt;
No idea if this will perform any better though. It is essentially the same - just using DISTINCT instead of GROUP BY.

Unable to use XML PATH with UNION ALL in SQL Server

I have used below code to generate Comma separate values, it was before perfect working without UNION ALL but once I apply UNION ALL with another SELECT then it is showing an XML like output. The query looks like:
SELECT ISNULL(STUFF((
SELECT plant from (
SELECT DISTINCT ', ' + Plant as Plant
FROM ASP_MstSKUPlantMapping SKUPM
WHERE SKUID = '702953' AND Plant LIKE 'P%'
UNION ALL
SELECT DISTINCT ', ' + Plant as Plant
FROM ASP_MstSKUPlantMapping SKUPM
WHERE SKUID = '702953' AND Plant LIKE 'S%'
) Temp
FOR XML PATH('')), 1, 2,''),'')
Current Output:
lant>, PN05</plant><plant>, PN10</plant>
Expected Output:
PN05, PN10
EDIT: If I removed as Plant this alias name then it showing an error as:
No column name was specified for column 1 of 'Temp'.
I don't think you need to use a UNION ALL for this.
It's from the same table, so it's just something for the WHERE clause to deal with.
SELECT
ISNULL(STUFF((
SELECT CONCAT(', ', Plant)
FROM ASP_MstSKUPlantMapping SKUPM
WHERE SKUID = '702953' AND Plant LIKE '[PS]%'
GROUP BY Plant
ORDER BY Plant
FOR XML PATH('')
), 1, 2, ''),'') AS [Plants];

Generate XML in SQL Server

I have a table like this -
Version itemid sampleid
--------------------------------
1 3 23
1 3 24
1 4 45
2 5 24
2 5 23
Where for each version there can be multiple itemid, and for each itemid there can be multiple sampleid.
I want to generate XML for this table in the following manner
<UserVersioningHistory>
<History>
<Version>1</Version>
<itemid>3</itemid>
<sampleid>23,24</sampleid>
</History>
<History>
<Version>1</Version>
<itemid>4</itemid>
<sampleid>45</sampleid>
</History>
<History>
<Version>2</Version>
<BusinessId>5</BusinessId>
<sampleid>24,23</sampleid>
</History>
</UserVersioningHistory>
Each node here can have only one version and itemid but can have multiple sampleid for corresponding itemid and Version Pair.
As I am not familiar in with generating XML in SQL Server, can someone give me a hint to what approach I should use?
Can I accomplish this task using while loop, or I should do this writing a subquery?
Try it out:
select * from
(SELECT
version, itemid,
STUFF(
(SELECT ',' + sampleid
FROM test
WHERE version = a.version AND itemid = a.itemid
FOR XML PATH (''))
, 1, 1, '') AS sampleid
FROM test AS a
GROUP BY version, itemid) as History
for xml auto, root ('UserVersioningHistory')
Always avoid WHILE loops unless when truly necessary.
Preferably you would have multiple tags for the SampleId section if more than one exists.
But to give you the result that you want the following would work. (I created a temp table to imitate your situation.)
I used FOR XML to do the XML formatting and had to use a second FOR XML to concatenate the SampleId sepparated by a comma.
STUFF is only used to remove the first comma in the string.
SET XACT_ABORT ON;
BEGIN TRANSACTION;
SELECT
X.Version,
X.ItemId,
X.SampleId
INTO
#Temp
FROM
(VALUES
(1, 3, 23),
(1, 3, 24),
(1, 4, 45),
(2, 5, 24),
(2, 5, 23)
) X ([Version], ItemId, SampleId)
SELECT
T.Version,
T.ItemId,
STUFF((
SELECT
',' + CONVERT(varchar(MAX), T2.SampleId)
FROM
#Temp T2
WHERE
T2.Version = T.Version AND
T2.ItemId = T.ItemId
FOR XML PATH ('')
),
1,
1,
''
) AS [SampleId]
FROM
#Temp T
GROUP BY
T.Version,
T.ItemId
FOR XML RAW ('History'), ROOT ('UserVersioningHistory'), ELEMENTS
ROLLBACK TRANSACTION;

T-SQL concatenate rows into string

I'm trying to use a sub query on a select statement for a field value but I can't seem to figure out the correct syntax. I want to pull a list of company names and as a field for that query, I want to select all the employees for that company.
Any ideas on what I'm doing wrong? The error I get is
Only one expression can be specified in the select list when the subquery is not introduced with EXISTS
T-SQL code:
SELECT
company_name,
company_type,
(SELECT
employee_firstname, employee_lastname
FROM
tblemployees
WHERE
tblemployees.company_id = tblCompanies.company_id) as employees
FROM
tblCompanies
Desired output:
Company Name | Company Type | Employees
----------------------------------------------------------
Test Co | Construction | Bob Smith, Jack Smith, etc
You'll need to concatenate the first and last names using FOR XML PATH or a similar solution. More details on the various methods here.
SELECT DISTINCT
c1.company_name,
c1.company_type,
STUFF((SELECT
', ' + c2.employee_firstname + ' ' + c2.employee_lastname
FROM
tblCompanies c2
WHERE
c1.company_id = c2.company_id
ORDER BY
employee_lastname, employee_firstname
FOR XML PATH(''), TYPE).value('.', 'varchar(max)'), 1, 1, '')
FROM tblCompanies c1
SQL Fiddle

Resources