SQL PIVOT 2 Columns and repeat for multiples - HEAD SCRATCHER - sql

I am having a HECK of a time trying to pivot some data & am wondering if anyone has an idea that would solve this!
I have tried dynamic pivots, but I run out of columns fast.
I have tried multiple pivots and joining them, but that is very clunky.
I have the following data:
CREATE TABLE dbo.Visits
(
SourceID varchar(32),
VisitID varchar(32),
EpisodeDate varchar(9),
EpisodeUrnID int,
SortOrder int,
ProcID char(7)
);
INSERT dbo.Visits
(SourceID,VisitID,EpisodeDate,EpisodeUrnID,SortOrder,ProcID)
VALUES
('SKREE','B20190531064919932','20-May-19',1,1,'5A1955Z'),
('SKREE','B20190531064919932','20-May-19',1,2,'0BH17EZ'),
('SKREE','B20190531064919932','24-May-19',2,1,'03HY32Z'),
('SKREE','B20190531064919932','6-Jun-19' ,3,1,'03HY32Z'),
('SKREE','B20190531064919932','21-May-19',4,1,'02HV33Z'),
('SKREE','B20190531064919932','21-May-19',4,2,'B548ZZA'),
('SKREE','B20210530154407871','30-May-21',1,1,'0DTJ4ZZ'),
('SKREE','B20210530154407871','3-Jun-21' ,2,1,'0W9G40Z'),
('SKREE','B20210530154407871','3-Jun-21' ,2,2,'0WJG4ZZ'),
('SKREE','B20210530154407871','7-Jun-21' ,3,1,'02HV33Z'),
('SKREE','B20210530154407871','7-Jun-21' ,3,2,'B548ZZA');
Basically, for every VisitID, there are multiple EpisodeUrnIds, which can have multiple SortOrders and I need to list the EpisodeDate and ProcID of each on the same row.
I have analyzed our tables and one VisitID can have up to 40 EpisodeUrnIDs (so far), with each having up to 20 SortOrders (so far).
My goal is to get it to look like this (I used the first VisitID only in this example):
SourceID|VisitID|EpisodeDate|ProcID|EpisodeDate|ProcID|EpisodeDate|ProcID|EpisodeDate|ProcID|EpisodeDate|ProcID|EpisodeDate|ProcID
SKREE|B20190531064919932|20-May-19|5A1955Z|20-May-19|0BH17EZ|24-May-19|03HY32Z|6-Jun-19|03HY32Z|21-May-19|02HV33Z|21-May-19|B548ZZA
Thanks!

