How to Condense SQL Rows Using Stuff? - sql

I have a table of data that looks like:
#test
RecordID
Name
hasSpanishVersion
Type
TypeID
1
Test One
Yes
FormType1
1
1
Test One
Yes
FormType2
2
3
Test Three
No
null
null
4
Test Four
Yes
FormType3
3
5
Test Five
Yes
FormType3
3
I also have another table that looks like:
#formTypes
TypeID
FormType
1
FormType1
2
FormType2
3
FormType3
What I am trying to do is condense the Type column where there are like-RecordIDs / Names. If "hasSpanishVersion" is null, the following two columns will also be null.
I am wanting the example table to look like:
RecordID
Name
hasSpanishVersion
Type
1
Test One
Yes
FormType1, FormType2
3
Test Three
null
null
4
Test Four
Yes
FormType3
5
Test Five
Yes
FormType3
I have tried the following code, but this only takes all of the FormTypes and condenses them for each of the three different types:
SELECT
*,
STUFF((SELECT '; ' + t.formTypeSpanish
FROM #test t
WHERE t.TypeID = ft.TypeID
FOR XML PATH('')), 1, 1, '') as FormTypes
FROM #formTypes ft
GROUP BY ft.TypeID, ft.FormType
ORDER BY 1

You might let your grouping column put in Where in a correlated subquery that and you concatenate value in SELECT
SELECT
RecordID, Name,hasSpanishVersion,
STUFF((SELECT ',' + tt.[Type]
FROM formTypes tt
WHERE
tt.RecordID = t1.RecordID AND
tt.Name = t1.Name AND
tt.hasSpanishVersion = t1.hasSpanishVersion
FOR XML PATH('')), 1, 1, '') as FormTypes
FROM formTypes t1
GROUP BY RecordID, Name,hasSpanishVersion
ORDER BY 1
if your sql-server support STRING_AGG there is another simple way to do that.
SELECT RecordID, Name,hasSpanishVersion,STRING_AGG([Type] ,',')
FROM formTypes
GROUP BY RecordID, Name,hasSpanishVersion
sqlfiddle

Related

SQL Select, is it possible to merge specific dupe rows into 1 based on a key?

