Single Column as N Rows [duplicate] - sql

This question already has answers here:
Efficiently convert rows to columns in sql server
(5 answers)
Closed 7 years ago.
I have a large table with 3 columns as follows:
Invoice Product Color
1 Pant Red
1 Pant Black
1 Shirt Green
2 Pant White
2 Pant Black
2 Pant Blue
I'd like to group on Invoice & Product and then have all unique Color values appear on the related grouped record as follows:
Invoice Product Colour1 Colour2 Colour3
1 Pant Red Black
1 Shirt Green
2 Pant White Black Blue
Is this possible in SQL Server?

It is possible in SQL Server -- if you know that there are three color columns. If there are a variable number, then it is still possible, but it requires dynamic SQL.
I would approach this using conditional aggregation:
select invoice, product,
max(case when seqnum = 1 then colour end) as colour1,
max(case when seqnum = 2 then colour end) as colour2,
max(case when seqnum = 3 then colour end) as colour3
from (select t.*,
row_number() over (partition by invoice, product order by (select nULL)) as seqnum
from table t
) t
group by invoice, product;

To convert rows into columns, you need to use Pivot in Sql Server. If you know the number of columns in advance, you can use pivoting statically as the answer suggested by Gordin Linoff.
Sometimes, the number of colors may vary(in your example there are only 3 colors). In such case, you cannot hardcode the column names. For that first of all you need to get columns names dynamically into a variable.
DECLARE #cols NVARCHAR (MAX)
SELECT #cols = COALESCE (#cols + ',[' + COLUMNNAME + ']', '[' + COLUMNNAME + ']')
FROM
(
SELECT DISTINCT
'COLOR'+CAST(ROW_NUMBER() OVER(PARTITION BY INVOICE,PRODUCT ORDER BY (SELECT 0)) AS VARCHAR(10)) COLUMNNAME
FROM #TEMP
) PV
ORDER BY COLUMNNAME
Now the above variable have values of columns as Comma Separated Values which can be used with IN operator dynamically for the below query. Since your table doesn't have values like COLOR1, COLOR2 etc, I have provided logic to get column names for each INVOICE and its PRODUCT using PARTITION BY clause.
DECLARE #query NVARCHAR(MAX)
SET #query = '-- This outer query forms your pivoted result
SELECT * FROM
(
-- Source data for pivoting
SELECT DISTINCT INVOICE,PRODUCT,COLOR,
''COLOR''+CAST(ROW_NUMBER() OVER(PARTITION BY INVOICE,PRODUCT ORDER BY (SELECT 0)) AS VARCHAR(10)) COLUMNNAME
FROM #TEMP
) x
PIVOT
(
--Defines the values in each dynamic columns
MIN(COLOR)
-- Get the names from the #cols variable to show as column
FOR COLUMNNAME IN (' + #cols + ')
) p
ORDER BY INVOICE;'
EXEC SP_EXECUTESQL #query
Click here to view result

Related

How to Condense SQL Rows Using Stuff?

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

more efficiently pivot rows

