SQL Server: Pivot - sql-server

Please help me with this. I have the data as this:
ID Name TotalCost IsCorporate
---- ---------------- ---------- -----------
1 Wash, Dry & Fold 175.00 1
2 Hand Wash and Fold 275.00 0
3 Pressing Only 25.00 0
4 Hand Wash and Fold 205.00 1
5 Pressing Only 100.00 0
If IsCorporate = 0 then the Total Cost will align to the Corporate column like this:
ID Wash, Dry & Fold Hand Wash and Fold Pressing Only Corporate
---- ---------------- ----------------- -------------- -----------
1 175.00
2 275.00
3 25.00
4 205.00
5 100.00
This is my stored procedure code:
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[ReportSales]
AS
BEGIN
SELECT
IDJO, ISCORP, ST.[Wash, Dry & Fold], ST.[Pressing Only],
ST.[Dry Clean], ST.[Hand Wash and Fold], ST.[Wash, Dry & Press],
ST.[Stain Removal], ST.[Hand Wash and Press], CORPORATE
FROM
(SELECT
JO.Id AS IDJO, CI.Active AS ISCORP, ST.Name,
ISNULL(JO.TotalCost, 0) AS TC
FROM
JobOrders JO
INNER JOIN
ClientInformations CI ON JO.ClientId = CI.Id
INNER JOIN
JobOrderDetails JOD ON JO.Id = JOD.JOrderId
INNER JOIN
ServiceTypes ST ON JOD.ServiceId = ST.Id
INNER JOIN
Payments P ON JO.Id = P.JobOrderId
INNER JOIN
PaymentStatus PS ON JO.PaymentStatusId = PS.Id
INNER JOIN
Status S ON JO.StatusId = s.Id) AS J
PIVOT
(SUM(TC) for Name IN ([Wash, Dry & Fold], [Pressing Only], [Dry
Clean], [Hand Wash and Fold], [Wash, Dry & Press], [Stain Removal],
[Hand Wash and Press], [Corporate]) ) AS ST
END