I'm a little stumped on this. I have a table that looks like the following:
Group_Key Trigger_Type Event_Type Result_Id
1 A A 1
2 B B 2
3 C C 3
3 C C 4
4 E E 5
5 F F 6
5 F F 7
There are rows that will have the same survey (all columns should be the same aside from result_id) key but they will have a different result_Id. Is it possible to do a select on the table that grabs the rows and instead of returning 2 rows because of the result_id, it groups those ones that have dupes into a single row with the result_id being a concatenated string? So for instance, return this:
Group_Key Trigger_Type Event_Type Result_Id
1 A A 1
2 B B 2
3 C C 3,4
4 E E 5
5 F F 6,7
Is this possible?
Thank you,
Here's an example using a recursive CTE to replicate the functionality of string_agg(). This example is from the upsert scripts for execsql, and was written by Elizabeth Shea. It will have to be modified for your particular use, substituting your own column names for the execsql variable references.
if object_id('tempdb..#agg_string') is not null drop table #agg_string;
with enum as
(
select
cast(!!#string_col!! as varchar(max)) as agg_string,
row_number() over (order by !!#order_col!!) as row_num
from
!!#table_name!!
),
agg as
(
select
one.agg_string,
one.row_num
from
enum as one
where
one.row_num=1
UNION ALL
select
agg.agg_string + '!!#delimiter!!' + enum.agg_string as agg_string,
enum.row_num
from
agg, enum
where
enum.row_num=agg.row_num+1
)
select
agg_string
into #agg_string
from agg
where row_num=(select max(row_num) from agg);
Using Gordon Linoff's hint you can GROUP BY the values that should be the same and concatenate the other values in one row using STRING_AGG:
SELECT Group_Key, Trigger_Type, Event_Type, STRING_AGG(Result_Id, ',') as ResultId
FROM myTable
GROUP BY Group_Key, Trigger_Type, Event_Type
I would add the ordering of values to MicSim's solutions as follows:
select Group_Key, Trigger_Type, Event_Type,
string_agg(Result_Id, ',') within group (order by Result_Id)
from survey
group by Group_Key, Trigger_Type, Event_Type
MS-SQL Server will not recognise STRING_AGG function. Try stuff() as shown below:
SELECT Group_Key ,Trigger_Type, Event_Type,
STUFF(
(SELECT CONCAT( Result_Id , ', ') AS [text()]
FROM [dbo].[TestTable] t2
WHERE t1.Group_Key = t2.Group_Key
AND t1.Trigger_Type = t2.Trigger_Type
AND t1.Event_Type= t2.Event_Type
ORDER BY Group_Key ,Trigger_Type, Event_Type
FOR XML PATH('')), 1, 0, '') AS Result_Id
FROM [dbo].[TestTable] t1
GROUP BY Group_Key ,Trigger_Type, Event_Type
Hope this helps.

Get the mentioned output by SQL query

I appeared for an interview lately. The interviewer asked me the problem.
I have 2 tables:
First table is Location like this:
ID | City
---+-----------
1 | Mumbai
2 | Delhi
3 | Bangalore
Second table is Item like this:
Item | Location_id
-----+-------------
A | 1,2
B | 2,3
C | 1,2,3
Now we want the output as below
Item | Location
-----+-------------------------
A | Mumbai,Delhi
B | Delhi,Bangalore
C | Mumbai,Delhi,Bangalore
Please help me write the query.
You can use below query . String split works on SQL server 2016 and later versions.
Select * into #temp
from #location l
join (
select item,value from #item
CROSS APPLY STRING_SPLIT(location_id, ',')
) A on l.id=a.value
select
item,
stuff((
select ',' + u.city
from #temp u
where u.item = A.item
for xml path('')
),1,1,'') as List
from #temp A
group
by item
Drop table #temp
You want to STUFF all matching results into one column:
SELECT i.item,
STUFF((SELECT ',' + l.city
FROM location l
WHERE CHARINDEX(cast(l.id AS VARCHAR(4)),i.location_id)>0
FOR XML PATH ('')), 1, 1, '')
AS Location
FROM item i;
CHARINDEX finds the first instance of a substring in a string, in this case it finds that you have the number 1 for Mumbai, 2 for Dehli etc in your string of location_id.
STUFF combines your results into a single result.
SQLFiddle
UPDATE -
As pointed out by Rajneesh, this only works because your IDs are single digit. String splitting is probably the best way to handle the possibility of such IDs. That can still be done within this one query, without the need for a temp table.
SELECT i.item,
STUFF((SELECT ',' + l.city
FROM location l
WHERE l.id IN (select value from STRING_SPLIT(i.location_id, ','))
FOR XML PATH ('')), 1, 1, '')
AS Location
FROM item i;
SQLFiddle

SQL Concatenate Distinct Values

I have the following query where I am concatenating a list of products and quotes by account and version number.
SELECT DISTINCT ST2.Account_No, ST2.version_num,
substring((SELECT ',' + ST1.ProductNo AS [text()]
FROM (SELECT DISTINCT Account_No, version_num, ProductNo, QuoteNo, RowNo
FROM uAccountProductInfo) ST1
WHERE ST1.version_num = ST2.version_num
AND ST1.Account_No = ST2.Account_No
ORDER BY ST1.RowNo, ST1.Account_No,ST1.version_num
FOR XML PATH (''))
, 2, 1000) [AllProduct]
,
substring((SELECT ','+ ST3.QuoteNo AS [text()]
FROM (SELECT DISTINCT Account_No, version_num, ProductNo, QuoteNo, RowNo
FROM [uAccountProductInfo]) ST3
WHERE ST3.version_num = ST2.version_num
AND ST3.Account_No = ST2.Account_No
ORDER BY ST3.RowNo, ST3.version_num
FOR XML PATH (''))
, 2, 1000) [AllQuote]
FROM uAccountProductInfo ST2
The problem I am experiencing is that the return values are not showing the distinct results. I understand the reason it's happening but cannot figure out how to adjust it.
Return results look this:
Account version_num AllProduct AllQuote
1 2 aaa,aaa,aaa 111,111,111
1 3 aaa,aaa,bbb 111,111,222
What I want is
Account version_num AllProduct AllQuote
1 2 aaa, 111
1 3 aaa,bbb 111,222
Test Data would be this:
Account version_num LOB Package Product Quote RowNo
1 2 GL 1 aaa 111 1
1 2 AU 1 aaa 111 2
1 2 PF 1 aaa 111 3
1 3 GL 1 aaa 111 1
1 3 AU 1 aaa 111 2
1 3 WK 0 bbb 222 3
The reason they are returning with multiple instances of the same product | quote is due to the inclusion of RowNo column. I had this excluded before which returned the distinct list of values, but I need to order by RowNo so that the values come in a specific order.
I have been wracking my brain all morning but cannot figure out how to adjust the query to only return the distinct values at the top level.
Any suggestions?
n.b. - this is part of a larger query but once this subquery is resolved it should flow into main one just fine (at least I think). I can post main query if people need.
I much prefer stuff() rather than substring() for removing the separating character. What you require, though, is select distinct or group by in the subquery:
stuff((SELECT ',' + ST1.ProductNo AS [text()]
FROM uAccountProductInfo ST1
WHERE ST1.version_num = ST2.version_num AND
ST1.Account_No = ST2.Account_No
GROUP BY ST1.ProductNo
ORDER BY MIN(ST1.RowNo)
FOR XML PATH ('')
), 1, 1, '')
Your additional subquery is superfluous. In fact, it is misleading because you are using SELECT DISTINCT and expect it to return one row per ProductNo -- even when multiple rows exist with different values in the other columns.
Note that the ordering is unclear. This bases it on the minimum RowNo.

Combining Data From Multiple Rows

I have 3 tables I am writing a query for: Memos, Memos_Description, Policies. The database was not designed by myself and I cannot change it, this is simply a report.
I currently have a query that seems to be working, but is extremely inefficient before joining the extra tables that I need.
SELECT
Main.CLIENTSNAME,
Main.ENTRYDATE,
Main.AUTHOR,
Main.POLICYNUMBER,
Main.CLIENTS_ID,
Main.MEMOS_ID,
Left(Main.DESCRIPTION,Len(Main.DESCRIPTION)) AS REGARDING
FROM
(
SELECT distinct ST1.MEMOS_ID,
(
SELECT ST2.DESCRIPTION + ' ' AS [text()]
FROM dbo.MEMOS_DESCRIPTION ST2
WHERE ST1.MEMOS_ID = ST2.MEMOS_ID
ORDER BY ST1.MEMOS_ID
For XML PATH ('')
) [DESCRIPTION],
ST1.CLIENTSNAME,
ST1.ENTRYDATE,
ST1.AUTHOR,
ST1.POLICYNUMBER,
ST1.REGARDING,
ST1.CLIENTS_ID
FROM dbo.MEMOS ST1
) [Main]
The tables look like this:
tbl.MEMOS
MEMOS_ID
POLICIES_ID
CLIENTSNAME
tbl.MEMOS_DESCRIPTION
MEMOS_ID
DESCRIPTION
tbl.POLICIES
POLICIES_ID
POLICYNUMBER
The data looks like this:
tbl1.MEMOS_ID | tbl1.CLIENTSNAME
1 PERSON ONE
2 PERSON TWO
3 PERSON THREE
tbl2.MEMOS_ID | tbl2.DESCRIPTION
1 This is a sentence
1 that can run over more
1 than one description record.
2 Person two has
2 something different.
3 Client Created.
tbl3.POLICIES_ID | tbl3.POLICYNUMBER
123 ABCDE
456 FGHIJ
I would like the report to look like:
tbl1.MEMOS_ID | tbl1.CLIENTSNAME | tbl2.DESCRIPTION | tbl3.POLICIES_ID | tbl3.POLICYNUMBER
1 PERSON ONE This is a sentence that can run over more tan one description record. 123 ABCDE
I hope this makes sense and thank you.
Updated Query as per Gordon's suggested answer:
SELECT ST1.*,
STUFF(
(SELECT ' ' + ST2.DESCRIPTION AS [text()]
FROM dbo.MEMOS_DESCRIPTION ST2
WHERE ST1.MEMOS_ID = ST2.MEMOS_ID
ORDER BY ST1.MEMOS_ID
For XML PATH ('')
), 1, 1, '') [REGARDING]
FROM
(SELECT DISTINCT ST1.MEMOS_ID,
ST1.CLIENTSNAME,
ST1.ENTRYDATE,
ST1.AUTHOR,
ST1.POLICYNUMBER,
ST1.CLIENTS_ID,
ST1.POLICIES_ID
FROM dbo.MEMOS ST1
) ST1
LEFT JOIN POLICIES B
ON ST1.POLICIES_ID = B.POLICIES_ID
WHERE ST1.ENTRYDATE >= DATEADD(month, -2, GETDATE())
AND (B.PROD1 = ('123') OR B.PROD1 = ('456') OR B.PROD1 = ('789'))
How does the performance compare if you do the select distinct before doing the string aggregation?
SELECT ST1.*,
STUFF((SELECT ' ' + ST2.DESCRIPTION AS [text()]
FROM dbo.MEMOS_DESCRIPTION ST2
WHERE ST1.MEMOS_ID = ST2.MEMOS_ID
ORDER BY ST1.MEMOS_ID
For XML PATH ('')
), 1, 1, '') ) [DESCRIPTION]
FROM (SELECT DISTINCT T1.MEMOS_ID, ST1.CLIENTSNAME, ST1.ENTRYDATE,
ST1.AUTHOR, ST1.POLICYNUMBER, ST1.REGARDING, ST1.CLIENTS_ID
FROM dbo.MEMOS ST1
) ST1
I suspect that SQL Server might be doing the string aggregation for every row before running distinct -- and that is a lot of unnecessary work.

SQL Query - Display Count & All ID's With Same Name

I'm trying to display the amount of table entries with the same name and the unique ID's associated with each of those entries.
So I have a table like so...
Table Names
------------------------------
ID Name
0 John
1 Mike
2 John
3 Mike
4 Adam
5 Mike
I would like the output to be something like:
Name | Count | IDs
---------------------
Mike 3 1,3,5
John 2 0,2
Adam 1 4
I have the following query which does this except display all the unique ID's:
select name, count(*) as ct from names group by name order by ct desc;
select name,
count(id) as ct,
group_concat(id) as IDs
from names
group by name
order by ct desc;
You can use GROUP_CONCAT for that
Depending on version of MSSQL you are using (2005+), you can use the FOR XML PATH option.
SELECT
Name,
COUNT(*) AS ct,
STUFF((SELECT ',' + CAST(ID AS varchar(MAX))
FROM names i
WHERE i.Name = n.Name FOR XML PATH(''))
, 1, 1, '') as IDs
FROM names n
GROUP BY Name
ORDER BY ct DESC
Closest thing to group_concat you'll get on MSSQL unless you use the SQLCLR option (which I have no experience doing). The STUFF function takes care of the leading comma. Also, you don't want to alias the inner SELECT as it will wrap the element you're selecting in an XML element (alias of TD causes each element to return as <TD>value</TD>).
Given the input above, here's the result I get:
Name ct IDs
Mike 3 1,3,5
John 2 0,2
Adam 1 4
EDIT: DISCLAIMER
This technique will not work as intended for string fields that could possibly contain special characters (like ampersands &, less than <, greater than >, and any number of other formatting characters). As such, this technique is most beneficial for simple integer values, although can still be used for text if you are ABSOLUTELY SURE there are no special characters that would need to be escaped. As such, read the solution posted HERE to ensure these characters get properly escaped.
Here is another SQL Server method, using recursive CTE:
Link to SQLFiddle
; with MyCTE(name,ids, name_id, seq)
as(
select name, CAST( '' AS VARCHAR(8000) ), -1, 0
from Data
group by name
union all
select d.name,
CAST( ids + CASE WHEN seq = 0 THEN '' ELSE ', ' END + cast(id as varchar) AS VARCHAR(8000) ),
CAST( id AS int),
seq + 1
from MyCTE cte
join Data d
on cte.name = d.name
where d.id > cte.name_id
)
SELECT name, ids
FROM ( SELECT name, ids,
RANK() OVER ( PARTITION BY name ORDER BY seq DESC )
FROM MyCTE ) D ( name, ids, rank )
WHERE rank = 1