I have a table that holds details of activities carried out by individuals - contents of this table is similar to the following:
| Person | Category | Activity |
--------------------------------------
| Username1 | X | X1 |
| Username1 | Y | Y1 |
| Username1 | Z | Z1 |
I need a SQL query that can produce something like the following and any help would be appreciated:
| Person | Cat1 | Cat1_Act|Cat2 | Cat2_Act| Cat3 | Cat3_Act |
---------------------------------------------------------------
| Username1 | X | X1 | Y | Y1 | Z | Z1 |
I understand reading through a number of posts that PIVOT can be used to achieve this but I have not been to find a solution close to what I need as most solutions are often to use values e.g 'X', 'Y', 'Z' (in my example table) as table headers but I want to ideally be able to specify name for the table headers holding the new columns (Hope this all makes sense and someone can help :-) )
There are several ways that you can get the desired result. If you have a limited number of values that you want to PIVOT into columns, then you can hard-code the query a few different ways.
Aggregate function with CASE:
select
person,
max(case when seq = 1 then category end) Cat1,
max(case when seq = 1 then activity end) Cat1_Act,
max(case when seq = 2 then category end) Cat2,
max(case when seq = 2 then activity end) Cat2_Act,
max(case when seq = 3 then category end) Cat3,
max(case when seq = 3 then activity end) Cat3_Act
from
(
select person, category, activity,
row_number() over(partition by person
order by category) seq
from yourtable
) d
group by person;
See SQL Fiddle with Demo. By assigning a sequence or row_number to each category per user, you can use this row number to convert the rows into columns.
Static PIVOT:
If you want to apply the PIVOT function, then I would first suggest unpivoting the category and activity columns into multiple rows and then apply the pivot function.
;with cte as
(
select person, category, activity,
row_number() over(partition by person
order by category) seq
from yourtable
)
select person,
cat1, cat1_act,
cat2, cat2_act,
cat3, cat3_act
from
(
select t.person,
col = case
when c.col = 'cat' then col+cast(seq as varchar(10))
else 'cat'+cast(seq as varchar(10))+'_'+col
end,
value
from cte t
cross apply
(
select 'cat', category union all
select 'act', activity
) c (col, value)
) d
pivot
(
max(value)
for col in (cat1, cat1_act, cat2, cat2_act,
cat3, cat3_act)
) piv;
See SQL Fiddle with Demo
Dynamic PIVOT: Finally if you have an unknown number of values then you can use dynamic SQL to get the result:
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX)
select #cols = STUFF((SELECT ','
+ QUOTENAME(case
when d.col = 'cat' then col+cast(seq as varchar(10))
else 'cat'+cast(seq as varchar(10))+'_'+col end)
from
(
select row_number() over(partition by person
order by category) seq
from yourtable
) t
cross apply
(
select 'cat', 1 union all
select 'act', 2
) d (col, so)
group by col, so, seq
order by seq, so
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = 'SELECT person, ' + #cols + '
from
(
select t.person,
col = case
when c.col = ''cat'' then col+cast(seq as varchar(10))
else ''cat''+cast(seq as varchar(10))+''_''+col
end,
value
from
(
select person, category, activity,
row_number() over(partition by person
order by category) seq
from yourtable
) t
cross apply
(
select ''cat'', category union all
select ''act'', activity
) c (col, value)
) x
pivot
(
max(value)
for col in (' + #cols + ')
) p '
execute sp_executesql #query;
See SQL Fiddle with Demo. All versions give a result:
| PERSON | CAT1 | CAT1_ACT | CAT2 | CAT2_ACT | CAT3 | CAT3_ACT |
| Username1 | X | X1 | Y | Y1 | Z | Z1 |
this is a simple example
SELECT
Person,
MAX(CASE Category WHEN 'X' THEN Activity ELSE 0 END) AS 'X'
MAX(CASE Category WHEN 'Y' THEN Activity ELSE 0 END) AS 'Y'
MAX(CASE Category WHEN 'Z' THEN Activity ELSE 0 END) AS 'Z'
FROM mytable
GROUP BY Person
Related
i have a problem with approach how to pivot this table:
How can i convert this:
Would you be kind enough give me hints (not whole code) how to do this? i really dont know where should i start (i dont know whether it is typical pivoting unpivoting)
+------+------+--------+----------+-----------+-----+
| ColI | Col2 | Month | Turnover | Provision | Fee |
+------+------+--------+----------+-----------+-----+
| 123 | Asdf | 201810 | 10000 | 100 | 0,1 |
| 123 | Asdf | 201811 | 20000 | 200 | 0,2 |
| 123 | Asdf | 201812 | 30000 | 300 | 0,3 |
+------+------+--------+----------+-----------+-----+
into this:
+------+------+---------------+-----------------+------------+---------------+-----------------+------------+---------------+-----------------+-----------+
| ColI | Col2 | Turnover20810 | Provision201810 | Fee201810 | Turnover20811 | Provision201811 | Fee201811 | Turnover20812 | Provision201812 | Fee201812 |
+------+------+---------------+-----------------+------------+---------------+-----------------+------------+---------------+-----------------+-----------+
| 123 | Asdf | 10000 | 100 | 0,1 | 20000 | 200 | 0,2 | 30000 | 300 | 0,3 |
+------+------+---------------+-----------------+------------+---------------+-----------------+------------+---------------+-----------------+-----------+
Thank you!
Assuming that each ColI, Col2 group would only have a maximum of three records, then we can try pivoting using the help of ROW_NUMBER:
WITH cte AS (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY ColI, Col2 ORDER BY Month) rn
FROM yourTable
)
SELECT
ColI,
Col2,
MAX(CASE WHEN rn = 1 THEN Turnover END) AS Turnover1,
MAX(CASE WHEN rn = 1 THEN Provision END) AS Provision1,
MAX(CASE WHEN rn = 1 THEN Fee END) AS Fee1,
MAX(CASE WHEN rn = 2 THEN Turnover END) AS Turnover2,
MAX(CASE WHEN rn = 2 THEN Provision END) AS Provision2,
MAX(CASE WHEN rn = 2 THEN Fee END) AS Fee2,
MAX(CASE WHEN rn = 3 THEN Turnover END) AS Turnover3,
MAX(CASE WHEN rn = 3 THEN Provision END) AS Provision3,
MAX(CASE WHEN rn = 3 THEN Fee END) AS Fee3
FROM cte
GROUP BY
ColI,
Col2;
Note that I did not hardwire more specific column names, to keep the query as general as possible. For example, perhaps there might be another ColI, Col2 group which would have a different three months.
By using Dynamic Sql
IF OBJECT_ID('tempdb..#TEMP') IS NOT NULL
DROP TABLE #TEMP
;WITH CTE (ColI , Col2 , Month , Turnover , Provision , Fee )
AS
(
SELECT 123 , 'Asdf' , 201810 , 10000 ,100 ,'0,1' UNION ALL
SELECT 123 , 'Asdf' , 201811 , 20000 ,200 , '0,2' UNION ALL
SELECT 123 , 'Asdf' , 201812 , 30000 ,300 , '0,3'
)
SELECT ColI , Col2,Turnover , Provision , Fee,MixedCol,Reqcol , ROW_NUMBER()OVER(ORDER BY (SELECT NULL)) AS Seq
INTO #Temp
FROM CTE
CROSS APPLY (VALUES (CONCAT('Turnover','_',[Month]),CAST(Turnover AS VARCHAR(20))),
(CONCAT('Provision','_',[Month]),CAST(Provision AS VARCHAR(20))),
(CONCAT('Fee','_',[Month]),CAST(Fee AS VARCHAR(20)))
)DT (MixedCol,Reqcol)
DECLARE #Sql nvarchar(max),
#DynamicColumn nvarchar(max),
#MaxDynamicColumn nvarchar(max)
SELECT #DynamicColumn = STUFF((SELECT ', '+QUOTENAME(CAST(MixedCol AS VARCHAR(100)))
FROM #TEMP ORDER BY Seq FOR XML PATH ('')),1,1,'')
SELECT #MaxDynamicColumn = STUFF((SELECT ', '+'MAX('+QUOTENAME(CAST(MixedCol AS VARCHAR(100)))+') AS '+QUOTENAME(CAST(MixedCol AS VARCHAR(100)))
FROM #TEMP ORDER BY Seq FOR XML PATH ('')),1,1,'')
SET #Sql=' SELECT ColI , Col2,'+ #MaxDynamicColumn+'
FROM
(
SELECT * FROM #TEMP
)AS src
PIVOT
(
MAX(Reqcol) FOR [MixedCol] IN ('+#DynamicColumn+')
) AS Pvt
GROUP BY ColI , Col2 '
EXEC (#Sql)
PRINT #Sql
Q: "...give me hints (not whole code) how to do this"
A: This example with dynamic transpose of table. You can try this. Unpivot your original table and transpose.
CREATE table #tbl (
color varchar(10), Paul int, John int, Tim int, Eric int);
insert #tbl select
'Red' ,1 ,5 ,1 ,3 union all select
'Green' ,8 ,4 ,3 ,5 union all select
'Blue' ,2 ,2 ,9 ,1;
select * FROM #tbl
--1) Transpose. Example without dynamic code. You create list of fields in query
select *
from #tbl
unpivot (value for name in ([Paul],[John],[Tim],[Eric])) up
pivot (max(value) for color in ([Blue],[Green],[Red])) p
--2) Transpose. Example with dynamic code, without names of fields.
DECLARE #cols NVARCHAR(MAX), #query NVARCHAR(MAX), #rows NVARCHAR(MAX)='';
-- XML with all values
SET #cols = STUFF(
(
SELECT DISTINCT
','+QUOTENAME(T.Color) -- Name of first column
FROM #tbl T FOR XML PATH(''), TYPE
).value('.', 'nvarchar(max)'), 1, 1, '');
SELECT #rows=#rows+','+QUOTENAME(Name)
FROM
(SELECT Name,ROW_NUMBER() OVER(ORDER BY column_id) AS 'RowNum'
FROM tempdb.sys.Columns
WHERE Object_ID = OBJECT_ID('tempdb..#tbl')
) AS A WHERE A.RowNum>1
SET #rows=STUFF(#rows,1,1,'')
SET #query='SELECT *
from #tbl
unpivot (value for name in ('+#rows+')) up
pivot (max(value) for color in ('+#Cols+')) p'
EXEC (#query)
I have a table that looks like this:
att1 att2
| a | 1 |
| a | 2 |
| b | 2 |
| b | 3 |
| c | 1 |
| c | 2 |
| c | 2 |
And I need the different record of att2 for the duplicate value on att1 to be grouped into a new column like this
att1 att2 att3
| a | 1 | 2 |
| b | 2 | 3 |
| c | 1 | 2 |
I tried to pivot, I tried to self join, but I can't seem to find the query to separate the values like this. Can someone please help me? Thanks
you can use a dynamic pivot query like below
see demo link
create table tt (att1 varchar(10), att2 int)
insert into tt values
('a',1)
,('a',2)
,('b',2)
,('b',3)
,('c',1)
,('c',2)
,('c',2)
go
declare #q varchar(max), #cols varchar(max)
set #cols
= STUFF((
SELECT distinct ',' +
QUOTENAME('att '+
cast(1+ row_number() over (partition by att1 order by att2 ) as varchar(max))
)
FROM (select distinct att1,att2 from tt)tt --note this distinct
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #q=
'select
att1,'+ #cols +
' from
(
select
att1,att2,
''att ''+
cast(1+row_number() over (partition by att1 order by att2 ) as varchar(max)) as r
from
(select distinct att1,att2 from tt)tt
)I
pivot
(
max(att2)
for r in ('+#cols+')
)piv'
exec(#q)
Any query like this always smells like report formatting, rather than genuine data requirement, which should probably be done in a reporting tool rather than a database. But as with all things it is possible with enough code.
This should work for you.
create table #t (att1 nvarchar(max) ,att2 int);
insert #t select 'a', 1 union all select 'a', 2;
insert #t select 'b', 2 union all select 'b', 3;
insert #t select 'c', 1 union all select 'c', 2 union all select 'c', 2;
select att1, 1 as att2, 2 as att3 from
(
select att1, att2, row_number() over (partition by att1 order by att1, att2) as r
from (select distinct att1, att2 from #t) as x
) src
pivot ( avg(att2) for r in ([1],[2])) p;
drop table #t;
The first step is to get the distinct values in your table, and then sort and group them by att1. I'm doing this with a row_number() command, which looks like this:
select att1, att2, row_number() over (partition by att1 order by att1, att2) as r
from (select distinct att1, att2 from #t) as x ;
att1 attr2 r
a 1 1
a 2 2
b 2 1
b 3 2
c 1 1
c 2 2
From there the pivot command transforms rows into columns. The catch with the pivot command is that the names of those new columns need to be data driven; during your row_number command you could provide better names, or you can alias them as I have done here.
Finally, this only works when there are only two values to pivot. To add more, modify the for r in ([1], [2]) line to include e.g. 3, 4, etc.
DECLARE #TABLE TABLE (NAME varchar(10), DOB Datetime2, Location varchar(50), Phone int)
INSERT INTO #TABLE (NAME, DOB, Location, Phone)
SELECT 'Name1','2000-01-01','USA',1234567890
UNION ALL
SELECT 'Name2','2000-01-02','CAN',0987654321
SELECT * FROM #TABLE
/*
Current Output
NAME DOB Location Phone
Name1 2000-01-01 00:00:00.0000000 USA 1234567890
Name2 2000-01-02 00:00:00.0000000 CAN 987654321
Desired Output
Catagory N1 N2 ...Nn
'NAME1' 'Name2'
DOB '2000-01-01' '2000-01-02'
Location 'USA' 'CAN'
Phone 1234567890 0987654321
Catagory, N1, N2,...Nn are column names (Nn = there can be dynamica number of "Name"
There is no catagory name for 'Name1,'Name2',...'Namen'
Not sure how to do this properly...XML maybe? Please help!
*/
Thank you
You can use the PIVOT function to get the result but you will need to use a few other functions first to get the final product.
First, you will want to create a unique sequence for each row (it doesn't look like you have one), this value is going to be used to create your final list of new columns. You can use row_number() to create this value:
select name, dob, location, phone,
row_number() over(order by name) seq
from yourtable
See SQL Fiddle with Demo. Once you have created this unique value then you can unpivot the multiple columns of data name, dob, location and phone. Depending on your version of SQL Server you can use the unpivot function or CROSS APPLY:
select 'N'+cast(seq as varchar(10)) seq,
category, value, so
from
(
select name, dob, location, phone,
row_number() over(order by name) seq
from yourtable
) src
cross apply
(
select 'name', name, 1 union all
select 'DOB', convert(varchar(10), dob, 120), 2 union all
select 'Location', location, 3 union all
select 'Phone', cast(phone as varchar(15)), 4
) c (category, value, so);
See SQL Fiddle with Demo. This will get your data in the format:
| SEQ | CATEGORY | VALUE | SO |
|-----|----------|------------|----|
| N1 | name | Name1 | 1 |
| N1 | DOB | 2000-01-01 | 2 |
| N1 | Location | USA | 3 |
| N1 | Phone | 1234567890 | 4 |
Now you can easily apply the PIVOT function:
SELECT category, n1, n2
FROM
(
select 'N'+cast(seq as varchar(10)) seq,
category, value, so
from
(
select name, dob, location, phone,
row_number() over(order by name) seq
from yourtable
) src
cross apply
(
select 'name', name, 1 union all
select 'DOB', convert(varchar(10), dob, 120), 2 union all
select 'Location', location, 3 union all
select 'Phone', cast(phone as varchar(15)), 4
) c (category, value, so)
) d
pivot
(
max(value)
for seq in (N1, N2)
) piv
order by so;
See SQL Fiddle with Demo. The above works great if you have a limited number of values but if you will have an unknown number of names, then you will need to use dynamic SQL:
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX)
select #cols = STUFF((SELECT ',' + QUOTENAME('N'+cast(seq as varchar(10)))
from
(
select row_number() over(order by name) seq
from yourtable
)d
group by seq
order by seq
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = 'SELECT category, ' + #cols + '
from
(
select ''N''+cast(seq as varchar(10)) seq,
category, value, so
from
(
select name, dob, location, phone,
row_number() over(order by name) seq
from yourtable
) src
cross apply
(
select ''name'', name, 1 union all
select ''DOB'', convert(varchar(10), dob, 120), 2 union all
select ''Location'', location, 3 union all
select ''Phone'', cast(phone as varchar(15)), 4
) c (category, value, so)
) x
pivot
(
max(value)
for seq in (' + #cols + ')
) p
order by so'
execute sp_executesql #query;
See SQL Fiddle with Demo. They both give a result of:
| CATEGORY | N1 | N2 |
|----------|------------|------------|
| name | Name1 | Name2 |
| DOB | 2000-01-01 | 2000-01-02 |
| Location | USA | CAN |
| Phone | 1234567890 | 987654321 |
I have this current select:
SELECT MIN(CONVERT(DateTime,SUBSTRING(NameAndDate,14,8))),
SUBSTRING(NameAndDate,1,12)
FROM MyData
WHERE pName IN (SELECT SUBSTRING(NameAndDate,1,12)
FROM MyData
GROUP BY SUBSTRING(NameAndDate,1,12)
HAVING COUNT(*) > 1)
GROUP BY SUBSTRING(NameAndDate,1,12)
Where SUBSTRING(NameAndDate,1,60) is the person's ID name and SUBSTRING(NameAndDate,61,8) is the date they came in.
There are many times where this data shows up multiple times in the table which is why I want to select the MIN date.
The problem is that there is another column in the table (ID) that I need to be added, but I don't want to group by it because it adds duplicates to the Person's ID.
Is there a way I can do the following:
SELECT ID,
MIN(CONVERT(DateTime,SUBSTRING(NameAndDate,14,8))),
SUBSTRING(NameAndDate,1,12)
FROM MyData
WHERE pName IN (SELECT SUBSTRING(NameAndDate,1,12)
FROM MyData
GROUP BY SUBSTRING(NameAndDate,1,12)
HAVING COUNT(*) > 1)
GROUP BY SUBSTRING(NameAndDate,1,12)
EDIT:
There could be multiple times a person comes through:
ID | NameAndDate
----+-----------------------
1 | J60047238486 08162013
2 | J60047238486 08182013
3 | J60047238486 08242013
4 | J60047238486 09032013
5 | J60047238486 10102013
6 | C40049872351 05302013
7 | C40049872351 07212013
8 | C40049872351 07252013
My current select pulls:
Name | Date
--------------+---------------------
J60047238486 | 08/16/2013 00:00:00
C40049872351 | 05/30/2013 00:00:00
But I want to add the ID column for those specific rows:
ID | Name | Date
----+--------------+---------------------
1 | J60047238486 | 08/16/2013 00:00:00
6 | C40049872351 | 05/30/2013 00:00:00
Try this
SELECT * FROM (
SELECT id,
CONVERT(DateTime,right (SUBSTRING(NameAndDate,14,8),4)
+ SUBSTRING(NameAndDate,14,4)) D,
SUBSTRING(NameAndDate,1,12) N,
COUNT(*) OVER (PARTITION BY SUBSTRING(NameAndDate,1,12)) Cnt,
ROW_NUMBER() OVER (PARTITION BY SUBSTRING(NameAndDate,1,12)
ORDER By SUBSTRING(NameAndDate,14,8)) rn
FROM
mydata
) v WHERE CNT > 1 and rn = 1;
SQL DEMO HERE
You can do this, but it aint' pretty. You have to run your original query to get the minimum date for each name, and then join that back to your MyData table. It's particularly ugly because of how you store the data. Converting your MMDDYYYY string to a data was really fun.
SQL Fiddle
select
MyData.[ID],
t1.theName,
t1.theDate
from
Mydata
inner join
(
select
SUBSTRING(NameAndDate,1,12) as theName,
min (
convert(datetime,
right (SUBSTRING(NameAndDate,14,8),4) + '-' +
left (SUBSTRING(NameAndDate,14,8),2) + '-' +
SUBSTRING((SUBSTRING(NameAndDate,14,8)),3,2)
))as theDate
from
mydata
where
SUBSTRING(NameAndDate,1,12) in
(SELECT SUBSTRING(NameAndDate,1,12)
FROM MyData
GROUP BY SUBSTRING(NameAndDate,1,12)
HAVING COUNT(*) > 1)
group by
SUBSTRING(NameAndDate,1,12) ) t1
ON SUBSTRING(mydata.NameAndDate,1,12) = t1.theName
AND (convert(datetime,
right (SUBSTRING(NameAndDate,14,8),4) + '-' +
left (SUBSTRING(NameAndDate,14,8),2) + '-' +
SUBSTRING((SUBSTRING(NameAndDate,14,8)),3,2))) = t1.theDate
with cte as (
-- first cte - parsing data
select
ID,
left(NameAndDate, 12) as Name,
convert(date,
right(NameAndDate, 4) +
substring(NameAndDate, 14, 2) +
substring(NameAndDate, 16, 2),
112) as Date
from Table1
), cte2 as (
-- second cte - create row_number
select
ID, Name, Date,
row_number() over(partition by Name order by Date) as rn
from cte
)
select
ID, Name, Date
from cte2
where rn = 1
sql fiddle demo
I have the following sample data:
Id Name Category
-----------------------
1 Joe A
2 Joe B
3 Joe D
4 Mary A
5 Mary C
6 Mary D
I would like to show the categories a person belongs to like so:
Name CategoryA CategoryB CategoryC CategoryD
--------------------------------------------------
Joe X X X
Mary X X X
1's and 0's could be used in place of X's and blanks.
This smells like a PIVOT question to me.
There are several ways that you can transform the data. Some use an aggregate function and others don't. But even though you are pivoting a string you can still apply an aggregate.
Aggregate with CASE:
select name,
max(case when category = 'A' then 'X' else '' end) CategoryA,
max(case when category = 'B' then 'X' else '' end) CategoryB,
max(case when category = 'C' then 'X' else '' end) CategoryC,
max(case when category = 'D' then 'X' else '' end) CategoryD
from yourtable
group by name
See SQL Fiddle with Demo
Static Pivot:
You can still use the PIVOT function to transform the data even though the values are strings. If you have a known number of categories, then you can hard-code the query:
select name,
coalesce(A, '') CategoryA,
coalesce(B, '') CategoryB,
coalesce(C, '') CategoryC,
coalesce(C, '') CategoryD
from
(
select name, category, 'X' flag
from yourtable
) d
pivot
(
max(flag)
for category in (A, B, C, D)
) piv
See SQL Fiddle with Demo.
Dynamic Pivot:
If you have an unknown number of categories, then you can use dynamic SQL:
DECLARE #cols AS NVARCHAR(MAX),
#colsNull AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX)
select #cols = STUFF((SELECT ',' + QUOTENAME(category)
from yourtable
group by category
order by category
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
select #colsNull = STUFF((SELECT ', coalesce(' + QUOTENAME(category)+', '''') as '+QUOTENAME('Category'+category)
from yourtable
group by category
order by category
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = 'SELECT name, ' + #colsNull + '
from
(
select name, category, ''X'' flag
from yourtable
) x
pivot
(
max(flag)
for category in (' + #cols + ')
) p '
execute(#query)
See SQL Fiddle with Demo.
Multiple Joins:
select c1.name,
case when c1.category is not null then 'X' else '' end as CategoryA,
case when c2.category is not null then 'X' else '' end as CategoryB,
case when c3.category is not null then 'X' else '' end as CategoryC,
case when c4.category is not null then 'X' else '' end as CategoryD
from yourtable c1
left join yourtable c2
on c1.name = c2.name
and c2.category = 'B'
left join yourtable c3
on c1.name = c3.name
and c3.category = 'C'
left join yourtable c4
on c1.name = c4.name
and c4.category = 'D'
where c1.category = 'A'
See SQL Fiddle with Demo
All queries will give the result:
| NAME | CATEGORYA | CATEGORYB | CATEGORYC | CATEGORYD |
--------------------------------------------------------
| Joe | X | X | | X |
| Mary | X | | X | X |