Use dynamic column collection to select PIVOT Data, because it gives you any new column value added in table, suppose after 2-3 days if new Name say for XYZ added in your table even that it show your new column in PIVOT result:
CREATE TABLE ReportSales
(
ID INT,
Name VARCHAR(50),
TotalCost DECIMAL(10,2),
IsCorporate BIT
)
INSERT INTO ReportSales VALUES(1,'Wash, Dry & Fold',175.00,1)
,(2,'Hand Wash and Fold',275.00,0)
,(3,'Pressing Only',25.00,0)
,(4,'Hand Wash and Fold',205.00,1)
,(5,'Pressing Only',100.00,0)
DECLARE #Name AS NVARCHAR(MAX),#Query AS NVARCHAR(MAX);
SET #Name = STUFF((SELECT distinct ',' + QUOTENAME(Name)
FROM ReportSales c
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
SET #Query =
'IF OBJECT_ID(''tempdb..#tblRS'') IS NOT NULL
DROP TABLE #tblRS
SELECT
ID,
RS.Name,
TotalCost,
ISCorporate,
CASE RS.IsCorporate WHEN 1 THEN 0 ELSE RS.TotalCost END AS Corporate
INTO #tblRS
FROM ReportSales RS
SELECT ID, ' + #Name + ',Corporate from
(
SELECT
*,
CASE ISCorporate WHEN 1 THEN TotalCost ELSE 0 END AS NewTotalCost
FROM #tblRS
) x
pivot
(
SUM(TotalCost)
FOR Name in (' + #Name + ')
) p '
PRINT(#query)
EXECUTE(#query)
#Name : It will give you your column list on which you wants to apply SUM

Related

Join SQL Server tables and add column values as table headers

I have 4 tables in Microsoft SQL Server 2019 Express:
Table Invoices
InvoiceID InvoiceType InvoiceDate InvoiceSeries InvoiceApology
-----------|-----------|-----------|-------------|--------------
1 InvType1 1/1/2020 InvSeries1 InvApology1
2 InvType2 1/2/2020 InvSeries2 InvApology2
Table InvoiceDescriptions
InvoiceDescriptionID InvoiceID AccountingCodeID BookTitleID Amount VatAmount TotalAmount
--------------------|---------|----------------|-----------|-------|----------|-------------
1 1 3 1 100,00 4,00 104,00
2 2 1 2 50,00 2,00 52,00
Table AccountingCodes
AccountingCodeID AccountingCode AccountingCodeDescription
-----------------|--------------|-------------------------
1 64.01.0000 AccDescription1
2 64.02.0000 AccDescription2
3 64.03.0000 AccDescription3
Table BookTitles
BookTitleID BookTitle
------------|------------
1 BookTitle1
2 BookTitle2
What i am trying to achieve without success is to make a query to show me a table like the one below
Result Table(not create the actual table, only to show it with the query and the BookTitles are going to be more day by day and need to be updated dynamically)
InvoiceID InvoiceDate InvoiceNumber InvoiceApology TotalAmount BookTitle1 BookTitle2
----------|------------|------------ |--------------|------------|----------|----------
1 1/1/2020 1 InvApology1 104,00 100,00
2 1/2/2020 2 InvApology2 52,00 50,00
Finally I found the solution after many tries with this block of code
IF OBJECT_ID('tempdb..##TempAccountingBook') IS NOT NULL
DROP TABLE ##TempAccountingBook
IF OBJECT_ID('tempdb..##TempAccountingBookReport') IS NOT NULL
DROP TABLE ##TempAccountingBookReport
SELECT
InvoiceDescriptions.InvoiceID, Invoices.InvcoiceDate, InvoiceSeries,
InvoiceNumber, InvoiceApology, TotalAmount, Amount, BookTitle, VatValue
INTO
##TempAccountingBook
FROM
Invoices
JOIN
InvoiceDescriptions ON InvoiceDescriptions.InvoiceID = Invoices.InvoiceID
JOIN
BookTitles ON InvoiceDescriptions.BookTitleID = BookTitles.BookTitleID
DECLARE #SQLQUERY AS NVARCHAR(MAX)
DECLARE #PivotColumns AS NVARCHAR(MAX)
SELECT #PivotColumns = COALESCE(#PivotColumns + ',','') + QUOTENAME(BookTitle)
FROM [dbo].[BookTitles]
SET #SQLQUERY = N'SELECT [InvoiceID], [InvcoiceDate], [InvoiceSeries], [InvoiceNumber], [InvoiceApology], [TotalAmount],' + #PivotColumns +' , [VatValue]
INTO ##TempAccountingBookReport
FROM ##TempAccountingBook
PIVOT (MAX([Amount])
FOR [BookTitle] IN (' + #PivotColumns +')) AS Q'
EXEC sp_executesql #SQLQUERY
SELECT * FROM ##TempAccountingBookReport

Converting rows to columns

I have a query with the columns 'Name', 'Amount', and 'ReasonId'. I want to sum the amount and put the reasons on one row to keep every name to a single line. There are about 50 distinct ReasonId's so I do not want to name the column the name of the ReasonId's. Instead, I would like to name the columns 'Reason1', 'Reason2', 'Reason3', and 'Reason4'. One single name can have up to 4 different reasons.
I have this:
Name Amount ReasonId
-------------------------
Bob $5 7
Bob $8 6
John $2 8
John $5 9
John $3 9
John $8 4
I want to produce the following:
Name Amount Reason1 Reason2 Reason3 Reason4
-----------------------------------------------------
Bob $13 7 6 NULL NULL
John $18 8 9 4 NULL
One way to do this is to use the dense_rank window function to number the rows, and then use conditional aggregation to put the reason in the correct columns.
I can't see anything that would give the specific order of the reason columns though, maybe there is some column missing that provides the order?
with cte as (
select
name,
reasonid,
amount,
dense_rank() over (partition by name order by reasonid) rn
from your_table
)
select
name,
sum(amount) amount,
max(case when rn = 1 then reasonid end) reason1,
max(case when rn = 2 then reasonid end) reason2,
max(case when rn = 3 then reasonid end) reason3,
max(case when rn = 4 then reasonid end) reason4
from cte
group by name
If you have some column that gives the order you want then change the order by clause used in the dense_rank function.
Sample SQL Fiddle (using PG as MSSQL seems to be offline).
The output from the query above would be:
| name | amount | reason1 | reason2 | reason3 | reason4 |
|------|--------|---------|---------|---------|---------|
| Bob | 13 | 6 | 7 | (null) | (null) |
| John | 18 | 4 | 8 | 9 | (null) |
You could also use a pivot to achieve this; if you know the columns you can enter them in the script, but if not, you can use dynamic sql (there are reasons why you might want to avoid the dynamic solution).
The advantage of this route is that you can enter the column list in a table and then changes to that table will result in changes to your output with change to the script involved. The disadvantages are all those associated with dynamic SQL.
In the interests of variation, here is a dynamic SQL solution using temp tables to hold your data, since a different possibility has been provided:
-- set up your data
CREATE TABLE #MyTab (Name VARCHAR(4), Amount INT, ReasonId INT)
CREATE TABLE #AllPossibleReasons (Id INT,Label VARCHAR(10))
INSERT #AllPossibleReasons
VALUES
(1,'Reason1')
,(2,'Reason2')
,(3,'Reason3')
,(4,'Reason4')
,(5,'Reason5')
,(6,'Reason6')
,(7,'Reason7')
,(8,'Reason8')
,(9,'Reason9')
INSERT #MyTab
VALUES
('Bob',7,7)
,('Bob',8,6)
,('John',2,8)
,('John',5,9)
,('John',3,9)
,('John',8,4)
-----------------------------------------------------------------------------
-- The actual query
DECLARE #ReasonList VARCHAR(MAX) = ''
DECLARE #SQL VARCHAR(MAX)
SELECT #ReasonList = #ReasonList + ',' + QUOTENAME(Label)
FROM #AllPossibleReasons
SET #ReasonList = SUBSTRING(#ReasonList,2,LEN(#ReasonList))
SET #SQL =
'SELECT Name,Value,' + #ReasonList + ' FROM
(SELECT
M.Name,SUM(Amount) AS This, Label, SUM(Total.Value) AS Value
FROM
#MyTab AS M
INNER JOIN #AllPossibleReasons AS Reason ON M.ReasonId = Reason.Id
INNER JOIN(SELECT T.Name, SUM(Amount)Value
FROM #MyTab T GROUP BY T.Name) AS Total ON M.Name = Total.Name
GROUP BY M.Name, Reason.Label) AS Up
PIVOT (SUM(THis) FOR Label IN (' + #ReasonList + ')) AS Pvt'
EXEC (#SQL)
DROP TABLE #AllPossibleReasons
DROP TABLE #MyTab
Working from the information in ListAGG in SQLSERVER, I came up with this somewhat ugly example:
with tbl1 as (
-- Set up initial data set
select 'Bob' name, 5 amount, 7 ReasonId
union all select 'Bob' , 3, 4
union all select 'Bob', 2, 1
union all select 'Brian', 8, 2
union all select 'Bob', 6, 4
union all select 'Brian', 1, 3
union all select 'Tim', 2, 2)
, TBL2 AS ( -- Add a blank to separate the concatenation
SELECT NAME
, AMOUNT
, CAST(ReasonId as varchar) + ' ' ReasonId from tbl1
)
select ta.name
, Total
, ReasonIds from (
(select distinct name, stuff((select distinct '' + t2.ReasonId from tbl2 t2
where t1.name = t2.name
for xml path(''), type).value('.','NVARCHAR(MAX)'),1,0,' ') ReasonIds from tbl2 t1) ta
inner join ( select name, sum(amount) Total from tbl1 group by name) tb on ta.name = tb.name) ;
This converts TBL1 to the following:
name Total ReasonIds
Bob 16 1 4 7
Brian 9 2 3
Tim 2 2

Sequential SQL inserts when triggered by CROSS APPLY

This process has several steps which are reflected in various tables of a database:
Production --> UPDATE to the inventory table using something like
UPDATE STOR SET
STOR.BLOC1 = T.BLOC1,
STOR.BLOC2 = T.BLOC2,
STOR.BLOC3 = T.BLOC3,
STOR.PRODUCTION = T.PROD,
STOR.DELTA = T.DELTA
FROM BLDG B INNER JOIN STOR S
ON S.B_ID = B.B_ID
CROSS APPLY dbo.INVENTORIZE(B.B_ID) AS T;
The above feeds a log table with a TRIGGER like this:
CREATE TRIGGER trgrCYCLE
ON STOR
FOR UPDATE
AS
INSERT INTO dbo.INVT
(TS, BLDG, PROD, ACT, VAL)
SELECT CURRENT_TIMESTAMP, B_ID, PRODUCTION,
CASE WHEN DELTA < 0 THEN 'SELL' ELSE 'BUY' END,
DELTA
FROM inserted WHERE COALESCE(DELTA,0) <> 0
And finally, every update should INSERT a row into a financials table which I added to the TRIGGER above:
INSERT INTO dbo.FINS
(COMPANY, TS, COST2, BAL)
SELECT CORP, CURRENT_TIMESTAMP, COST,
((SELECT TOP 1 BAL FROM FINS WHERE COMPANY = CORP ORDER BY TS DESC)- COST)
FROM inserted WHERE COALESCE(COST,0) <> 0
The problem is with this line:
((SELECT TOP 1 BAL FROM FINS WHERE COMPANY = CORP ORDER BY TS DESC)- COST)
which is meant to calculate the latest balance of an account. But because the CROSS APPLY treats all the INSERTS as a batch, the calculation is done off of the same last record and I get an incorrect balance figure. Example:
COST BALANCE
----------------
1,000 <-- initial balance
-150 850
-220 780 <-- should be 630
What would be the way to solve that? A trigger on the FINS table instead for the balance calculation?
Understanding existing logic in your query
UPDATE statement will fire a trigger only once for a set or batch satisfying join conditions, Inserted statement will have all the records that are being updated. This is because of BATCH processing not because of CROSS APPLY but because of UPDATE.
In this query of yours
SELECT CORP, CURRENT_TIMESTAMP, COST,
((SELECT TOP 1 BAL FROM FINS WHERE COMPANY = CORP ORDER BY TS DESC)- COST)
FROM inserted WHERE COALESCE(COST,0) <> 0
For each CORP from an Outer query, same BAL will be returned.
(SELECT TOP 1 BAL FROM FINS WHERE COMPANY = CORP ORDER BY TS DESC)
That being said, your inner query will be replaced by 1000(value you used in your example) every time CORP = 'XYZ'
SELECT CORP, CURRENT_TIMESTAMP, COST, (1000- COST)
FROM inserted WHERE COALESCE(COST,0) <> 0
Now your inserted statement has all the records that are being inserted. So every record's cost will be subtracted by 1000. Hence you are getting unexpected result.
Suggested solution
As per my understanding, you want to calculate some cumulative frequency kind of thing. Or last running total
Data Preparation for problem statement. Used my dummy data to give you an idea.
--Sort data based on timestamp in desc order
SELECT PK_LoginId AS Bal, FK_RoleId AS Cost, AddedDate AS TS
, ROW_NUMBER() OVER (ORDER BY AddedDate DESC) AS Rno
INTO ##tmp
FROM dbo.M_Login WHERE AddedDate IS NOT NULL
--Check how data looks
SELECT Bal, Cost, Rno, TS FROM ##tmp
--Considering ##tmp as your inserted table,
--I just added Row_Number to apply Top 1 Order by desc logic
+-----+------+-----+-------------------------+
| Bal | Cost | Rno | TS |
+-----+------+-----+-------------------------+
| 172 | 10 | 1 | 2012-12-05 08:16:28.767 |
| 171 | 10 | 2 | 2012-12-04 14:36:36.483 |
| 169 | 12 | 3 | 2012-12-04 14:34:36.173 |
| 168 | 12 | 4 | 2012-12-04 14:33:37.127 |
| 167 | 10 | 5 | 2012-12-04 14:31:21.593 |
| 166 | 15 | 6 | 2012-12-04 14:30:36.360 |
+-----+------+-----+-------------------------+
Alternative logic for subtracting cost from last running balance.
--Start a recursive query to subtract balance based on cost
;WITH cte(Bal, Cost, Rno)
AS
(
SELECT t.Bal, 0, t.Rno FROM ##tmp t WHERE t.Rno = 1
UNION ALL
SELECT c.Bal - t.Cost, t.Cost, t.Rno FROM ##tmp t
INNER JOIN cte c ON t.RNo - 1 = c.Rno
)
SELECT * INTO ##Fin FROM cte;
SELECT * FROM ##Fin
Output
+-----+------+-----+
| Bal | Cost | Rno |
+-----+------+-----+
| 172 | 0 | 1 |
| 162 | 10 | 2 |
| 150 | 12 | 3 |
| 138 | 12 | 4 |
| 128 | 10 | 5 |
| 113 | 15 | 6 |
+-----+------+-----+
You have to tweet your columns little bit to get this functionality into your trigger.
I think you can try a trigger on the Fins.
You can use IDENT_CURRENT('Table')) to take the last primary key from the table and make a select.
I think it's better than "select top 1".
To to take the last balance value, set a variable last_bal = select bal from FINS where primary_key = Ident_Current("FINS")
well
first sql is a game where it work with groups or rather "set" so always you have think about that.
if you work with a simple item is correct, it maybe be better approach
declare #myinsert table(id int identity(1,1), company VArchar(35), ts datetime, cost2 smallmoney, bal smallmoney)
insert into #myinsert(company,ts, cost2, bal)
SELECT CORP, CURRENT_TIMESTAMP, COST,
FROM inserted WHERE COALESCE(COST,0) <> 0
declare #current int
select #current = min(id) from #myinsert
while exists(select * from #myinsert where id = #current)
begin
INSERT INTO dbo.FINS
(COMPANY, TS, COST2, BAL)
SELECT COMPANY, CURRENT_TIMESTAMP, COST,
((SELECT TOP 1 BAL FROM FINS WHERE COMPANY = my.COMPANY ORDER BY TS DESC)- COST)
from #myinsert my where id = #current
select #current = min(id) from #myinsert where id > #current
end
i am not giving you exact query .For a moment forget trigger.Because you are unable to test your query .
I suggest to use Output clause .This will atleast help you to construct proper query and test it.
this query is running ok,(if you can use merge then that is best).
Declare #t table
(
BLOC1,BLOC2,BLOC3 ,PRODUCTION ,DELTA --whatever column is require here
)
UPDATE STOR SET
STOR.BLOC1 = T.BLOC1,
STOR.BLOC2 = T.BLOC2,
STOR.BLOC3 = T.BLOC3,
STOR.PRODUCTION = T.PROD,
STOR.DELTA = T.DELTA
Output inserted.BLOC1 ,inserted.BLOC2, and so on into #t
FROM BLDG B INNER JOIN STOR S
ON S.B_ID = B.B_ID
CROSS APPLY dbo.INVENTORIZE(B.B_ID) AS T;
now you have inserted value in table variable #t
SELECT CORP, CURRENT_TIMESTAMP, COST,
BAL,Row_Number() over(partition by company order by TS desc) RN
FROM #t inner join FINS on COMPANY = CORP
WHERE COALESCE(COST,0) <> 0
Verify this query till here.Think of optimizing or trigger later on.
I think i gave good suggestion.and I guess subtraction is not a problem.I am telling to put everything in output clause and analyze the query and test it.
you can use CTE inside trigger also but how will you test it.
;With CTE as
(
SELECT CORP, CURRENT_TIMESTAMP, COST,BAL
ROW_NUMBER()over(ORDER BY TS DESC )rn
FROM inserted
inner join FINS on COMPANY = CORP
WHERE COALESCE(COST,0) <> 0
)
select * from CTE --check this what you are getting
Something like that, Isn't complete.
CREATE TRIGGER trgrCYCLE
ON STOR
FOR UPDATE
AS
begin
declare #last_bal int
declare #company varchar(50)
declare #ts --type
declare #cost int
declare #bal --type
--etc whatever you need
select #company = company, #ts= ts , #cost = cost , #bal = bal from INSERTED
--others selects and sets
set #last_bal = select bal from dbo.FINS where you_primary_key = IDENT_CURRENT('FINS'))
set #last_bal = #last_bal - #cost
Insert INTO FINS (company, ts, cost2, bal) VALUES (#company, #ts, #cost, #last_bal) where --your conditions
end
If, similar to #Shantanu's method, you could associate a sequence with inserted, the virtual table associated with the trigger you could do this by subtracting all the COSTs that come before the current record.
This could be accomplished by adding a rowversion to STOR, which will be updated automatically with each delete.
Then instead of:
((SELECT TOP 1 BAL FROM FINS WHERE COMPANY = CORP ORDER BY TS DESC)- COST)
from inserted ...
make the rowversion RV, and:
(SELECT SUM(X.B) FROM
(SELECT TOP 1 BAL B
FROM FINS
WHERE COMPANY = CORP
ORDER BY TS DESC
UNION
SELECT -COST B
FROM inserted ii
WHERE ii.RV >= i.RV AND ii.CORP = i.CORP
) AS X)
FROM inserted i WHERE COALESCE(COST,0) <> 0
Should do what you want. You could conceivably do this with a timestamp that was more find-grained than CURRENT_TIMESTAMP which, I believe, goes down only to seconds but that requires you update it in the UPDATE statement. The rowversion may cause problems with your STOR insert statements.

Using PIVOT over a group by query

I've been trying to create a pivot for following query :
select mainstate, customertypeid, count(1) as [counter] from customers group by customertypeid, mainstate
This query should display as many customers types per state, it looks like this (order by doesn't matter) :
State|customertypeid|counter
UT 3 200
CA 3 500
NY 3 300
UT 2 100
CA 2 200
NY 2 120
UT 1 20
CA 1 50
NY 1 30
I've tried to use PIVOT as follow (I'm sure I'm wrong) :
SELECT *
FROM ( select mainstate, customertypeid, count(1) as [counter] from customers where customertypeid in (1,2,3) and mainstate != '' group by customertypeid, mainstate) as NonPivotedDataForReport2
PIVOT
(
COUNT([counter])
FOR mainstate IN ([# of Amb],[# Whole Sale Customers],[# Retail Customers])
) AS PivotedDataForReport2
I'm getting this :
customertypeid|type1|type2|type3
1 0 0 0
2 0 0 0
3 0 0 0
and the report should look like this :
State|type1|type2|type3
UT 20 100 200
CA 50 200 500
NY 30 120 300
*Ps : I don't really want to go back to CASE + SUM Statement,
Thanks a lot!
This will do:
SELECT mainstate [State],
[1] type1,
[2] type2,
[3] type3
FROM ( SELECT mainstate, customertypeid, COUNT(1) [counter]
FROM customers
WHERE customertypeid in (1,2,3)
AND mainstate != ''
GROUP BY customertypeid, mainstate) as NonPivotedDataForReport2
PIVOT(SUM([counter]) FOR customertypeid IN ([1],[2],[3])) AS PivotedDataReport2
This (perhaps slightly edited) should do the job for you without case/sum/pivot. Create a temp table, insert starting data and then dynamically add columns depending on how many customer type ids there is.
declare #s varchar(10), #xx1 varchar(500)
select distinct state into #temp from customers
DECLARE myCursor CURSOR FOR SELECT distinct customertypeid from customers
open MyCursor
FETCH NEXT FROM myCursor into #S
WHILE ##FETCH_STATUS = 0
begin
set #xx1 = 'alter table #temp add ['+#s+'] varchar(5)'
execute sp_executesql #xx1
set #xx1 = 'update a set a.['+#s+'] = coalesce(b.counter,0) from #temp a, customers b where b.customertypeid = '+#s+' and a.state = b.state'
execute sp_executesql #xx1
FETCH NEXT FROM myCursor into #S
End
Close myCursor
DEALLOCATE myCursor
select * from #temp

Create query with dynamic columns from different tables

I'm trying to create a query with dynamic columns, based on data from three tables.
This is the database structure:
STUDENT
studentID int,
studentNumber int,
studentName nvarchar(100).
EXAM:
examID int,
examName varchar(100),
examenDate datetime,
EXAM_REGISTRATION:
studentID int,
examID int,
A record is added to the EXAM_REGISTRATION table when a student has registered for an exam.
What I'm trying to get is a list of all the exams and all the students in a pivot table to see which students have registered for which exams, like this:
Quite frankly I don't know where to start.
I can query everything individually and put it all together but how can I combine it into one query?
I've been researching pivot tables, but every example seems to query only from one table and uses numbers and functions like MIN, AVG etc.
Can someone help me along?
ok lets go
some data to play with
create table #student
(studentID int, studentNumber int, studentName nvarchar(100))
create table #exam
(examID int, examName nvarchar(100), examDate datetime)
create table #examReg
(studentID int, examID int)
insert into #student
values (1, 787878, 'pierwszy')
,(2, 89898, 'drugi')
,(3, 343434, 'trzeci')
,(4, 121212, 'czwarty')
insert into #exam
values (1, 'exPierwszy', GETDATE())
,(2, 'exDrugi', GETDATE())
,(3, 'exTrzeci', GETDATE())
insert into #examReg
values (1,2),(1,3)
, (2,2),(2,3)
,(3,1),(3,2)
,(4,1),(4,2),(4,3)
and now the main part, and explanation
first of all you have to get pivot query
select examName, examDate , min([1]) , min([2]), min([3]) ,min([4])--studentID as studentID, examDate --,studentNumber
from
(select a.studentID , studentNumber, examDate, examName
from #student a
join #examReg b on a.studentID = b.studentID
join #exam c on c.examID = b.examID ) as m
pivot
(min(studentNumber) FOR studentID in ([1],[2],[3],[4])) as t
group by examName, examDate
as you have it , just change it select statement and studentID list in pivot declaration, you have to generate those parts dynamicly , so we just copy previously written query and replace columns with our token
declare #sqlTemplate nvarchar(max) =
'select examName, examDate ##sqlColumnList##
from
(select a.studentID , studentNumber, examDate, examName
from #student a
join #examReg b on a.studentID = b.studentID
join #exam c on c.examID = b.examID ) as m
pivot
(min(studentNumber) FOR studentID in (##sqlStudentIDList##)) as t
group by examName, examDate
'
after that you generate column list and studentID list by concatenting strings in tsql
declare #sqlColumnList nvarchar(max) = ''
select #sqlColumnList += ',min([' + cast(studentID as nvarchar(10)) + ']) as [' + studentName +'(' + cast(studentNumber as nvarchar(10)) + ')]'
from #student
declare #sqlStudentIDList nvarchar(max) = ''
select #sqlStudentIDList += '[' + CAST(studentID as nvarchar(10)) + '],'
from #student
set #sqlStudentIDList = SUBSTRING(#sqlStudentIDList, 0, LEN(#sqlStudentIDList))
select #sqlStudentIDList
once you have it , all you have to do is to replace tokens in previous template
set #sqlTemplate = REPLACE(#sqlTemplate, '##sqlColumnList##', #sqlColumnList)
set #sqlTemplate = REPLACE(#sqlTemplate, '##sqlStudentIDList##', #sqlStudentIDList)
select #sqlTemplate
exec sp_executesql #sqlTemplate
and thats it
if you want to read more about pivot go for msdn
if you want to read about dynamic go for this link
edit: to adjust the query for the question from comment you would have to change #sqlColumnList like that
select #sqlColumnList += ',min(' + QUOTENAME(studentID) + ') as Student' + CAST(studentID as nvarchar(10)) + '_REG,
'''+ studentName + ''' as Student' + cast(studentID as nvarchar(10)) + '_NAME,
'+ cast(studentID as nvarchar(10)) + ' as Student' + cast(studentID as nvarchar(10)) + '_ID'
from #student
This is a pivot of the data. I would perform this slightly different than the other answer. If you know all of the values, then you can hard-code the values.
A static version will be:
select examname,
examendate,
IsNull([Smith, John (14323)], 'false') [Smith, John (14323)],
IsNull([Craft, Peter (14334)], 'false') [Craft, Peter (14334)],
IsNull([Davis, Alan (13432)], 'false') [Davis, Alan (13432)],
IsNull([Newman, Ted (133123)], 'false') [Newman, Ted (133123)]
from
(
select e.examname,
e.examenDate,
s.studentName + ' ('+cast(s.studentnumber as varchar(50))+')' studentNameNum,
'true ' as Flag
from exam e
left join exam_registration er
on e.examid = er.examid
right join student s
on er.studentid = s.studentid
) src
pivot
(
max(flag)
for studentNameNum in ([Smith, John (14323)], [Craft, Peter (14334)],
[Davis, Alan (13432)], [Newman, Ted (133123)])
) piv
See SQL Fiddle with Demo
If your values are unknown then the query will be:
DECLARE #cols AS NVARCHAR(MAX),
#colsNull AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX)
select #cols = STUFF((SELECT distinct ',' + QUOTENAME(s.studentName + ' ('+cast(s.studentnumber as varchar(50))+')')
from student s
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
select #colsNull = STUFF((SELECT distinct ',IsNull(' + QUOTENAME(s.studentName + ' ('+cast(s.studentnumber as varchar(50))+')')+', ''false'')'+' as '+QUOTENAME(s.studentName+' ('+cast(s.studentnumber as varchar(50))+')')
from student s
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = 'SELECT examname,
examenDate,' + #colsNull + ' from
(
select e.examname,
e.examenDate,
s.studentName + '' (''+cast(s.studentnumber as varchar(50))+'')'' studentNameNum,
''true '' as Flag
from exam e
left join exam_registration er
on e.examid = er.examid
right join student s
on er.studentid = s.studentid
) x
pivot
(
max(flag)
for studentNameNum in (' + #cols + ')
) p '
execute(#query)
See SQL Fiddle with Demo
The result will be:
| EXAMNAME | EXAMENDATE | CRAFT, PETER (14334) | DAVIS, ALAN (13432) | NEWMAN, TED (133123) | SMITH, JOHN (14323) |
----------------------------------------------------------------------------------------------------------------------------
| Exam 1 | 2013-01-01 12:00:00 | false | false | true | false |
| Exam 2 | 2013-01-01 14:00:00 | true | false | false | true |
| Exam 3 | 2013-01-02 12:00:00 | true | true | false | false |
| Exam 4 | 2013-01-02 14:00:00 | false | false | true | false |
| Exam 5 | 2013-01-03 12:00:00 | false | false | false | true |

Resources