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.
I want something like:
Particulars Info
Abc 1,2,3
instead of :
Particulars Info
Abc 1
Abc 2
Abc 3
you can use groupconcat
Reference given below::
https://www.w3resource.com/mysql/aggregate-functions-and-grouping/aggregate-functions-and-grouping-group_concat.php
this is the solution for sql-server
select Particulars, stuff((
select ',' + cast(Info as varchar(20))
from YourTable b
where a.Particulars = b.Particulars
for xml path('')), 1, 1, '') as Info
from YourTable a group by Particulars
If you are using SQL Server 2017, you can use STRING_AGG to get your expected result.
SELECT Particulars, STRING_AGG (Info, ',') AS Info
FROM TableName
GROUP BY Particulars;
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
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
I'm trying to extract some data from a third party system which uses an SQL Server database. The DB structure looks something like this:
Order
OrderID OrderNumber
1 OX101
2 OX102
OrderItem
OrderItemID OrderID OptionCodes
1 1 12,14,15
2 1 14
3 2 15
Option
OptionID Description
12 Batteries
14 Gift wrap
15 Case
[etc.]
What I want is one row per order item that includes a concatenated field with each option description. So something like this:
OrderItemID OrderNumber Options
1 OX101 Batteries\nGift Wrap\nCase
2 OX101 Gift Wrap
3 OX102 Case
Of course this is complicated by the fact that the options are a comma separated string field instead of a proper lookup table. So I need to split this up by comma in order to join in the options table, and then concat the result back into one field.
At first I tried creating a function which splits out the option data by comma and returns this as a table. Although I was able to join the result of this function with the options table, I wasn't able to pass the OptionCodes column to the function in the join, as it only seemed to work with declared variables or hard-coded values.
Can someone point me in the right direction?
I would use a splitting function (here's an example) to get individual values and keep them in a CTE. Then you can join the CTE to your table called "Option".
SELECT * INTO #Order
FROM (
SELECT 1 OrderID, 'OX101' OrderNumber UNION SELECT 2, 'OX102'
) X;
SELECT * INTO #OrderItem
FROM (
SELECT 1 OrderItemID, 1 OrderID, '12,14,15' OptionCodes
UNION
SELECT 2, 1, '14'
UNION
SELECT 3, 2, '15'
) X;
SELECT * INTO #Option
FROM (
SELECT 12 OptionID, 'Batteries' Description
UNION
SELECT 14, 'Gift Wrap'
UNION
SELECT 15, 'Case'
) X;
WITH N AS (
SELECT I.OrderID, I.OrderItemID, X.items OptionCode
FROM #OrderItem I CROSS APPLY dbo.Split(OptionCodes, ',') X
)
SELECT Q.OrderItemID, Q.OrderNumber,
CONVERT(NVarChar(1000), (
SELECT T.Description + ','
FROM N INNER JOIN #Option T ON N.OptionCode = T.OptionID
WHERE N.OrderItemID = Q.OrderItemID
FOR XML PATH(''))
) Options
FROM (
SELECT N.OrderItemID, O.OrderNumber
FROM #Order O INNER JOIN N ON O.OrderID = N.OrderID
GROUP BY N.OrderItemID, O.OrderNumber) Q
DROP TABLE #Order;
DROP TABLE #OrderItem;
DROP TABLE #Option;