Dynamic PIVOTs are fun, there are definitely some different approaches to finding the limit. Here's one way:
DECLARE #sql nvarchar(max) = N'SELECT SourceID, VisitID';
;WITH x AS /* how many pivots do we need? */
(
SELECT TOP 1 c = COUNT(*) FROM dbo.Visits
GROUP BY SourceID, VisitID ORDER BY c DESC
),
n(n) AS /* produce that many rows for dynamic SQL */
(
SELECT 1 UNION ALL
SELECT n+1 FROM n WHERE n < (SELECT c FROM x)
)
SELECT #sql += N',
EpisodeDate' + CONVERT(varchar(11), n) + N' = MAX(CASE WHEN rn = '
+ CONVERT(varchar(11), n) + N' THEN EpisodeDate END),
ProcID' + CONVERT(varchar(11), n) + N' = MAX(CASE WHEN rn = '
+ CONVERT(varchar(11), n) + N' THEN ProcID END)'
FROM n OPTION (MAXRECURSION 32767); -- in case you go beyond 100
SET #sql += N' FROM src
GROUP BY SourceID, VisitID;';
SET #sql = N'
;WITH src AS
(
SELECT SourceID, VisitID, EpisodeDate, ProcID,
rn = ROW_NUMBER() OVER (PARTITION BY SourceID, VisitID ORDER BY SortOrder)
FROM dbo.Visits
)
' + #sql;
EXEC sys.sp_executesql #sql;
Working demo on dbfiddle
Article for more background

Related

SQL Server dynamic columns creation

I have a table with column and values as below
How do I fetch the result as in the second tabular column with the DYNAMIC column names as -- first with "prgmg_product_id" and the rest of the column as "source ID 1","source ID 2", "source ID 3"
To achieve this using Dynamic SQL, the below will help:
CREATE TABLE #Prgmg (
prgmg_product_id INT
,source_id_other INT
);
INSERT #Prgmg (
prgmg_product_id
,source_id_other
)
VALUES (3310,11478)
,(3337,10833)
,(3354,11466)
,(4039,4846)
,(4039,65454)
,(4039,65456);
DECLARE #DYColumns NVARCHAR(1000)
,#DYSqlQuery NVARCHAR(4000);
-- CREATE THE COLUMNS REQUIRED
SET #DYColumns = STUFF((
SELECT DISTINCT ','
+ N'sourceID'
+ CAST(ROW_NUMBER() OVER (PARTITION BY prgmg_product_id ORDER BY prgmg_product_id, source_id_other) AS NVARCHAR(10))
FROM #Prgmg
FOR XML PATH('')
), 1, 1, '');
-- CREATE THE DYNAMIC SQL AND ADD IN THE CREATED COLUMNS
SET #DYSqlQuery = '
SELECT prgmg_product_id,'
+ #DYColumns
+ ' FROM (
SELECT prgmg_product_id
,CAST(N''sourceID'' + CAST(ROW_NUMBER() OVER (
PARTITION BY prgmg_product_id ORDER BY prgmg_product_id, source_id_other
) AS NVARCHAR(10)) AS NVARCHAR(100)) AS Col
,source_id_other
FROM #Prgmg S1
) X
PIVOT(MIN(source_id_other) FOR Col IN (' + #DYColumns + ')) P'
EXECUTE sp_executesql #DYSqlQuery;
Whilst this does offer you the solution, you should spend time understanding the concepts used. Such as the creation of the Columns required using ROW_NUMBER and the how that maps to the use of the PIVOT.
I had a little time to I tossed this together. I know that around SO the preference is to use a dynamic pivot. I do not much care for PIVOT in sql server. I find the syntax to be very obtuse. I tend to prefer a cross tab query (also known as conditional aggregation) instead. The added benefit is that this approach is almost always slightly quicker than a dynamic PIVOT.
You also have to realize that more than half of the code posted here is setting up the problem. In the future you should post ddl and sample data in a consumable format like this. It makes it so much easier for us to help.
if OBJECT_ID('tempdb..#Something') is not null
drop table #Something
create table #Something
(
prgmg_product_id int,
source_id_other int
)
insert #Something (prgmg_product_id, source_id_other) values
(3310, 11478),
(3337, 10833),
(3354, 11466),
(4039, 4846),
(4039, 65454),
(4039, 65456)
declare #StaticPortion nvarchar(2000) =
'with OrderedResults as
(
select *, ROW_NUMBER() over(partition by prgmg_product_id order by source_id_other) as RowNum
from #Something
)
select prgmg_product_id';
declare #DynamicPortion nvarchar(max) = '';
declare #FinalStaticPortion nvarchar(2000) = ' from OrderedResults Group by prgmg_product_id order by prgmg_product_id desc';
with E1(N) AS (select 1 from (values (1),(1),(1),(1),(1),(1),(1),(1),(1),(1))dt(n)),
E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
cteTally(N) AS
(
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
)
select #DynamicPortion = #DynamicPortion +
', MAX(Case when RowNum = ' + CAST(N as varchar(6)) + ' then source_id_other end) as SourceID' + CAST(N as varchar(6)) + CHAR(10)
from cteTally t
where t.N <=
(
select top 1 Count(*)
from #Something
group by prgmg_product_id
order by COUNT(*) desc
)
declare #SqlToExecute nvarchar(max) = #StaticPortion + #DynamicPortion + #FinalStaticPortion;
exec sp_executesql #SqlToExecute

SQL Query - Rows to Columns but without pivot

