Related
I have a narrow table containing unique key and source data
Unique_Key
System
1
IT
1
ACCOUNTS
1
PAYROLL
2
IT
2
PAYROLL
3
IT
4
HR
5
PAYROLL
I want to be able to pick a system as a base - in this case IT - then create a dynamic SQL query where it counts:
distinct unique key in the chosen system
proportion of shared unique key with other systems. These systems could be dynamic and there are lot more than 4
I'm thinking of using dynamic SQL and PIVOT to first pick out all the system names outside of IT. Then using IT as a base, join to that table to get the information.
select distinct Unique_Key, System_Name
into #staging
from dbo.data
where System_Name <> 'IT'
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX);
SET #cols = STUFF((SELECT distinct ',' + QUOTENAME(System_Name)
FROM #staging
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = 'SELECT Unique_Key, ' + #cols + ' into dbo.temp from
(
select Unique_Key, System_Name
from #staging
) x
pivot
(
count(System_Name)
for System_Name in (' + #cols + ')
) p '
execute(#query)
select *
from
(
select distinct Unique_Key
from dbo.data
where System_Name = 'IT'
) a
left join dbo.temp b
on a.Unique_Key = b.Unique_Key
So the resulting table is:
Unique_Key
PAYROLL
ACCOUNTS
HR
1
1
1
0
2
1
0
0
3
0
0
0
What I want is one step further:
Distinct Count IT Key
PAYROLL
ACCOUNTS
HR
3
67%
33%
0%
I can do a simple join with specific case when/sum statement but wondering if there's a way to do it dynamically so I don't need to specify every system name.
Appreciate any tips/hints.
You can try to use dynamic SQL as below, I would use condition aggregate function get pivot value then we might add OUTER JOIN or EXISTS condition in dynamic SQL.
I would use sp_executesql instead of exec to avoid sql-injection.
DECLARE #System_Name NVARCHAR(50) = 'IT'
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX),
#parameter AS NVARCHAR(MAX);
SET #parameter = '#System_Name NVARCHAR(50)'
select DISTINCT System_Name
into #staging
from dbo.data t1
WHERE t1.System_Name <> #System_Name
SET #cols = STUFF((SELECT distinct ', SUM(IIF(System_Name = '''+ System_Name+''',1,0)) * 100.0 / SUM(IIF(System_Name = #System_Name,0,1)) ' + QUOTENAME(System_Name)
FROM #staging
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = 'SELECT SUM(IIF(System_Name = #System_Name,0,1)) [Distinct Count IT Key], ' + #cols + ' from dbo.data t1
WHERE EXISTS (
SELECT 1
FROM dbo.data tt
WHERE tt.Unique_Key = t1.Unique_Key
AND tt.System_Name = #System_Name
) '
EXECUTE sp_executesql #query, #parameter, #System_Name
sqlfiddle
When writing Dynamic Query, you start off with a non-dynamic query. Make sure you gets the result of the query is correct before you convert to dynamic query.
For the result that you required, the query will be
with cte as
(
select it.Unique_Key, ot.System_Name
from data it
left join data ot on it.Unique_Key = ot.Unique_Key
and ot.System_Name <> 'IT'
where it.System_Name = 'IT'
)
select [ITKey] = count(distinct Unique_Key),
[ACCOUNTS] = count(case when System_Name = 'ACCOUNTS' then Unique_Key end) * 100.0
/ count(distinct Unique_Key),
[HR] = count(case when System_Name = 'HR' then Unique_Key end) * 100.0
/ count(distinct Unique_Key),
[PAYROLL] = count(case when System_Name = 'PAYROLL' then Unique_Key end) * 100.0
/ count(distinct Unique_Key)
from cte;
Once you get the result correct, it is not that difficult to convert to dynamic query. Use string_agg() or for xml path for those repeated rows
declare #sql nvarchar(max);
; with cte as
(
select distinct System_Name
from data
where System_Name <> 'IT'
)
select #sql = string_agg(sql1 + ' / ' + sql2, ',' + char(13))
from cte
cross apply
(
select sql1 = char(9) + quotename(System_Name) + ' = '
+ 'count(case when System_Name = ''' + System_Name + ''' then Unique_Key end) * 100.0 ',
sql2 = 'count(distinct Unique_Key)'
) a
select #sql = 'with cte as' + char(13)
+ '(' + char(13)
+ ' select it.Unique_Key, ot.System_Name' + char(13)
+ ' from data it' + char(13)
+ ' left join data ot on it.Unique_Key = ot.Unique_Key' + char(13)
+ ' and ot.System_Name <> ''IT''' + char(13)
+ ' where it.System_Name = ''IT''' + char(13)
+ ')' + char(13)
+ 'select [ITKey] = count(distinct Unique_Key), ' + char(13)
+ #sql + char(13)
+ 'from cte;' + char(13)
print #sql;
exec sp_executesql #sql;
db<>fiddle demo
This solution changes the aggregation function of the PIVOT itself.
First, let's add a column [has_it] to #staging that keeps track of whether each Unique_Key has an IT row:
select Unique_Key, System_Name, case when exists(select 1 from data d2 where d2.Unique_Key=d1.Unique_Key and d2.System_Name='IT') then 1 else 0 end as has_it
into #staging
from data d1
where System_Name <> 'IT'
group by Unique_Key, System_Name
Now, the per-System aggregation (sum) of this column divided by the final total unique keys needed (example case=3) returns the requests numbers. Change the PIVOT to the following and it's ready as-is, without further queries:
set #query = ' select *
from
(
select System_Name,cnt as [Distinct Count IT Key],has_it*1.0/cnt as divcnt
from #staging
cross join
(
select count(distinct Unique_Key) as cnt
from dbo.data
where System_Name = ''IT''
)y
) x
pivot
(
sum(divcnt)
for System_Name in (' + #cols + ')
) p'
I have two tables like this:
**tblTagDescription**
and **tblDataLog**
Now I want to show record of any specific group and in one there might be same group for multiple id in tbltagdescription. And id of tbltagdescription is foreign key for tblDataLog as TagDescID.
Here 'Group1' has 10 ID as from 1 to 10. and there might be multiple record for these ID (from 1 to 10) in tbldatalog. I want these ID from 1 to as columns. For this I used pivot:
DECLARE #COlsID NVARCHAR(MAX)
DECLARE #SQL NVARCHAR(MAX)
DECLARE #Group NVARCHAR(50) = 'Group1'
IF OBJECT_ID('tempdb..##MYTABLE') IS NOT NULL
DROP TABLE ##MYTABLE
SELECT
#COlsID = COALESCE(#ColsID + '],[','') + CONVERT(NVARCHAR(5), z.TagDescID)
FROM
(SELECT DISTINCT TOP 50 tblDataLog.TagDescID
FROM tblDataLog
INNER JOIN tblTagDescription ON tblDataLog.TagDescID = tblTagDescription.ID
ORDER BY tblDataLog.TagDescID) z
SET #COlsID='[' + #COlsID + ']'
SET #SQL='select [DATE],SHIFT, ' + #COlsID + ' into ##MYTABLE from ( select [Date], Value,
(CASE
WHEN ((DATEPART(hour,[DATE]))>6 and (DATEPART(hour,[DATE]))<14) THEN ''A''
WHEN ((DATEPART(hour,[DATE]))>=14 and (DATEPART(hour,[DATE]))<22) THEN ''B''
WHEN ((DATEPART(hour,[DATE]))>=22 or (DATEPART(hour,[DATE]))<6) THEN ''C''
END )AS SHIFT
from tblDataLog )d pivot(max(Value) for TagDescID in (' + #COlsID + ')) piv;'
EXEC (#SQL)
Now when I execute this statement, I get an error:
Invalid column name 'TagDescID'
but there is this column in tbldatalog. How to solve this query?
You need TagDescID column in subquery.
DECLARE #COlsID NVARCHAR(MAX) = ''
DECLARE #COlsAlias NVARCHAR(MAX) = ''
DECLARE #SQL NVARCHAR(MAX)
DECLARE #Group NVARCHAR(50) = 'Group1'
SELECT
#COlsID = #ColsID + ',' + z.TagDescID,
#COlsAlias = #COlsAlias + ',' + z.TagDescID + ' AS ' + z.ReportTag
FROM
(SELECT DISTINCT TOP 50 tblDataLog.TagDescID ID, QUOTENAME(CONVERT(NVARCHAR(5), tblDataLog.TagDescID )) TagDescID, QUOTENAME(tblTagDescription.ReportTag) ReportTag
FROM tblDataLog
INNER JOIN tblTagDescription ON tblDataLog.TagDescID = tblTagDescription.ID
ORDER BY tblDataLog.TagDescID
) z
SET #COlsID= STUFF(#COlsID,1,1,'')
SET #COlsAlias= STUFF(#COlsAlias,1,1,'')
SET #SQL='select [DATE],SHIFT, ' + #COlsAlias + ' into ##MYTABLE from ( select [Date], Value, TagDescID,
(CASE
WHEN ((DATEPART(hour,[DATE]))>6 and (DATEPART(hour,[DATE]))<14) THEN ''A''
WHEN ((DATEPART(hour,[DATE]))>=14 and (DATEPART(hour,[DATE]))<22) THEN ''B''
WHEN ((DATEPART(hour,[DATE]))>=22 or (DATEPART(hour,[DATE]))<6) THEN ''C''
END )AS SHIFT
from tblDataLog )d pivot(max(Value) for TagDescID in (' + #COlsID + ')) piv;'
EXEC (#SQL)
I am trying to execute the following query to check the records from the 4 tables and then calling a function,but i get an error near the brackets.It works fine if I use case,but exits when the first condition is met. I need to evaluate for all the tables in the 4 IF's:
select account_id,
(
if exists (select account_id from [dbo].[TEST_R6])
Begin
select dbo.make_indicator(cent,chem,co,dim,lg,pl,strs,vis) + space(1) + 'rr'
End
if exists (select account_id from tbl_noR6)
begin
select dbo.make_indicator(acent,chem,co,dim,lg,pl,str,vis) + space(2) + 'cc'
end
if exists (select account_id from tbl_acct)
begin
select dbo.make_indicator(cent,chem,co,dim,lg,pl,str,vis) + space(3) + 'pp'
end
if exists (select account_id from test_con)
begin
select dbo.make_indicator(cent,chem,co,dim,lg,pl,str,vis) + space(4) + 'no'
end
)as value from CRS_PRODLINE
code works partially with case statement and gives output only for the first case is met and does not check for the others:
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX);
SET #cols = STUFF((SELECT distinct ',' + QUOTENAME(c.A_NAME)
FROM TEST_DEL c
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
SET #query = 'SELECT account_id, ' + #cols + ' from
(select account_id,
(case
when exists (select account_id from [dbo].[TEST_R6])
then
dbo.make_indicator(acent,chem,co,dim,lg,pl,str,vis) + space(1)
when exists (select account_id from tbl_noR6)
then
dbo.make_indicator(acent,chem,co,dim,lg,pl,str,vis) + space(2)
when exists (select account_id from tbl_acct)
then
dbo.make_indicator(acent,chem,co,dim,lg,pl,str,vis) + space(2)
end) as value,
assay_name
from CRS_PRODLINE
) x
pivot
(
MAX(newvalue)
for a_name in (' + #cols + ')
) p '
execute(#query)
Based on your comment that CASE only evaluates once being a problem, this should build a string of all the applicable cases.
select account_id,
(
''
+
case when exists (select account_id from dbo.[TEST_R6]) THEN select dbo.make_indicator(cent,chem,co,dim,lg,pl,strs,vis) + space(1) + 'rr' ELSE '' END
+
case when exists (select account_id from dbo.tbl_noR6) THEN select dbo.make_indicator(acent,chem,co,dim,lg,pl,str,vis) + space(2) + 'cc' ELSE '' END
+
case when exists (select account_id from dbo.tbl_acct) THEN select dbo.make_indicator(cent,chem,co,dim,lg,pl,str,vis) + space(3) + 'pp' ELSE '' END
+
case when exists (select account_id from dbo.test_con) THEN select dbo.make_indicator(cent,chem,co,dim,lg,pl,str,vis) + space(4) + 'no' ELSE '' END
) as value
from CRS_PRODLINE
First of all, if there is any record in the table your exists clause will return true. I will assume that this is intentional and you are not trying to match on a specific account_id.
If you want to use this structure you will have to use dynamic SQL. Something like this:
DECLARE #Sql VARCHAR(8000)
IF EXISTS (SELECT account_id FROM tbl_noR6)
BEGIN
SET #Sql = 'select dbo.make_indicator(acent,chem,co,dim,lg,pl,str,vis) + space(2) + ''cc'''
SET #Sql = #Sql + ')as value from CRS_PRODLINE'
EXEC #Sql
END
IF EXISTS (SELECT account_id FROM tbl_acct)
BEGIN
SET #Sql = 'select dbo.make_indicator(cent,chem,co,dim,lg,pl,str,vis) + space(3) + ''pp'''
SET #Sql = #Sql + ')as value from CRS_PRODLINE'
EXEC #Sql
END
IF EXISTS (SELECT account_id FROM test_con)
BEGIN
SET #Sql = 'select dbo.make_indicator(cent,chem,co,dim,lg,pl,str,vis) + space(4) + ''no'''
SET #Sql = #Sql + ')as value from CRS_PRODLINE'
EXEC #Sql
END
I have the following table
PnlId LineTotalisationId Designation Totalisation
1 A Gross Fees Formule A01+A02+A03+A04+A05
2 A01 GF1 Comptes B01+B02+B03+B04+B05
3 A02 GF2 Comptes C01+C02+C03+C04+C05
4 A03 GF3 Comptes 99991
5 A04 GF4 Comptes 99996
6 A05 GF5 Comptes 999995
14 B1 Perm Comptes 12+14+25
I am looking to get
If Designation=Formule in Totalisation LineTotalisationId name for PnlId contents number so I should concat Totalisation for all Totalisation else I will simply add the row
I try the following code
SELECT Ref.*,
CASE
WHEN Charindex('+', Ref.totalisation) > 0 THEN '+'
WHEN Charindex('/', Ref.totalisation) > 0 THEN '/'
WHEN Charindex('*', Ref.totalisation) > 0 THEN '*'
WHEN Charindex('*', Ref.totalisation) > 0 THEN '-'
END AS Operator
INTO ##ttresults
FROM [dbo].[pnlreference] Ref
WHERE [typetotalisation] = 'Formule'
AND totalisation <> ''
AND designation NOT LIKE '%[0-9]%'
AND designation != ''
SELECT split.linetotalisationid AS PNLParentId,
NULL AS Totalisation
INTO ##tempresults
FROM (SELECT tbl.designation,
tbl.linetotalisationid,
tbl.typetotalisation,
tbl.pnlid,
tbl.totalisation,
Cast('<t>'
+ Replace(tbl.totalisation, tbl.operator, '</t><t>')
+ '</t>' AS XML) x,
tbl.operator
FROM ##ttresults AS tbl) split
CROSS apply x.nodes('/t') t(c)
INNER JOIN [dbo].[pnlreference] Ref
ON Ref.linetotalisationid = t.c.value('.', 'nvarchar(255)')
WHERE Ref.designation LIKE '%[0-9]%'
OR Ref.designation = ''
GROUP BY split.linetotalisationid ;
The code above returns as result
PNLParentId
A
Is there a way to modify it to get totalisation as it is described ?
If your sample data represent your data, you can try below sql
Assuming Panel is your table name and the Line field only have 1 string character
Declare #sql nvarchar(max);
;
set #sql =
'with cte as
(
select
case
when patindex(''%[A-Z]%'',Line) > 0
then
substring(line,patindex(''%[A-Z]%'',Line),1)
end as LineId
,Totalisation
from panel
where line in (' +''''+ (select replace(a.totalisation,'+',''',''') from Panel a where Designation='formule' ) + '''' +')
union all
select
Line as LineId,
Totalisation
from panel
where line not in (' +''''+ (select replace(a.totalisation,'+',''',''') from Panel a where Designation='formule' ) + '''' +')
and Designation <> ''Formule''
'
+ ')
select
distinct
p.pnlId, p.Line, p.TotalisationId--, p.Designation ,P.Totalisation
,LineId, LTRIM(substring(stuff
(
(
select '' | '' + c2.Totalisation from cte c2 where c.LineId = c2.LineId for xml path('''')
)
,1,0,''''
),3,len(stuff
(
(
select '' | '' + c2.Totalisation from cte c2 where c.LineId = c2.LineId for xml path('''')
)
,1,0,''''
)))
) as Totalisation
from cte c
right join panel p on c.LineId = p.Line
where c.Totalisation is not null
'
;
exec(#sql)
/*
RESULT
pnlId Line TotalisationId LineId Totalisation
----------- ----- -------------- ------ --------------------------------------
1 A Gross Fees A 99999 | 99998 | 99991 | 99996 | 999995
14 B1 Perm B1 12+14+25
*/
UPDATED
TO use #roopesh Sample data B1 'Formule'
declare #formula nvarchar(max);
;
with cte as
(
select
distinct 1 as id, p.Totalisation
from
panel2 p
where Designation = 'formule'
)
select
distinct #formula = '''' + Replace(replace(substring(stuff
(
(
select ',' + c2.Totalisation from cte c2 where c.id = c2.id for xml path('')
)
,1,0,''
),2,len(stuff
(
(
select ',' + c2.Totalisation from cte c2 where c.id = c2.id for xml path('')
)
,1,0,''
))),',',''','''),'+',''',''') + ''''
from cte c
;
Declare #sql nvarchar(max);
;
set #sql =
'
;with cte as
(
select
case
when patindex(''%[A-Z]%'',Line) > 0
then
substring(line,patindex(''%[A-Z]%'',Line),1)
end as LineId
,Totalisation
from panel2
where line in (' + #formula +')
union all
select
Line as LineId,
Totalisation
from panel2
where line not in (' + #formula +')
and Designation <> ''Formule''
'
+ ')
select
distinct
p.pnlId, p.Line, p.TotalisationId--, p.Designation , p.totalisation
,LineId, Case when c.totalisation is null and p.designation=''Formule'' then p.totalisation
else
LTRIM(substring(stuff
(
(
select '' | '' + c2.Totalisation from cte c2 where c.LineId = c2.LineId for xml path('''')
)
,1,0,''''
),3,len(stuff
(
(
select '' | '' + c2.Totalisation from cte c2 where c.LineId = c2.LineId for xml path('''')
)
,1,0,''''
)))
)
end as Totalisation
from cte c
right join panel2 p on c.LineId = p.Line
where p.Designation = ''Formule''
'
;
exec(#sql)
This stored procedure comes as a solution for your problem. It is using cursor. May be there is a way to remove the cursor, but could not get till now. So got this solution.
CREATE Procedure [dbo].[spGetResult]
As
Begin
declare #curPNL cursor
declare #pnlid int
declare #Line varchar(10), #TotalisationId varchar(20), #Totalisation varchar(50)
declare #spresult table(PNLId int, Line varchar(10), TotalisationId varchar(20), result varchar(4000));
--declare the cursor
set #curPNL = cursor
for select PnlId, Line, TotalisationId, totalisation
from PNLTable where designation = 'Formule'
open #curPNL
Fetch Next From #curPNL into #pnlId, #Line, #TotalisationId, #Totalisation
While ##FETCH_STATUS = 0
Begin
declare #nsql nvarchar(4000);
declare #table table(tname varchar(50));
declare #result varchar(4000)
delete from #table
--get the totalisation data for specific column
set #nsql = 'select totalisation from PNLTable Where Line in (''' + replace(#Totalisation,'+',''',''') + ''')';
print 'Calling child'
insert into #table
exec(#nsql);
set #result = '';
if not exists (select 1 from #table)
Begin
set #result = replace(#Totalisation,'+','|')
End
else
Begin
--get the values of totalisation in a pipe separated string
select #result = case when #result = '' then '' else #result + '|' end + tname from #table;
End
--insert the values in the temporary table
insert into #spresult(PNLId, Line, TotalisationId, result)
select #pnlid, #Line, #TotalisationId, #result
Fetch Next From #curPNL into #pnlId, #Line, #TotalisationId, #Totalisation
End
close #curPNL
deallocate #curPNL
select * from #spresult;
End
Though the table structure was not very much clear to me. But I used the following script to create the table and insert the data.
CREATE TABLE [dbo].[PNLTable](
[PnlId] [int] NOT NULL,
[Line] [varchar](10) NULL,
[TotalisationId] [varchar](20) NULL,
[Designation] [varchar](20) NULL,
[Totalisation] [varchar](50) NULL,
PRIMARY KEY CLUSTERED
(
[PnlId] ASC
)
)
--insert data
INSERT [PNLTable]
([PnlId], [Line], [TotalisationId], [Designation], [Totalisation])
VALUES (1, N'A', N'Gross Fees', N'Formule', N'A01+A02+A03+A04+A05'), (2, N'A01', N'GF1', N'Comptes', N'99999')
,(3, N'A02', N'GF2', N'Comptes', N'99998'), (4, N'A03', N'GF3', N'Comptes', N'99991'), (5, N'A04', N'GF4', N'Comptes', N'99996')
, (6, N'A05', N'GF5', N'Comptes', N'999995'), (14, N'B1', N'Perm', N'Formule', N'12+14+25')
You have to generate a dynamic sql query using the formular from Designation=Formule instead of using a big static query. Then run the code by using EXEC.
The query could be something like this:
SELECT
(SELECT Totalisation WHERE Line = 'A01')
+
(SELECT Totalisation WHERE Line = 'A02')
+
(SELECT Totalisation WHERE Line = 'A03')
+
(SELECT Totalisation WHERE Line = 'A04')
+
(SELECT Totalisation WHERE Line = 'A05')
FROM [dbo].[pnlreference] AS Ref
WHERE Designation = 'Formule'
AND LEFT(Line, 1) = 'A'
Maybe split the formular content into single entries and operators and load it into a temp table wich contains the formular (perhabs A01 and A02, etc.) and a id.
Then generate the code: The code could be something like this:
EXEC 'SELECT
(SELECT Totalisation WHERE Line = ' + CAST((SELECT Formular FROM #formular WHERE id = 0) AS VARCHAR(MAX)) +')
' + CAST((SELECT Operator FROM #formular WHERE id = 1) AS VARCHAR(MAX)) +'
(SELECT Totalisation WHERE Line = ' + CAST((SELECT Formular FROM #formular WHERE id = 1) AS VARCHAR(MAX)) +')
' + CAST((SELECT Operator FROM #formular WHERE id = 2) AS VARCHAR(MAX)) +'
(SELECT Totalisation WHERE Line = ' + CAST((SELECT Formular FROM #formular WHERE id = 2) AS VARCHAR(MAX)) +')
' + CAST((SELECT Operator FROM #formular WHERE id = 3) AS VARCHAR(MAX)) +'
(SELECT Totalisation WHERE Line = ' + CAST((SELECT Formular FROM #formular WHERE id = 3) AS VARCHAR(MAX)) +')
' + CAST((SELECT Operator FROM #formular WHERE id = 4) AS VARCHAR(MAX)) +'
(SELECT Totalisation WHERE Line = ' + CAST((SELECT Formular FROM #formular WHERE id = 4) AS VARCHAR(MAX)) +')
FROM [dbo].[pnlreference] AS Ref
WHERE Designation = ''Formule''
AND LEFT(Line, 1) = ' + LEFT(CAST((SELECT Formular FROM #formular WHERE id = 0) AS VARCHAR(MAX)), 1);
Let's jump straight into it. Here's the code
SELECT [prov], [201304], [201305], [201306], [201307]
FROM (
SELECT [prov], [arrival], [Amount]
FROM [tblSource]) up
PIVOT (SUM([Amount]) FOR [arrival] IN ([201304], [201305], [201306], [201307])) AS pvt
GO
It brings me back an ever so lovely table. I was wondering how I would get the totals for each "date" column to show in an appended last row?
In addition, the underlying table will have more data added, specifically more dates. This means that 201308 will be added next, then 201309 etc
This will mean that currently I will have to amend the code above each month to reflect the addition. Is there anyway around this?
You can dynamically create the columns using dynamic SQL, however, I would really recommend handling dynamic pivots in a layer designed for it, such as SSRS or excel.
DECLARE #SQL NVARCHAR(MAX) = '',
#SQL2 NVARCHAR(MAX) = '',
#SQL3 NVARCHAR(MAX) = '';
-- COMPILE THE UNIQUE VALUES FOR ARRIVAL THAT NEED TO BE PIVOTED
SELECT #SQL = #SQL + ',' + QUOTENAME(Arrival),
#SQL2 = #SQL2 + '+ISNULL(' + QUOTENAME(Arrival) + ', 0)',
#SQL3 = #SQL3 + ',' + QUOTENAME(Arrival) + ' = ISNULL(' + QUOTENAME(Arrival) + ', 0)'
FROM (SELECT DISTINCT Arrival FROM tblSource) s;
-- COMBINE THEM INTO A SINGLE QUERY
SET #SQL = 'SELECT [Prov]' + #SQL3 + ', [Total] = ' + STUFF(#SQL2, 1, 1, '') + '
FROM ( SELECT Arrival, Prov, Amount
FROM [tblSource]
UNION ALL
SELECT Arrival, ''Total'', SUM(Amount)
FROM [tblSource]
GROUP BY Arrival
) up
PIVOT
( SUM(Amount)
FOR Arrival IN (' + STUFF(#SQL, 1, 1, '') + ')
) pvt;';
-- EXECUTE THE QUERY
EXECUTE SP_EXECUTESQL #SQL;
This creates and executes the following SQL:
SELECT [Prov],
[2013-01-01] = ISNULL([2013-01-01], 0),
[2013-02-01] = ISNULL([2013-02-01], 0),
[Total] = ISNULL([2013-01-01], 0) + ISNULL([2013-02-01], 0)
FROM ( SELECT Arrival, Prov, Amount
FROM [tblSource]
UNION ALL
SELECT Arrival, 'Total', SUM(Amount)
FROM [tblSource]
GROUP BY Arrival
) up
PIVOT
( SUM(Amount)
FOR Arrival IN ([2013-01-01],[2013-02-01])
) pvt;
It is the query below union in the subquery up that adds the total row at the bottom, and the row total is simply created by adding all the columns in the row.
Example on SQL Fiddle
I will stress again though, I really recommend handling manipulation of data like this outside of SQL.
EDIT
An alternative to using the UNION to get the the total row is to use GROUPING SETS as follows:
DECLARE #SQL NVARCHAR(MAX) = '',
#SQL2 NVARCHAR(MAX) = '',
#SQL3 NVARCHAR(MAX) = '';
-- COMPILE THE UNIQUE VALUES FOR ARRIVAL THAT NEED TO BE PIVOTED
SELECT #SQL = #SQL + ',' + QUOTENAME(Arrival),
#SQL2 = #SQL2 + '+ISNULL(' + QUOTENAME(Arrival) + ', 0)',
#SQL3 = #SQL3 + ',' + QUOTENAME(Arrival) + ' = ISNULL(' + QUOTENAME(Arrival) + ', 0)'
FROM (SELECT DISTINCT Arrival FROM tblSource) s;
-- COMBINE THEM INTO A SINGLE QUERY
SET #SQL = 'SELECT [Prov]' + #SQL3 + ', [Total] = ' + STUFF(#SQL2, 1, 1, '') + '
FROM ( SELECT Arrival, Prov = ISNULL(Prov, 'Total'), Amount = SUM(Amount)
FROM [tblSource]
GROUP BY GROUPING SETS((Prov, arrival), (arrival))
) up
PIVOT
( SUM(Amount)
FOR Arrival IN (' + STUFF(#SQL, 1, 1, '') + ')
) pvt;';
-- EXECUTE THE QUERY
EXECUTE SP_EXECUTESQL #SQL;
SAMPLE TABLE
CREATE TABLE #TEMP([prov] VARCHAR(100),[arrival] INT, AMOUNT NUMERIC(12,2))
INSERT INTO #TEMP
SELECT 'A' [prov],'201304' [arrival],100 AMOUNT
UNION ALL
SELECT 'A' ,'201305' ,124
UNION ALL
SELECT 'A' ,'201306' ,156
UNION ALL
SELECT 'B' ,'201304' ,67
UNION ALL
SELECT 'B' ,'201305' ,211
UNION ALL
SELECT 'B' ,'201306' ,176
UNION ALL
SELECT 'C' ,'201304' ,43
UNION ALL
SELECT 'C' ,'201305' ,56
UNION ALL
SELECT 'C' ,'201306' ,158
QUERY
You can use ROLLUP to get the row total. More about ROLLUP here
-- Get the columns for dynamic pivot
DECLARE #cols NVARCHAR (MAX)
SELECT #cols = COALESCE (#cols + ',[' + CAST([arrival] AS VARCHAR(50)) + ']',
'[' + CAST([arrival] AS VARCHAR(50)) + ']')
FROM (SELECT DISTINCT [arrival] FROM #TEMP) PV
ORDER BY [arrival]
-- Replace NULL value with zero
DECLARE #NulltoZeroCols NVARCHAR (MAX)
SELECT #NullToZeroCols = SUBSTRING((SELECT ',ISNULL(['+[arrival]+'],0) AS ['+[arrival]+']'
FROM (SELECT DISTINCT CAST([arrival] AS VARCHAR(50)) [arrival] FROM #TEMP)TAB
ORDER BY CAST([arrival]AS INT) FOR XML PATH('')),2,8000)
DECLARE #query NVARCHAR(MAX)
SET #query = 'SELECT [prov],' + #NullToZeroCols + ' FROM
(
SELECT
ISNULL([prov],''Total'')[prov],
SUM(AMOUNT)AMOUNT ,
ISNULL(CAST([arrival] AS VARCHAR(50)),''Total'')[arrival]
FROM #TEMP
GROUP BY [arrival],[prov]
WITH ROLLUP
) x
PIVOT
(
MIN(AMOUNT)
FOR [arrival] IN (' + #cols + ')
) p
ORDER BY CASE WHEN ([prov]=''Total'') THEN 1 ELSE 0 END,[prov]'
EXEC SP_EXECUTESQL #query
Note : If you do not want to replace NULL with zero, just replace #NullToZeroCols with #cols in outer query of dynamic pivot