I am trying to join multiple tables together. One of the tables I am trying to join has hundreds of rows per ID of data. I am trying to pivot about 100 rows for each ID into columns. The value I am trying to use isn't always in the same row. Below is an example (my real table has hundreds of rows per ID). AccNum for example in ID 1 may be in the NumV column, but for ID 2 it may be in the CharV column.
ID QType CharV NumV
1 AccNum 10
1 EmpNam John Inc 0
1 UW Josh 0
2 AccNum 11
2 EmpNam CBS 0
2 UW Dan 0
The original code I used was a select statement with hundreds of lines like one below:
Max(Case When PM.[QType] = 'AccNum' Then NumV End) as AccNum
This code with hundreds on lines completed in just under 10 min. The problem however is that in only pulls in values from the column I specify, so I will always loss the data that is in a different column. (In the example above I would get AccNum 10, but not AccNum11 because it's in the CharV column).
I updated the code to use a pivot:
;with CTE
As
(
Select [PMID], [QType],
Value=concat(Nullif([CharV],''''),Nullif([NumV],0))
From [DBase].[dbo].[PM]
)
Select C.[ID] AS M_ID
,Max(c.[AccNum]) As AcctNum
,Max(c.[EmpNam]) As EmpName
and so on...
I then select all of my hundreds of rows and then pivot it the data:
from CTE
pivot (max(Value) for [QType] in ([AccNum],[EmpNam],(more rows)))As c
The problem with this code, however, is that it takes almost 2 hours to run.
Is there a different, more efficient solution to what I am trying to accomplish? I need to have the speed of the first code, but the result of the second.
Perhaps you can reduce the Concat/NullIf processing by using a UNION ALL
Select ID,QType,Value=CharV From #YourTable where CharV>''
Union All
Select ID,QType,Value=cast(NumV as varchar(25)) From #YourTable where NumV>0
For the conditional aggregation approach
No need to worry about which field, just reference VALUE
Select [ID]
,[Accnum] = Max(Case When [QType] = 'AccNum' Then Value End)
,[EmpNam] = Max(Case When [QType] = 'EmpNam' Then Value End)
,[UW] = Max(Case When [QType] = 'UW' Then Value End)
From (
Select ID,QType,Value=CharV From #YourTable where CharV>''
Union All
Select ID,QType,Value=cast(NumV as varchar(25)) From #YourTable where NumV>0
) A
Group By ID
For the PIVOT approach
Select [ID],[AccNum],[EmpNam],[UW]
From (
Select ID,QType,Value=CharV From #YourTable where CharV>''
Union All
Select ID,QType,Value=cast(NumV as varchar(25)) From #YourTable where NumV>0
) A
Pivot (max([Value]) For [QType] in ([AccNum],[EmpNam],[UW])) p

How do I aggregate numbers from a string column in SQL

I am dealing with a poorly designed database column which has values like this
ID cid Score
1 1 3 out of 3
2 1 1 out of 5
3 2 3 out of 6
4 3 7 out of 10
I want the aggregate sum and percentage of Score column grouped on cid like this
cid sum percentage
1 4 out of 8 50
2 3 out of 6 50
3 7 out of 10 70
How do I do this?
You can try this way :
select
t.cid
, cast(sum(s.a) as varchar(5)) +
' out of ' +
cast(sum(s.b) as varchar(5)) as sum
, ((cast(sum(s.a) as decimal))/sum(s.b))*100 as percentage
from MyTable t
inner join
(select
id
, cast(substring(score,0,2) as Int) a
, cast(substring(score,charindex('out of', score)+7,len(score)) as int) b
from MyTable
) s on s.id = t.id
group by t.cid
[SQLFiddle Demo]
Redesign the table, but on-the-fly as a CTE. Here's a solution that's not as short as you could make it, but that takes advantage of the handy SQL Server function PARSENAME. You may need to tweak the percentage calculation if you want to truncate rather than round, or if you want it to be a decimal value, not an int.
In this or most any solution, you have to count on the column values for Score to be in the very specific format you show. If you have the slightest doubt, you should run some other checks so you don't miss or misinterpret anything.
with
P(ID, cid, Score2Parse) as (
select
ID,
cid,
replace(Score,space(1),'.')
from scores
),
S(ID,cid,pts,tot) as (
select
ID,
cid,
cast(parsename(Score2Parse,4) as int),
cast(parsename(Score2Parse,1) as int)
from P
)
select
cid, cast(round(100e0*sum(pts)/sum(tot),0) as int) as percentage
from S
group by cid;

SQL: Lookup table rows into columns for reporting purposes

I have the following two table data structure for dealing with custom user fields:
[UserFieldID] [UserFieldName]
-------------------------------
1 Location
2 Color
[UserID] [UserFieldID] [UserFieldValue]
----------------------------------------
1 1 Home
1 2 Orange
2 1 Office
2 2 Red
This allows any number of fields to be defined (globally) and users to have values for those custom fields. I need to figure out how to display this information for reporting purposes as part of a pre-existing report, in the following format:
UserID ... Location Color
----------------------------------------------------
1 Home Orange
2 Office Red
I know this probably involves using either PIVOT or UNPIVOT, but try as I might, they just confuse me.
Thanks in advance
There are several different ways that you can get the result, you can use an aggregate function with a CASE expression or you can use the PIVOT function to get this. Based on your comment that any number of fields can be defined, it sounds like you will need to use dynamic SQL to get the final result. Before writing a dynamic SQL version, I would always start with a static or hard-coded version of the query, then convert it to dynamic SQL.
Besides using these methods, I would also recommend using the windowing function row_number() to generate a unique value for each combination of userid and fieldname. Since you are pivoting string values, then you have to use either the max/min aggregate function which will return only one value for each fieldname, by adding the row_number you will be able to return multiple combinations of Location, etc for each user.
If you were using an aggregate function with a CASE expression the query would be:
select
userid,
max(case when userfieldname = 'Location' then userfieldvalue end) location,
max(case when userfieldname = 'Color' then userfieldvalue end) Color
from
(
select v.userid,
f.userfieldname,
v.userfieldvalue,
row_number() over(partition by v.userid, v.userfieldid
order by v.userfieldid) seq
from userFields f
left join userValues v
on f.userfieldId = v.userFieldId
) d
group by userid, seq
order by userid;
See SQL Fiddle with Demo
If you were using PIVOT, the hard-coded version of the query would be:
select userid, Location, Color
from
(
select v.userid,
f.userfieldname,
v.userfieldvalue,
row_number() over(partition by v.userid, v.userfieldid
order by v.userfieldid) seq
from userFields f
left join userValues v
on f.userfieldId = v.userFieldId
) d
pivot
(
max(userfieldvalue)
for userfieldname in (Location, Color)
) p
order by userid;
See SQL Fiddle with Demo.
Once you have the correct logic you can convert the PIVOT to dynamic SQL to be executed:
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX)
select #cols = STUFF((SELECT ',' + QUOTENAME(UserFieldName)
from UserFields
group by UserFieldName, userfieldId
order by userfieldid
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = 'SELECT userid, ' + #cols + '
from
(
select v.userid,
f.userfieldname,
v.userfieldvalue,
row_number() over(partition by v.userid, v.userfieldid
order by v.userfieldid) seq
from userFields f
left join userValues v
on f.userfieldId = v.userFieldId
) x
pivot
(
max(userfieldvalue)
for userfieldname in (' + #cols + ')
) p
order by userid'
execute sp_executesql #query;
See SQL Fiddle with Demo. All versions will give a result:
| USERID | LOCATION | COLOR |
|--------|----------|--------|
| 1 | Home | Orange |
| 1 | Office | (null) |
| 2 | Office | Red |

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