I'm trying to create a dynamic pivot table to get results in different columns rather than rows.
This is the table I'm using to test
CREATE TABLE [dbo].[Authors](
[Client_Id] [nvarchar](50) NOT NULL,
[Project_Id] [nvarchar](50) NOT NULL,
[Person_Id] [int] NOT NULL,
[Author_Number] [int] NOT NULL,
[Family_Name] [nvarchar](50) NULL,
[First_Name] [nvarchar](50) NULL,
)
INSERT INTO Authors (Client_Id, Project_Id, Person_Id, Author_Number, Family_Name, First_Name)
VALUES ('TEST','TEST',12345,1,'Giust','Fede')
INSERT INTO Authors (Client_Id, Project_Id, Person_Id, Author_Number, Family_Name, First_Name)
VALUES ('TEST','TEST',12345,2,'Giust','Fede')
INSERT INTO Authors (Client_Id, Project_Id, Person_Id, Author_Number, Family_Name, First_Name)
VALUES ('TEST','TEST',12346,1,'Giust','Fede')
INSERT INTO Authors (Client_Id, Project_Id, Person_Id, Author_Number, Family_Name, First_Name)
VALUES ('TEST','TEST',12346,2,'Giust','Fede')
INSERT INTO Authors (Client_Id, Project_Id, Person_Id, Author_Number, Family_Name, First_Name)
VALUES ('TEST','TEST',12346,3,'Giust','Fede')
So far I get the results like this
CLIENT_ID PROJECT_ID PERSON_ID AUTHOR_NUMBER FAMILY_NAME FIRST_NAME
TEST TEST 12345 1 Giust Fede
TEST TEST 12345 2 Ma Ke
TEST TEST 12346 1 Jones Peter
TEST TEST 12346 2 Davies Bob
TEST TEST 12346 3 Richards Craig
I need the results to come out like this, and to be dynamic because sometime I can have 2 authors, or 10 authors.
CLIENT_ID PROJECT_ID PERSON_ID FAMILY_NAME_1 FIRST_NAME_1 FAMILY_NAME_2 FIRST_NAME_2 FAMILY_NAME_3 FIRST_NAME_3
TEST TEST 12345 Giust Fede Ma Ke
TEST TEST 12346 Jones Peter Davies Bob Richards Craig
I've been trying to use this code, but keep getting errors
SQL Fiddle
Here are a few suggestions to solve your issue.
First, your current code is unpivoting all of the columns in the table, you only need to UNPIVOT the Family_Name and First_Name columns. As a result I would not use a variable to get this list of columns, just hard-code the two columns you need to unpivot.
Then to get the list of columns to PIVOT, alter the code to use both the Author_number and then a string with the column names Family_Name and First_Name:
select #colsPivot = STUFF((SELECT ',' + QUOTENAME(c.col + '_'+cast(Author_Number as varchar(10)))
from Authors
cross apply
(
select 'Family_Name' col, 1 so union all
select 'First_Name', 2
) c
group by col, author_number, so
order by author_number, so
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
This gives a list of cols:
| COLUMN_0 |
------------------------------------------------------------------------------------------------
| [Family_Name_1],[First_Name_1],[Family_Name_2],[First_Name_2],[Family_Name_3],[First_Name_3] |
Making these changes to your code the final query will be
DECLARE #colsPivot AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX)
select #colsPivot = STUFF((SELECT ',' + QUOTENAME(c.col + '_'+cast(Author_Number as varchar(10)))
from Authors
cross apply
(
select 'Family_Name' col, 1 so union all
select 'First_Name', 2
) c
group by col, author_number, so
order by author_number, so
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query
= 'select Client_id, Project_id, Person_Id, '+#colsPivot+'
from
(
select Client_id, Project_id, Person_Id,
col+''_''+cast(Author_Number as varchar(10)) col, val
from
(
select Client_id, Project_id,
Person_Id,
Author_Number,
Family_Name,
First_Name
from Authors
) s
unpivot
(
val
for col in (Family_Name, First_Name)
) u
) x1
pivot
(
max(val)
for col in ('+ #colspivot +')
) p'
exec(#query)
See SQL Fiddle with Demo. This gives a result:
| CLIENT_ID | PROJECT_ID | PERSON_ID | FAMILY_NAME_1 | FIRST_NAME_1 | FAMILY_NAME_2 | FIRST_NAME_2 | FAMILY_NAME_3 | FIRST_NAME_3 |
-----------------------------------------------------------------------------------------------------------------------------------
| TEST | TEST | 12345 | Giust | Fede | Ma | Ke | (null) | (null) |
| TEST | TEST | 12346 | Jones | Peter | Davies | Bob | Richards | Craig |
Related
This question already has answers here:
How to use GROUP BY to concatenate strings in SQL Server?
(22 answers)
How to concatenate text from multiple rows into a single text string in SQL Server
(47 answers)
Closed 3 years ago.
I have a table like this :
id | movie | actorid | actor | roleid | rolename
----+---------+---------+---------+--------+------------------
1 | mi3 | 121 | tom | 6 | actor
2 | avenger | 104 | scarlett| 4 | actress
2 | avenger | 3 | russo | 2 | action director
I'm expecting the output like :
id | movie | actorid | actor | roleid | rolename
----+---------+---------+----------------+--------+--------------------------
1 | mi3 | 121 | tom | 6 | actor
2 | avenger | 104,3 | scarlett,russo | 4,2 | actress, action director
For latest SQL Server version, I saw the STRING_AGG function to concatenate columns or row data. But how can I achieve the expected output with SQL Server 2014 using STUFF ?
Try this:
DECLARE #DataSource TABLE
(
[id] INT
,[movie] VARCHAR(12)
,[actiorid] INT
,[actor] VARCHAR(12)
,[roleid] INT
,[rolename] VARCHAR(36)
);
INSERT INTO #DataSource ([id], [movie], [actiorid], [actor], [roleid], [rolename])
VALUES (1, 'mi3 ', 121, 'tom ', 6, 'actor')
,(2, 'avenger', 104, 'scarlett', 4, 'actress')
,(2, 'avenger', 3, 'russo', 2, 'action director');
-- SQL Server 2017
SELECT [id]
,[movie]
,STRING_AGG([actiorid], ',') AS [actorid]
,STRING_AGG([actor], ',') AS [actor]
,STRING_AGG([roleid], ',') AS [roleid]
,STRING_AGG([rolename], ',') AS [rolename]
FROM #DataSource
GROUP BY [id]
,[movie];
-- SQL Server
WITH DataSoruce AS
(
SELECT DISTINCT [id]
,[movie]
FROM #DataSource
)
SELECT *
FROM DataSoruce A
CROSS APPLY
(
SELECT STUFF
(
(
SELECT DISTINCT ',' + CAST([actiorid] AS VARCHAR(12))
FROM #DataSource S
WHERE A.[id] = S.[id]
AND A.[movie] = S.[movie]
FOR XML PATH, TYPE
).value('.', 'VARCHAR(MAX)')
,1
,1
,''
)
) R1 ([actiorid])
CROSS APPLY
(
SELECT STUFF
(
(
SELECT DISTINCT ',' + CAST([actor] AS VARCHAR(12))
FROM #DataSource S
WHERE A.[id] = S.[id]
AND A.[movie] = S.[movie]
FOR XML PATH, TYPE
).value('.', 'VARCHAR(MAX)')
,1
,1
,''
)
) R2 ([actor])
CROSS APPLY
(
SELECT STUFF
(
(
SELECT DISTINCT ',' + CAST([roleid] AS VARCHAR(12))
FROM #DataSource S
WHERE A.[id] = S.[id]
AND A.[movie] = S.[movie]
FOR XML PATH, TYPE
).value('.', 'VARCHAR(MAX)')
,1
,1
,''
)
) R3 ([roleid])
CROSS APPLY
(
SELECT STUFF
(
(
SELECT DISTINCT ',' + CAST([rolename] AS VARCHAR(12))
FROM #DataSource S
WHERE A.[id] = S.[id]
AND A.[movie] = S.[movie]
FOR XML PATH, TYPE
).value('.', 'VARCHAR(MAX)')
,1
,1
,''
)
) R4 ([rolename]);
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 the following data:
DECLARE #DataSource TABLE
(
[ColumnA] INT
,[ColumnB] INT
,[ColumnC] INT
)
INSERT INTO #DataSource ([ColumnA], [ColumnB], [ColumnC])
VALUES (5060,1006,100118)
,(5060,1006,100119)
,(5060,1006,100120)
,(5060,1007,100121)
,(5060,1007,100122)
,(5060,1012,100123)
SELECT [ColumnA]
,[ColumnB]
,[ColumnC]
FROM #DataSource
and I need to converted like this:
The difficult part is that the data is dynamic (I do not know how many columns I will have) and I am not able to use a standard pivot here because the values in ColumnC are different and as a result I am going to have as many columns as values appears in ColumnC.
Is there any technique to achieve this?
Any kind of help (answers, articles, suggestions) will be appreciated.
My suggestion whenever you are working with PIVOT is to alway write the query first with the values hard-coded, then you can easily convert the query to a dynamic solution.
Since you are going to have multiple values of columnC that will be converted to columns, then you need to look at using the row_number() windowing function to generate a unique sequence for each columnc based on the values of columnA and columnB.
The starting point for your query will be:
select [ColumnA],
[ColumnB],
[ColumnC],
'SampleTitle'+
cast(row_number() over(partition by columna, columnb
order by columnc) as varchar(10)) seq
from DataSource;
See Demo. This query will generate the list of new columns names SampleTitle1, etc:
| COLUMNA | COLUMNB | COLUMNC | SEQ |
|---------|---------|---------|--------------|
| 5060 | 1006 | 100118 | SampleTitle1 |
| 5060 | 1006 | 100119 | SampleTitle2 |
| 5060 | 1006 | 100120 | SampleTitle3 |
You can then apply the pivot on columnC with the new column names listed in seq:
select columnA, columnB,
SampleTitle1, SampleTitle2, SampleTitle3
from
(
select [ColumnA],
[ColumnB],
[ColumnC],
'SampleTitle'+
cast(row_number() over(partition by columna, columnb
order by columnc) as varchar(10)) seq
from DataSource
) d
pivot
(
max(columnc)
for seq in (SampleTitle1, SampleTitle2, SampleTitle3)
) piv;
See SQL Fiddle with Demo.
Once you have the correct logic, you can convert the data to dynamic SQL. The key here is generating the list of new column names. I typically use FOR XML PATH for this similar to:
select STUFF((SELECT distinct ',' + QUOTENAME(seq)
from
(
select 'SampleTitle'+
cast(row_number() over(partition by columna, columnb
order by columnc) as varchar(10)) seq
from DataSource
) d
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
See Demo. Once you have the list of column names, then you will generate your sql string to execute, the full code will be:
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX)
select #cols = STUFF((SELECT distinct ',' + QUOTENAME(seq)
from
(
select 'SampleTitle'+
cast(row_number() over(partition by columna, columnb
order by columnc) as varchar(10)) seq
from DataSource
) d
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = 'SELECT columnA, ColumnB,' + #cols + '
from
(
select [ColumnA],
[ColumnB],
[ColumnC],
''SampleTitle''+
cast(row_number() over(partition by columna, columnb
order by columnc) as varchar(10)) seq
from DataSource
) x
pivot
(
max(columnc)
for seq in (' + #cols + ')
) p '
execute sp_executesql #query;
See SQL Fiddle with Demo. These give a result:
| COLUMNA | COLUMNB | SAMPLETITLE1 | SAMPLETITLE2 | SAMPLETITLE3 |
|---------|---------|--------------|--------------|--------------|
| 5060 | 1006 | 100118 | 100119 | 100120 |
| 5060 | 1007 | 100121 | 100122 | (null) |
| 5060 | 1012 | 100123 | (null) | (null) |
Ok I am working on a database table with 4 columns, lets say a first name, middle name, last name and a group id. I want to group people based on the fact that they have the same first and last names regardless of their middle name. I also want to, if a new entry comes in, give that entry the correct group id.
Here is an example of the result:
----------------------------------------------------------
| First_Name | Middle_Name | Last_Name | Group_ID |
----------------------------------------------------------
| Jon | Jacob | Schmidt | 1 |
----------------------------------------------------------
| William | B. | Schmidt | 1 |
----------------------------------------------------------
| Sally | Anne | Johnson | 2 |
----------------------------------------------------------
I'm not sure whether or not this falls under the jurisdiction of a computed column, some kind of join or something far less obscure, Please help!
If you only need to enumerate the groups within a query then row_number() will work for you:
declare #Names table ( First_Name varchar(10), Middle_Name varchar(10), Last_Name varchar(10))
insert into #Names
select 'Jon', 'Jacob', 'Schmidt' union all
select 'William', 'B.', 'Schmidt' union all
select 'Sally', 'Anne', 'Johnson' union all
select 'Jon', 'Two', 'Schmidt'
;with Groups (First_Name, Last_Name, Group_ID) as
( select First_Name, Last_Name, row_number()over(order by Last_Name)
from #Names
group
by First_Name, Last_Name
)
select n.First_Name, n.Middle_Name, n.Last_Name, g.Group_Id
from #Names n
join Groups g on
n.First_Name = g.First_Name and
n.Last_Name = g.Last_Name;
Be aware the Group_ID value will change as new nameGroups are introduced.
If you want to assign and persist a Group_ID then I would suggest creating an ancillary table and assign the Group_IDs there.
By storing the mapping outside of the #Names table you are allowing users to change their names and not have to worry about re-evaluating the group assignment. It also allows you to modify the grouping logic without re-assigning names. You also have the ability to map similar enough values to the same grouping (John, Jon, Jonny).
Group_ID is composed of a First_Name and Last_Name. So, store it that way.
declare #Names table ( First_Name varchar(10), Middle_Name varchar(10), Last_Name varchar(10))
insert into #Names
select 'Jon', 'Jacob', 'Schmidt' union all
select 'William', 'B.', 'Schmidt' union all
select 'Sally', 'Anne', 'Johnson' union all
select 'Jon', 'Two', 'Schmidt'
declare #NameGroup table (Group_Id int identity(1,1), First_Name varchar(10), Last_Name varchar(10) unique(First_Name, Last_Name));
insert into #NameGroup (First_Name, Last_Name)
select 'Jon', 'Schmidt' union all
select 'Sally', 'Johnson';
declare #Group_ID int;
declare #First_Name varchar(10),
#Middle_Name varchar(10),
#Last_Name varchar(10)
select #First_Name = 'Jon',
#Middle_Name = 'X',
#Last_Name = 'Schmidt'
--be sure the Id has already been assigned
insert into #NameGroup
select #First_Name, #Last_Name
where not exists(select 1 from #NameGroup where First_Name = #First_Name and Last_Name = #Last_Name)
--resolve the id
select #Group_ID = Group_ID
from #NameGroup
where First_Name = #First_Name and
Last_Name = #Last_Name;
--store the name
insert into #Names (First_Name, Middle_Name, Last_Name)
values(#First_Name, #Middle_Name, #Last_Name);
select n.First_Name, n.Middle_Name, n.Last_Name, ng.Group_Id
from #Names n
join #NameGroup ng on
n.First_Name = ng.First_Name and
n.Last_Name = ng.Last_Name;
Imagine I have this table
BirthDay |Name
1-10-2010 | 'Joe'
2-10-2010 | 'Bob'
2-10-2010 | 'Alice'
How can I get a result like this
BirthDay |Name
1-10-2010 | 'Joe'
2-10-2010 | 'Bob', 'Alice
tks
try this:
set nocount on;
declare #t table (BirthDay datetime, name varchar(20))
insert into #t VALUES ('1-10-2010', 'Joe' )
insert into #t VALUES ('2-10-2010', 'Bob' )
insert into #t VALUES ('2-10-2010', 'Alice')
set nocount off
SELECT p1.BirthDay,
stuff(
(SELECT
', ' + p2.name --use this if you want quotes around the names: ', ''' + p2.name+''''
FROM #t p2
WHERE p2.BirthDay=p1.BirthDay
ORDER BY p2.name
FOR XML PATH('')
)
,1,2, ''
) AS Names
FROM #t p1
GROUP BY
BirthDay
OUTPUT:
BirthDay Names
----------------------- ------------
2010-01-10 00:00:00.000 Joe
2010-02-10 00:00:00.000 Alice, Bob
(2 row(s) affected)
This solution works with no need of deploy from Visual studio or dll file in server.
Copy-Paste and it Work!
http://groupconcat.codeplex.com/
dbo.GROUP_CONCAT(VALUE )
dbo.GROUP_CONCAT_D(VALUE ), DELIMITER )
dbo.GROUP_CONCAT_DS(VALUE , DELIMITER , SORT_ORDER )
dbo.GROUP_CONCAT_S(VALUE , SORT_ORDER )