I need an efficient way to convert the data returned in rows to columns for a football database I'm working on.
2 tables, one holding fixtures, one holding predictions.
Fixtures:
Predictions
I would like the data in those two table returned as follows (Can't post another link, but this query will give you an idea):
SELECT 1 as UserID,
2 as [Stoke vs Man United],
1 as [Bournemouth vs Crystal Palace],
2 as [Swansea vs West Brom],
1 as [Chelsea vs Watford],
3 as [Liverpool vs Leicester],
1 as [Tottenham vs Norwich],
2 as [Aston Villa vs West Ham]
The problem is, each week, the chosen teams will change so I couldn't get a pivot query to work. Any ideas?
2 things you need to know about to make this work.
Dynamic Pivot
sp_executesql
Example
-- Param Definitions
DECLARE #Sql NVARCHAR(MAX),
#Columns NVARCHAR(MAX),
#WeekNo INT = 6,
#UserID INT = 1,
#Params NVARCHAR(MAX) = N'#Week INT, #User INT'
-- Dynamic Column Names
SELECT #Columns = COALESCE(#Columns + ',','') + QUOTENAME(HomeTeamName + ' vs ' + AwayTeamName)
FROM usr_SS_Fixtures
WHERE WeekNo = #WeekNo
-- Dynamic SQL
SET #Sql = N'
SELECT *
FROM
(
SELECT
pred.UserId,
pred.ResultCode,
CONCAT(HomeTeamName,'' vs '', AwayTeamName) AS Teams
FROM
usr_SS_Fixtures fix
JOIN usr_SS_Predictions pred ON fix.EventID = pred.EventID AND fix.WeekNo = pred.WeekNo
WHERE
fix.WeekNo = #Week
AND pred.UserID = #User
) t
PIVOT
(
MAX(ResultCode)
FOR Teams IN (' + #Columns + ')
) p
'
-- Executes the dynamic SQL
EXECUTE sp_executesql #Sql, #Params, #Week = #WeekNo, #User = #UserID
DEMO
You have an interesting variation on the normal dynamic pivot problem, due to the known-in-advance maximum number of games, that makes one possible solution a little simpler. Try the SQL below; it creates a first row that contains the desired column headings, which may be sufficient for simple display purposes.
declare #weekNo as int = 6
,#UserID as int = 1;
with
fixture(UserID, EventID, [Value], GameNo) as (
select -1, EventID, AwayTeamName + ' at ' + HomeTeamName
, row_number() over (partition by WeekNo order by EventID)
from usr_SS_Fixtures
),
prediction(UserID, EventID, [Value], GameNo) as (
select UserID, EventID, ResultCode
, row_number() over (partition by WeekNo order by EventID)
from usr_SS_Predictions
),
data as (
select
WeekNo, UserID
,[1],[2],[3],[4],[5],[6],[7]
from (
select WeekNo, UserID, EventID, [Value] from fixture
union all
select WeekNo, UserID, EventID, [Value] from prediction
) T
pivot (
max([Value]) for GameNo in ([1],[2],[3],[4],[5],[6],[7])
) pvt
)
select
UserID,[1],[2],[3],[4],[5],[6],[7]
from data
where WeekNo = #WeekNo
order by
UserID

Merging rows to columns

I have the following situation (heavily abstracted, please ignore bad design):
CREATE TABLE dbo.PersonTest (Id INT, name VARCHAR(255))
INSERT INTO dbo.PersonTest
(Id, name )
VALUES (1, 'Pete')
, (1, 'Marie')
, (2, 'Sam')
, (2, 'Daisy')
I am looking for the following result:
Id Name1 Name2
1 Marie Pete
2 Daisy Sam
So, for each Id, the rows should be merged.
Getting this result I used the following query:
WITH PersonRN AS
(
SELECT *
, ROW_NUMBER() OVER(PARTITION BY Id ORDER BY name) RN
FROM dbo.PersonTest
)
SELECT PT1.Id
, PT1.name Name1
, PT2.name Name2
FROM PersonRN AS PT1
LEFT JOIN PersonRN AS PT2 -- Left join in case there's only 1 name
ON PT2.Id = PT1.Id
AND PT2.RN = 2
WHERE PT1.RN = 1
Which works perfectly fine.
My question is: Is this the best way (best in terms of performance and resilience)? If, for example, one of these Id's has a third name, this third name is ignored by my query. I'm thinking the best way to deal with that would be dynamic SQL, which would be fine, but if it can be done without dynamic, I would prefer that.
Aside from dynamic PIVOT, you can do this using Dynamic Crosstab, which I prefer for readability.
SQL Fiddle
DECLARE #sql1 VARCHAR(1000) = '',
#sql2 VARCHAR(1000) = '',
#sql3 VARCHAR(1000) = ''
DECLARE #max INT
SELECT TOP 1 #max = COUNT(*) FROM PersonTest GROUP BY ID ORDER BY COUNT(*) DESC
SELECT #sql1 =
'SELECT
ID' + CHAR(10)
SELECT #sql2 = #sql2 +
' , MAX(CASE WHEN RN =' + CONVERT(VARCHAR(5), RN)
+ ' THEN name END) AS ' + QUOTENAME('Name' + CONVERT(VARCHAR(5), RN)) + CHAR(10)
FROM(
SELECT TOP(#max)
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS RN
FROM sys.columns
)t
ORDER BY RN
SELECT #sql3 =
'FROM(
SELECT *,
RN = ROW_NUMBER() OVER(PARTITION BY ID ORDER BY name)
FROM PersonTest
)t
GROUP BY ID
ORDER BY ID'
PRINT (#sql1 + #sql2 + #sql3)
EXEC (#sql1 + #sql2 + #sql3)

Select values from multiple rows from 1 table as 1 record

I'm struggling to even explain what I need to do so please be patient with me.
I have the following table and rows in it:
TBNAME: Distances
Track, Person, Date, Distance
TRACK1, P1, 1/1/2014, 15
TRACK2, P1, 13/1/2014, 12
TRACK1, P1, 20/2/2014, 10
TRACK2, P1, 15/1/2014, 9
TRACK1, P2, 2/1/2014, 11
TRACK2, P2, 14/1/2014, 13
TRACK1, P2, 21/2/2014, 8
TRACK2, P2, 16/1/2014, 6
What I would, ideally, like to see as a result is something like this:
P1, TRACK1, 20/2/2014, 10, TRACK2, 15/1/2014, 9
P2, TRACK1, 21/2/2014, 8, TRACK2, 16/1/2014, 6
Or, in other words, for each person, the most recent date and distance for that date for each track in one row.
Hope someone can understand this and offer a solution too :)
Cheers,
Pierre
Try this:
SELECT T1.Person, T1.Track, MAX(T1.Date), MIN(T1.Distance),
T2.Track, MAX(T2.Date), MIN(T2.Distance)
FROM Distances AS T1 INNER JOIN
Distances AS T2 ON T1.Person = T2.Person
WHERE T1.Track <> T2.Track AND T1.Track = 'Track1'
GROUP BY T1.Track, T1.Person, T2.Track
The output result of the query is showing exactly the same of your expected result.
Try combining the table by itself and connecting them with the common column.
In your case you want Person.
Select t1.Person,
t1.Tract,
t1.Date,
t1.Distance,
t2.Tract,
t2.date,
t2.Distance
From table_name t1, table_name t2
WHERE t1.Person = t2.Person;
Try this:
SELECT DISTINCT ON ("Person", "Track") *
FROM "Table"
ORDER BY "Person", "Date" DESC NULLS LAST;
Here is a query to get the records needed. First get the maximum date per track and person. Then join with the table to get the complete record.
If you know beforehand which tracks you will get, you can use a pivot query for this. As I've never done this, I ask you to look this up yourself. However, as mentioned in my comment to your request, I would use a programming language (C#, Java, PHP or whatever) to care about that.
select d.track, d.person, d.date, d.distance
from
(
select track, person, max(distances.date) as `date`
from distances
group by track, person
) lastd
inner join distances d on d.track = lastd.track and d.person = lastd.person and d.date = lastd.date
order by d.track, d.person;
BTW: date is a reserved keyword. I would not recommend to use it for a column name. Whenever you use it without a qualifier you will have to use those strange quotes.
Look for ROW_NUMBER() and OVER PARITION BY.
Idea is something like (I did not try to run this query):
;WITH
data AS
(
SELECT
*,
-- returns number for each pair of person and track starting from most recent date
--Code enhanced at here
row_number() over (partition BY person, track order by dte DESC) nr
FROM distances
)
SELECT
*
FROM
data
WHERE
nr = 1 -- we want just the most recent one
ORDER BY
person, -- "group by" person
track ;
It's still doesn't support showing one row for each person...
I don't think you can do it with SQL (because of unknown number of tracks).
There is PIVOT/UNPIVOT, but I don't think it fits here.
WITH CTE AS
(
Select P1.Track,P1.Person,ROW_NUMBER() OVER (Partition by Person,Track Order by Date
Desc) AS RN1
,Date,Distance
from Distances P1
)Select T.Person,T.Track1,T.T1Date
,T.T1Distance,T.Track2,T.T2Date,T.T2Distance
From (
Select C1.Person,C1.Track AS 'Track1',C1.Date AS 'T1Date',
C1.Distance 'T1Distance',
C2.Track AS 'Track2',C2.Date As 'T2Date',C2.Distance 'T2Distance',
ROW_NUMBER() OVER (Partition BY C1.Person Order by C1.Date Desc) RNX
from
CTE C1
JOIN
CTE C2 ON C1.RN1=1 AND C2.RN1=1
AND C1.Person=C2.Person
AND C1.Track<>C2.Track
)t Where t.RNX=1
you may also use dynamic query to achieve your expected result :)
DECLARE #nCount INT
DECLARE #nStart INT
DECLARE #Query NVARCHAR(MAX) =' '
DECLARE #sPerson NVARCHAR(MAX)
DECLARE #sTrack NVARCHAR(MAX)
SET #nCount = (SELECT COUNT(DISTINCT(person)) FROM DISTANCES)
SET #nStart = 1
WHILE #nStart <= #nCount
BEGIN
SET #sPerson = (SELECT PERSON FROM (
SELECT PERSON, ROW_NUMBER() OVER (ORDER BY PERSON) RN FROM (
SELECT DISTINCT(PERSON) FROM DISTANCES
) T1
) T2 WHERE RN = #nStart
)
SET #Query = #Query + '
SELECT '''+#sPerson+''' + '','' + STUFF( '','' +(
SELECT TRACK + '', '' + DATE + '', '' + DISTANCE FROM (
SELECT TRACK, DATE,DISTANCE,
ROW_NUMBER() OVER (PARTITION BY TRACK ORDER BY DATE DESC) RN FROM (
SELECT TRACK,date,DISTANCE FROM DISTANCES WHERE PERSON = '''+#sPerson+'''
) T1
) T2
WHERE RN = 1 FOR XML PATH('''')
),1,1,''''
)
'
IF(#nStart != #nCount)
SET #Query = #Query + ' UNION ALL '
SET #nStart = #nStart + 1
END
EXEC SP_EXECUTESQL #Query
To have a general query it need to be dynamic
DECLARE #query AS NVARCHAR(MAX)
DECLARE #pivotCols AS NVARCHAR(MAX)
DECLARE #cols AS NVARCHAR(MAX)
SELECT #pivotCols = STUFF((SELECT DISTINCT ',' + QUOTENAME([Track])
FROM Distances
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)') , 1, 1, '')
;WITH T AS (
SELECT Track
, _ID = CAST(Row_Number() OVER (ORDER BY Track) AS VARCHAR)
FROM Distances
GROUP BY Track
)
SELECT #Cols = STUFF((
SELECT ', Track_' + _ID + ' = ''' + Track + ''''
+ ', LastRun_' + _ID + ' = ' + QUOTENAME([Track])
+ ', Distance_' + _ID + '
= SUM(CASE WHEN d.Date = ' + QUOTENAME([Track]) + '
AND d.Track = ''' + Track + '''
THEN d.Distance ELSE NULL END)'
FROM T FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)') , 1, 1, '')
SELECT #query = '
With LR AS (
SELECT Person, ' + #pivotCols + '
FROM (SELECT Track, Person, [Date] FROM Distances) d
PIVOT (MAX([Date]) FOR Track IN (' + #pivotCols + ')) pvt
)
SELECT d.Person, ' + #Cols + '
FROM Distances d
INNER JOIN LR ON d.Person = LR.Person AND d.Date IN (' + #pivotCols + ')
GROUP BY d.Person, ' + #pivotCols + ''
execute(#query);
SQLFiddle demo
The first query generate the list of field for the PIVOT.
The second one generate the fields for the compound query.
The PIVOT is used to get, for every person, the last run on every track, that is than joined back to the base data to get the distance

Dynamic SELECT statement, generate columns based on present and future values

Currently building a SELECT statement in SQL Server 2008 but would like to make this SELECT statement dynamic, so the columns can be defined based on values in a table. I heard about pivot table and cursors, but seems kind of hard to understand at my current level, here is the code;
DECLARE #date DATE = null
IF #date is null
set # date = GETDATE() as DATE
SELECT
Name,
value1,
value2,
value3,
value4
FROM ref_Table a
FULL OUTER JOIN (
SELECT
PK_ID ID,
sum(case when FK_ContainerType_ID = 1 then 1 else null) Box,
sum(case when FK_ContainerType_ID = 2 then 1 else null) Pallet,
sum(case when FK_ContainerType_ID = 3 then 1 else null) Bag,
sum(case when FK_ContainerType_ID = 4 then 1 else null) Drum
from
Packages
WHERE
#date between PackageStart AND PackageEnd
group by PK_ID ) b on a.Name = b.ID
where
Group = 0
The following works great for me , but PK_Type_ID and the name of the column(PackageNameX,..) are hard coded, I need to be dynamic and it can build itself based on present or futures values in the Package table.
Any help or guidance on the right direction would be greatly appreciated...,
As requested
ref_Table (PK_ID, Name)
1, John
2, Mary
3, Albert
4, Jane
Packages (PK_ID, FK_ref_Table_ID, FK_ContainerType_ID, PackageStartDate, PackageEndDate)
1 , 1, 4, 1JAN2014, 30JAN2014
2 , 2, 3, 1JAN2014, 30JAN2014
3 , 3, 2, 1JAN2014, 30JAN2014
4 , 4, 1, 1JAN2014, 30JAN2014
ContainerType (PK_ID, Type)
1, Box
2, Pallet
3, Bag
4, Drum
and the result should look like this;
Name Box Pallet Bag Drum
---------------------------------------
John 1
Mary 1
Albert 1
Jane 1
The following code like I said works great, the issue is the Container table is going to grow and I need to replicated the same report without hard coding the columns.
What you need to build is called a dynamic pivot. There are plenty of good references on Stack if you search out that term.
Here is a solution to your scenario:
IF OBJECT_ID('tempdb..##ref_Table') IS NOT NULL
DROP TABLE ##ref_Table
IF OBJECT_ID('tempdb..##Packages') IS NOT NULL
DROP TABLE ##Packages
IF OBJECT_ID('tempdb..##ContainerType') IS NOT NULL
DROP TABLE ##ContainerType
SET NOCOUNT ON
CREATE TABLE ##ref_Table (PK_ID INT, NAME NVARCHAR(50))
CREATE TABLE ##Packages (PK_ID INT, FK_ref_Table_ID INT, FK_ContainerType_ID INT, PackageStartDate DATE, PackageEndDate DATE)
CREATE TABLE ##ContainerType (PK_ID INT, [Type] NVARCHAR(50))
INSERT INTO ##ref_Table (PK_ID,NAME)
SELECT 1,'John' UNION
SELECT 2,'Mary' UNION
SELECT 3,'Albert' UNION
SELECT 4,'Jane'
INSERT INTO ##Packages (PK_ID, FK_ref_Table_ID, FK_ContainerType_ID, PackageStartDate, PackageEndDate)
SELECT 1,1,4,'2014-01-01','2014-01-30' UNION
SELECT 2,2,3,'2014-01-01','2014-01-30' UNION
SELECT 3,3,2,'2014-01-01','2014-01-30' UNION
SELECT 4,4,1,'2014-01-01','2014-01-30'
INSERT INTO ##ContainerType (PK_ID, [Type])
SELECT 1,'Box' UNION
SELECT 2,'Pallet' UNION
SELECT 3,'Bag' UNION
SELECT 4,'Drum'
DECLARE #DATE DATE, #PARAMDEF NVARCHAR(MAX), #COLS NVARCHAR(MAX), #SQL NVARCHAR(MAX)
SET #DATE = '2014-01-15'
SET #COLS = STUFF((SELECT DISTINCT ',' + QUOTENAME(T.[Type])
FROM ##ContainerType T
FOR XML PATH, TYPE).value('.', 'NVARCHAR(MAX)'),1,1,'')
SET #SQL = 'SELECT [Name], ' + #COLS + '
FROM (SELECT [Name], [Type], 1 AS Value
FROM ##ref_Table R
JOIN ##Packages P ON R.PK_ID = P.FK_ref_Table_ID
JOIN ##ContainerType T ON P.FK_ContainerType_ID = T.PK_ID
WHERE #DATE BETWEEN P.PackageStartDate AND P.PackageEndDate) X
PIVOT (COUNT(Value) FOR [Type] IN (' + #COLS + ')) P
'
PRINT #COLS
PRINT #SQL
SET #PARAMDEF = '#DATE DATE'
EXEC SP_EXECUTESQL #SQL, #PARAMDEF, #DATE=#DATE
Output:
Name Bag Box Drum Pallet
Albert 0 0 0 1
Jane 0 1 0 0
John 0 0 1 0
Mary 1 0 0 0
Static Query:
SELECT [Name],[Box],[Pallet],[Bag],[Drum] FROM
(
SELECT *
FROM
(
SELECT rf.Name,cnt.[Type], pk.PK_ID AS PKID, rf.PK_ID AS RFID
FROM ref_Table rf INNER JOIN Packages pk ON rf.PK_ID = pk.FK_ref_Table_ID
INNER JOIN ContanerType cnt ON cnt.PK_ID = pk.FK_ContainerType_ID
) AS SourceTable
PIVOT
(
COUNT(PKID )
FOR [Type]
IN ( [Box],[Pallet],[Bag],[Drum])
) AS PivotTable
) AS Main
ORDER BY RFID
Dynamic Query:
DECLARE #columnList nvarchar (MAX)
DECLARE #pivotsql nvarchar (MAX)
SELECT #columnList = STUFF(
(
SELECT ',' + '[' + [Type] + ']'
FROM ContanerType
FOR XML PATH( '')
)
,1, 1,'' )
SET #pivotsql =
N'SELECT [Name],' + #columnList + ' FROM
(
SELECT *
FROM
(
SELECT rf.Name,cnt.[Type], pk.PK_ID AS PKID, rf.PK_ID AS RFID
FROM ref_Table rf INNER JOIN Packages pk ON rf.PK_ID = pk.FK_ref_Table_ID
INNER JOIN ContanerType cnt ON cnt.PK_ID = pk.FK_ContainerType_ID
) AS SourceTable
PIVOT
(
COUNT(PKID )
FOR [Type]
IN ( ' + #columnList + ')
) AS PivotTable
) AS Main
ORDER BY RFID;'
EXEC sp_executesql #pivotsql
Following my tutorial below will help you to understand the PIVOT functionality:
We write sql queries in order to get different result sets like full, partial, calculated, grouped, sorted etc from the database tables. However sometimes we have requirements that we have to rotate our tables. Sounds confusing?
Let's keep it simple and consider the following two screen grabs.
SQL Table:
Expected Results:
Wow, that's look like a lot of work! That is a combination of tricky sql, temporary tables, loops, aggregation......, blah blah blah
Don't worry let's keep it simple, stupid(KISS).
MS SQL Server 2005 and above has a function called PIVOT. It s very simple to use and powerful. With the help of this function we will be able to rotate sql tables and result sets.
Simple steps to make it happen:
Identify all the columns those will be part of the desired result set.
Find the column on which we will apply aggregation(sum,ave,max,min etc)
Identify the column which values will be the column header.
Specify the column values mentioned in step3 with comma separated and surrounded by square brackets.
So, if we now follow above four steps and extract information from the above sales table, it will be as below:
Year, Month, SalesAmount
SalesAmount
Month
[Jan],[Feb] ,[Mar] .... etc
We are nearly there if all the above steps made sense to you so far.
Now we have all the information we need. All we have to do now is to fill the below template with required information.
Template:
Our SQL query should look like below:
SELECT *
FROM
(
SELECT SalesYear, SalesMonth,Amount
FROM Sales
) AS SourceTable
PIVOT
(
SUM(Amount )
FOR SalesMonth
IN ( [Jan],[Feb] ,[Mar],
[Apr],[May],[Jun] ,[Jul],
[Aug],[Sep] ,[Oct],[Nov] ,[Dec])
) AS PivotTable;
In the above query we have hard coded the column names. Well it's not fun when you have to specify a number of columns.
However, there is a work arround as follows:
DECLARE #columnList nvarchar (MAX)
DECLARE #pivotsql nvarchar (MAX)
SELECT #columnList = STUFF(
(
SELECT ',' + '[' + SalesMonth + ']'
FROM Sales
GROUP BY SalesMonth
FOR XML PATH( '')
)
,1, 1,'' )
SET #pivotsql =
N'SELECT *
FROM
(
SELECT SalesYear, SalesMonth,Amount
FROM Sales
) AS SourceTable
PIVOT
(
SUM(Amount )
FOR SalesMonth
IN ( ' + #columnList +' )
) AS PivotTable;'
EXEC sp_executesql #pivotsql
Hopefully this tutorial will be a help to someone somewhere.
Enjoy coding.