Getting PIVOT using GroupBy - sql

I have this table:
Person Job Day EndDay
1 101 1 12
1 102 2 11
1 103 3 11
3 101 1 11
3 102 2 11
3 JobOff 3 11
2 101 1 11
2 102 2 11
2 103 3 11
2 JobOff 4 11
...
PS: My days are upto 'N', which I don't know. So, static SQL is not feasible here.
And I need to change it to this form:
Day (Columns)
Person (Rows)
Person 1 2 3 4
1 101 102 103 N/A
2 101 102 103 JobOff
3 101 102 JobOff N/A
Any help would be appreciated. I am very new in SQL.
Thanks to Roman, I have this S.Procedure:
CREATE PROCEDURE [dbo].[MyStoredProcedure] AS
BEGIN
declare #stmt nvarchar(max)
select #stmt =
isnull(#stmt + ', ', '') +
'isnull(max(case when [Day] = ' + cast([Day_I] as nvarchar(max)) +
' then Job end), ''N/A'') as [' + cast([Day_I] as nvarchar(max)) + ']'
from (select distinct [Day_I] from dbo.Solution) as t
order by [Day_I]
select #stmt = '
select
Person_I, ' + #stmt +'
from dbo.Solution_Format
group by Person_I'
END
But when I am executing this stored procedure like this:
exec MyStoredProcedure
Its showing me NO results. No errors!

select
Person,
isnull(max(case when [Day] = 1 then Job end), 'N/A') as [1],
isnull(max(case when [Day] = 2 then Job end), 'N/A') as [2],
isnull(max(case when [Day] = 3 then Job end), 'N/A') as [3],
isnull(max(case when [Day] = 4 then Job end), 'N/A') as [4]
from Table1
group by Person
sql fiddle demo
it's also possible to do this with dynamic SQL if you need this to arbitrary number of columns:
create procedure [dbo].[MyStoredProcedure]
as
begin
declare #stmt nvarchar(max)
select #stmt =
isnull(#stmt + ', ', '') +
'isnull(max(case when [Day] = ' + cast([Day] as nvarchar(max)) +
' then Job end), ''N/A'') as [' + cast([Day] as nvarchar(max)) + ']'
from (select distinct [Day] from Table1) as t
order by [Day]
select #stmt = '
select
Person, ' + #stmt +'
from Table1
group by Person'
exec sp_executesql
#stmt = #stmt
end
sql fiddle demo

One way to do this is to use multiple self joins
Select
t1.Person,
COALESCE(t1.Job, 'N/A') [1],
COALESCE(t2.Job, 'N/A' )[2],
COALESCE(t3.Job, 'N/A') [3],
COALESCE(t4.Job, 'N/A') [4]
FROM
table1 t1
LEFT JOIN table1 t2
ON t1.Person = t2.Person
and t2.Day = 2
LEFT JOIN table1 t3
ON t1.Person = t3.Person
and t3.Day = 3
LEFT JOIN table1 t4
ON t1.Person = t4.Person
and t4.Day = 4
WHERE
t1.Day = 1
ORDER BY
t1.Person
DEMO
Another ways is to use MAX(CASE however I'm not a fan of using aggregates like this.
Select
t1.Person,
COALESCE(MAX(CASE WHEN t1.Day = 1 then t1.Job END), 'N/A') [1],
COALESCE(MAX(CASE WHEN t1.Day = 2 then t1.Job END), 'N/A') [2],
COALESCE(MAX(CASE WHEN t1.Day = 3 then t1.Job END), 'N/A') [3],
COALESCE(MAX(CASE WHEN t1.Day = 4 then t1.Job END), 'N/A') [4]
FROM
table1 t1
GROUP BY
t1.Person
DEMO
And lastly there's the pivot clause which I don't really like because of the MAX here
SELECT person,
COALESCE([1], 'N/A') [1],
COALESCE([2], 'N/A') [2],
COALESCE([3], 'N/A') [3],
COALESCE([4], 'N/A') [4]
FROM (SELECT t1.person,
t1.day,
t1.job
FROM table1 t1) p
PIVOT (Max (job)
FOR day IN ( [1],
[2],
[3],
[4]) ) AS pvt
DEMO

Related

What is the SQL code for aggregating values?

I have the following table:
GR WORD NO.
1 A 4
2 B 5
3 C 6
1 G 5
2 H 5
3 I 5
I would like to get the following table:
GR 4 5 6
1 1 1 0
2 0 2 0
3 0 1 1
For each GR column value I count the NO. values.
Here's a dynamic solution:
--Sample data
--CREATE TABLE tbl (GR int, WORD char(1), [NO] int)
--INSERT INTO tbl values
--(1,'A',4),
--(2,'B',5),
--(3,'C',6),
--(1,'G',5),
--(2,'H',5),
--(3,'I',5)
DECLARE #sql NVARCHAR(MAX)
SELECT #sql = '
SELECT *
FROM tbl
PIVOT(
COUNT(WORD) FOR [NO] IN (' +
(SELECT STUFF(
(
SELECT DISTINCT ',' + QUOTENAME(CAST([NO] AS VARCHAR(10)))
FROM tbl
FOR XML PATH('')
)
, 1, 1, ''))
+ ')
) p
'
EXEC sp_executesql #sql
This is a conditional aggregation
select
GR
,[4] = count(case when NO. = 4 then WORD end)
,[5] = count(case when NO. = 5 then WORD end)
,[6] = count(case when NO. = 6 then WORD end)
from YourTable
group by GR
Or a pivot
select *
from YourTable
pivot(
count(WORD) for NO. in ([4],[5],[6])
) p

SQL: Display the table and its job by yes and no

This is my table:
User Table
Id | Username |
---------------
1 | jdoe |
Job Table
Id | Job |
----------------
1 | Waiter |
2 | Office |
3 | Freelance |
User Job Table
Id |UserId | JobId |
--------------------
1 | 1 | 2 |
2 | 1 | 3 |
How to select this table and display it to look like this below:
Id | Username | Waiter| Office | Freelance|
-------------------------------------------
1 | jdoe | No | Yes | Yes |
This is a pretty standard pivot query question, with an additional slight twist. In case a given user does not have a certain type of job assigned, you want to display 'No'. One way to do this is to make use of COALESCE and replace NULL job aggregates.
SELECT u.Id,
u.Username,
COALESCE(MAX(CASE WHEN j.Job = 'Waiter' THEN 'Yes' END), 'No') AS Waiter,
COALESCE(MAX(CASE WHEN j.Job = 'Office' THEN 'Yes' END), 'No') AS Office,
COALESCE(MAX(CASE WHEN j.Job = 'Freelance' THEN 'Yes' END), 'No') AS Freelance
FROM User u
LEFT JOIN User_Job uj
ON u.Id = uj.UserId
LEFT JOIN Job j
ON uj.JobId = j.Id
GROUP BY u.Id,
u.Username
Try this..
WITH cte AS
(
SELECT u.username,job,CASE WHEN uj.jobid IS NULL THEN 'No' ELSE 'yes' END AS jobid
FROM USER u INNER JOIN UserJob uj on u.id = uj.Userid
RIGHT JOIN Job j on j.id = uj.jobid
)
SELECT username,ISNULL(waiter,'no') waiter,
ISNULL(Office,'no') Office,
ISNULL(Freelance,'no') Freelance
FROM cte
PIVOT
(
MAX(jobid) FOR job IN (Waiter, Office,Freelance)
) piv
WHERE username IS NOT NULL
DECLARE #cols AS NVARCHAR(MAX),#Listcols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX)
select #cols = STUFF((SELECT ',' + 'Isnull('+QUOTENAME(t.Job) +',''No'') as ' + t.Job
from Job t
order by Job desc
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
select #Listcols = STUFF((SELECT ',' + QUOTENAME(t.Job)
from Job t
order by Job desc
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = N'SELECT Id, Username,' + #cols + N' from
(
select D1.Id, D1.Username,
COALESCE(CASE WHEN isnull(D2.UserId,0) > 0 THEN ''Yes''
Else ''No''
End, ''No'') as UserId,
D3.Job
from [User] D1
Inner Join User_Job D2
On D1.Id = D2.UserId
Inner Join Job D3
On D2.JobId = D3.Id
) x
pivot
(
max(UserId)
for Job in (' + #Listcols + N')
) p '
exec sp_executesql #query;
If the job items are come from a result.
CREATE TABLE #T1(id INT ,Username VARCHAR(100))
INSERT INTO #T1 SELECT 1,'jdoe'
CREATE TABLE #T2(id INT ,Job VARCHAR(100))
INSERT INTO #T2 VALUES(1,'Waiter'),(2,'Office'),(3,'Freelance')
CREATE TABLE #T3(id INT ,UserId INT ,JobId INT )
INSERT INTO #T3 VALUES(1,1,2),(2,1,3)
DECLARE #col NVARCHAR(max),#sql NVARCHAR(max)
SELECT #col=ISNULL(#col+',[','[')+Job+']' FROM #t2
PRINT #col
SET #sql='
SELECT * FROM (
SELECT uj.Job,uj.Username,CASE WHEN t3.UserId IS NULL THEN ''No'' ELSE ''Yes'' END AS f FROM (
SELECT t2.id AS JobId, t2.Job,t1.id AS UserId, t1.Username
FROM #T1 AS t1,#t2 AS t2
) uj LEFT JOIN #T3 AS t3 ON t3.JobId = uj.JobId AND t3.UserId = uj.UserId
) AS t PIVOT(MAX(f) FOR job IN ('+#col+')) p'
EXEC(#sql)

How to use SQL Server 2005 Pivot based on lookup table

table [Status] has the following data:
ID Status
1 PaymentPending
2 Pending
3 Paid
4 Cancelled
5 Error
====================================
Data Table has the following structure:
ID WeekNumber StatusID
1 1 1
2 1 2
3 1 3
4 2 1
5 2 2
6 2 2
7 2 3
Looking for a Pivot
Week # PaymentPending Pending Paid Cancelled
Week 1 1 1 1 0
Week 2 1 2 1 0
SELECT 'Week '+CAST(coun.WeekNumber AS VARCHAR(10)) [Week #],[PaymentPending],[Pending],[Paid],[Cancelled],[Error] FROM
(SELECT [WeekNumber],[Status] FROM dbo.WeekDetails
INNER JOIN [dbo].[Status] AS s
ON [dbo].[WeekDetails].[StatusID] = [s].[ID]) AS wee
PIVOT (COUNT(wee.[Status]) FOR wee.[Status]
IN ([PaymentPending],[Pending],[Paid],[Cancelled],[Error])) AS Coun
A pivot might look like this:
SELECT * FROM
(SELECT
'Week ' + CAST(D.WeekNumber AS varchar(2)) [Week #],
S.Status
FROM DataTbl D
INNER JOIN Status S ON D.StatusID = S.ID
) Derived
PIVOT
(
COUNT(Status) FOR Status IN
([PaymentPending], [Pending], [Paid], [Cancelled]) -- add [Error] if needed
) Pvt
If you expect the number of items in theStatustable to change you might want to consider using a dynamic pivot to generate the column headings. Something like this:
DECLARE #sql AS NVARCHAR(MAX)
DECLARE #cols AS NVARCHAR(MAX)
SELECT #cols = ISNULL(#cols + ',','') + QUOTENAME(Status)
FROM (SELECT ID, Status FROM Status) AS Statuses ORDER BY ID
SET #sql =
N'SELECT * FROM
(SELECT ''Week '' + CAST(D.WeekNumber AS varchar(2)) [Week #], S.Status
FROM Datatbl D
INNER JOIN Status S ON D.StatusID = S.ID) Q
PIVOT (
COUNT(Status)
FOR Status IN (' + #cols + ')
) AS Pvt'
EXEC sp_executesql #sql;
Sample SQL Fiddle
You can use CASE based aggregation with GROUP BY
SELECT 'Week ' + cast(WeekNumber as varchar(10)) as 'Week#',
SUM ( CASE WHEN StatusId =1 THEN 1 else 0 end) as 'PaymentPending',
SUM ( CASE WHEN StatusId =2 THEN 1 else 0 end) as 'Pending',
SUM ( CASE WHEN StatusId =3 THEN 1 else 0 end) as 'Paid',
SUM ( CASE WHEN StatusId =4 THEN 1 else 0 end) as 'Cancelled'
FROM DataTbl D
GROUP BY 'Week ' + cast(WeekNumber as varchar(10))

Pivot table with more then one record

I have data that is structured as follow:
LocationId, GroupId, DayOfWeek, Count, DatetimeValue15Min
2 9 4 5 2014-01-02 08:15:00.000
2 9 4 5 2014-01-02 09:15:00.000
I want to calculate the mode for each day, the data above already contains the count to know the mode. I have written a query with a pivot.
SELECT
pvt.LocationId, pvt.GroupId, [1], [2], [3], [4],[5]
FROM
#TempResult
PIVOT
(min ([DatetimeValue15Min])
FOR DayOfWeek IN ( [1], [2], [3], [4],[5])) AS pvt
In this case I have two modes but i want to show them both. My query returns in this case just the mode with the minimum value. I know that I can make a second query with the max value but what if i have more than two modes?
The output should be like:
LocationId GroupId 1 2 3 4 5
2 9 08:15, 09:15
I am using SQL Server 2005.
You are almost there. You just need to build the comma-separated list. A little xml type abuse works really well for that.
;WITH
t1 AS ( --Add a grouping id for quick reference
SELECT
[LocationId],[GroupId],[DayOfWeek],[DatetimeValue15Min],
DENSE_RANK() OVER(ORDER BY [LocationId],[GroupId],[DayOfWeek]) [i]
FROM #TempResult
),
t2 AS ( --Build a comma-separated list of all [DatetimeValue15Min] with same grouping id
SELECT [LocationId],[GroupId],[DayOfWeek],
CAST(REPLACE((SELECT CONVERT(time, [DatetimeValue15Min]) AS a FROM t1 WHERE [i] = t.[i] FOR xml PATH('')),'</a><a>',',') AS xml).value('a[1]','varchar(max)') [dtv_list]
FROM t1 t
)
SELECT pvt.LocationId, pvt.GroupId, [1], [2], [3], [4],[5]
FROM t2
PIVOT
(
min ([dtv_list])
FOR DayOfWeek IN ( [1], [2], [3], [4],[5])
) AS pvt
The xml trick works like this:
SELECT [DatetimeValue15Min] FOR XML -> <a>08:15</a><a>09:15</a>
replace '</a><a>' with ',' -> <a>08:15,09:15</a>
extract first node from xml -> '08:15,09:15'
SELECT LocationId, GroupId
, STUFF (
MAX (CASE WHEN DayOfWeek = 1 AND num = 1 THEN Value ELSE '' END)
+ MAX (CASE WHEN DayOfWeek = 1 AND num = 2 THEN Value ELSE '' END), 1, 2, '') AS [1]
, STUFF (
MAX (CASE WHEN DayOfWeek = 2 AND num = 1 THEN Value ELSE '' END)
+ MAX (CASE WHEN DayOfWeek = 2 AND num = 2 THEN Value ELSE '' END), 1, 2, '') AS [2]
, STUFF (
MAX (CASE WHEN DayOfWeek = 3 AND num = 1 THEN Value ELSE '' END)
+ MAX (CASE WHEN DayOfWeek = 3 AND num = 2 THEN Value ELSE '' END), 1, 2, '') AS [3]
, STUFF (
MAX (CASE WHEN DayOfWeek = 4 AND num = 1 THEN Value ELSE '' END)
+ MAX (CASE WHEN DayOfWeek = 4 AND num = 2 THEN Value ELSE '' END), 1, 2, '') AS [4]
, STUFF (
MAX (CASE WHEN DayOfWeek = 5 AND num = 1 THEN Value ELSE '' END)
+ MAX (CASE WHEN DayOfWeek = 5 AND num = 2 THEN Value ELSE '' END), 1, 2, '') AS [5]
FROM (
SELECT LocationId, GroupId, DayOfWeek, ', ' + CONVERT (VARCHAR(5), DatetimeValue15Min, 8) AS Value
, ROW_NUMBER() OVER(PARTITION BY LocationId, GroupId, DayOfWeek ORDER BY DatetimeValue15Min) AS num
FROM #TempResult
) T
GROUP BY LocationId, GroupId
UPD. Solution for unlimited number of values per day.
WITH cte AS (
SELECT LocationId, GroupId, DayOfWeek, CONVERT (VARCHAR(5), DatetimeValue15Min, 8) AS Value
, ROW_NUMBER() OVER(PARTITION BY LocationId, GroupId, DayOfWeek ORDER BY DatetimeValue15Min) AS num
FROM #TempResult
)
SELECT LocationId, GroupId
, MIN (CASE DayOfWeek WHEN 1 THEN Value END) AS [1]
, MIN (CASE DayOfWeek WHEN 2 THEN Value END) AS [2]
, MIN (CASE DayOfWeek WHEN 3 THEN Value END) AS [3]
, MIN (CASE DayOfWeek WHEN 4 THEN Value END) AS [4]
, MIN (CASE DayOfWeek WHEN 5 THEN Value END) AS [5]
FROM (
SELECT DISTINCT c1.LocationId, c1.GroupId, c1.DayOfWeek
, STUFF ( (
SELECT ', ' + c2.Value
FROM cte c2
WHERE c1.LocationId = c2.LocationId AND c1.GroupId = c2.GroupId AND c1.DayOfWeek = c2.DayOfWeek
FOR XML PATH ('')
), 1, 2, '') AS Value
FROM cte c1
) t
GROUP BY LocationId, GroupId

SQL Server concatenation of records with aggregate function in select statement

I have a view vw_XC_DocInfo_1 with columns VId,VName,DocId,Amount,INum. Below is the table data.
Vid VName DocId Amount INum
1 ABC 10 100 INV1
1 ABC 11 10 INV2
1 ABC 12 20 INV3
1 ABC 13 30 INV4
2 XYZ 14 200 INV5
2 XYZ 15 10 INV6
2 XYZ 16 20 INV7
2 XYZ 17 30 INV8
I need to display output like below.
Vid VName DocIdsList Amount INumList
1 ABC 10,11 110 INV1,INV2
1 ABC 12,13 50 INV3,INV4
2 XYZ 14,15 210 INV5,INV6
3 XYZ 16,17 50 INV7,INV8
I have tried different ways but unable to include aggregate function with STUFF function, please find the query I have tired.
with CTE
as (
select top 20 V.VendorId,
V.VendorName,
STUFF((
select top 3 ',' + CONVERT(varchar(MAX), V1.DocumentId)
from vw_XC_DocInfo_1 V1
where V1.VendorID = V.VendorId
order by V1.DocumentId
for xml PATH('')
), 1, 1, '') as DocIdsList,
STUFF((
select top 3 ',' + CONVERT(varchar(MAX), V1.InvoiceNumber)
from vw_XC_DocInfo_1 V1
where V1.VendorID = V.VendorId
order by V1.InvoiceNumber
for xml PATH('')
), 1, 1, '') as InvNumList
from vw_XC_DocInfo_1 V
order by V.VendorID
)
select VendorId,
VendorName,
DocIdsList,
InvNumList
from CTE
group by VendorId,
VendorName,
DocIdsList,
InvNumList
How about something slightly more outside the box?
SELECT VendorId, VendorName,
CASE WHEN COUNT(DocumentId)>1
THEN CAST(MIN(DocumentId) AS VARCHAR(MAX)) + ',' +
CAST(MAX(DocumentId) AS VARCHAR(MAX))
ELSE CAST(MIN(DocumentId) AS VARCHAR(MAX))
END AS DocIdList,
SUM(Amount) Amount,
CASE WHEN COUNT(InvoiceNumber)>1
THEN MIN(InvoiceNumber) + ',' + MAX(InvoiceNumber)
ELSE MIN(InvoiceNumber)
END AS INumList
FROM
(SELECT *,(ROW_NUMBER() OVER (PARTITION BY VendorId
ORDER BY VendorId) - 1) / 2 AS seq
FROM vw_XC_DocInfo_1) AS result
GROUP BY VendorId, VendorName, seq
Demo here.
Would that work for you?
SELECT V.VendorId,
V.VendorName,
STUFF((
select ',' + CONVERT(varchar(MAX), V1.DocumentId)
from vw_XC_DocInfo_1 V1
where V1.VendorID = V.VendorId
order by V1.DocumentId
for xml PATH('')
), 1, 1, '') as DocIdsList,
SUM(V.Amount) as AmountSums,
STUFF((
select ',' + CONVERT(varchar(MAX), V1.InvoiceNumber)
from vw_XC_DocInfo_1 V1
where V1.VendorID = V.VendorId
order by V1.InvoiceNumber
for xml PATH('')
), 1, 1, '') as InvNumList
FROM vw_XC_DocInfo_1 V
GROUP BY V.VendorId, V.VendorName
ORDER BY V.VendorId, V.VendorName
sorry I had many mistakes in my typing...I think I got it thanks to Joachim Isaksson
you could look at coalesce -
http://www.mssqltips.com/sqlservertip/1521/the-many-uses-of-coalesce-in-sql-server/
but after a brief hunt - I think the question here will help more: Concatenate many rows into a single text string?
Look at the answer by Ritesh that uses XML_Path
;with a
as
(
select Vid,VName,DocId,Amount,INum,
row_number() over(partition by vid order by Inum) n
from vw_XC_DocInfo_1
)
select a.Vid, a.vname,
cast(a.docid as varchar(3)) + coalesce(','+cast(b.docid as varchar(9)), '') docid,
a.amount + coalesce(b.amount, 0) amount,
a.INum + coalesce(',' + b.INum, '') INumList
from a left join a b on a.n + 1 = b.n and a.vid = b.vid
where a.n%2 = 1