Dynamic SQL Pivot Query with Union All - sql-server

I have a static pivot query that tracks historical pricing (see below); it is union all of one table of current data, one table of historical data, and one related table.
I would like to convert it to a dynamic SQL pivot query so that I don't manually have to update the column heading dates all of the time, but I am having problems doing so. The data comes out of an accounting system (Microsoft Dynamics) running on SQL Server, and due to system limitations, I am unable to use ansi nulls/ paddings/ warnings and also unable to use quoted identifiers. Is there another way to create a dynamic sql pivot query?
Here is my current query. I want to replace the month-end dates with dynamic sql. I have view access to the data only.
SELECT ACTDESCR AS "ACCT NAME", concat(right(actnumbr_2,2), actnumbr_3) as PLU, [3/31/2015], [2/28/2015], [1/31/2015], [12/31/2015], [11/30/2014], [10/31/2014], [9/30/2014], [8/31/2014], [7/31/2014], [6/30/2014], [5/31/2014], [4/30/2014], [3/31/2014], [2/28/2014], [1/31/2014]
FROM
(SELECT A.ACTDESCR, EOMONTH(DATEFROMPARTS(B.YEAR1,B.PERIODID,1),0) AS DATE1, A.actnumbr_2, a.actnumbr_3, B.PERDBLNC, A.ACTNUMBR_1
FROM TEST.dbo.table1 AS A
LEFT OUTER JOIN TEST.dbo.TABLE2 AS b
ON (a.ACTINDX = b.ACTINDX)
UNION ALL
SELECT A.ACTDESCR, EOMONTH(DATEFROMPARTS(C.YEAR1,C.PERIODID,1),0) AS DATE1, A.actnumbr_2, a.actnumbr_3, C.PERDBLNC, A.ACTNUMBR_1
FROM TEST.dbo.TABLE1 AS A
LEFT OUTER JOIN TEST.dbo.TABLE3 AS C
ON (a.ACTINDX = C.ACTINDX)
) AS ST
PIVOT
(
SUM(PERDBLNC)
FOR DATE1 IN ([3/31/2015], [2/28/2015], [1/31/2015], [12/31/2015], [11/30/2014], [10/31/2014], [9/30/2014], [8/31/2014], [7/31/2014], [6/30/2014], [5/31/2014], [4/30/2014], [3/31/2014], [2/28/2014], [1/31/2014])
) AS PVT
WHERE ACTNUMBR_1 = ?

You can use T-SQL with dynamic SQL for this:
--VARIABLE TO HOLD DATES--
DECLARE #DATES NVARCHAR(500)
--VARIABLE TO HOLD CODE--
DECLARE #SQL NVARCHAR(MAX)
--TEMP TABLE TO FIND DISTINCT DATES--
CREATE #DATES (COLUMNVALS NVARCHAR(10))
--INSERT DISTINCT DATES INTO TEMP TABLE--
INSERT INTO #DATES
SELECT DISTINCT DATE1 FROM (
SELECT EOMONTH(DATEFROMPARTS(YEAR1,PERIODID,1),0) AS DATE1
FROM TEST.DBO.TABLE2
UNION ALL
SELECT EOMONTH(DATEFROMPARTS(YEAR1,PERIODID,1),0) AS DATE1
FROM TEST.DBO.TABLE3)
--CONCAT DATES INTO SELECT LIST--
SET #DATES = COALESCE(#DATES+', ','') + '[' + DATE1 + ']' FROM #DATES
--CREATE THE SELECT STATEMENT--
SELECT #SQL = '
;WITH ST AS (
SELECT A.ACTDESCR, EOMONTH(DATEFROMPARTS(B.YEAR1,B.PERIODID,1),0) AS DATE1, A.ACTNUMBR_2, A.ACTNUMBR_3, B.PERDBLNC, A.ACTNUMBR_1
FROM TEST.DBO.TABLE1 AS A
LEFT OUTER JOIN TEST.DBO.TABLE2 AS B
ON A.ACTINDX = B.ACTINDX
UNION ALL
SELECT A.ACTDESCR, EOMONTH(DATEFROMPARTS(C.YEAR1,C.PERIODID,1),0) AS DATE1, A.ACTNUMBR_2, A.ACTNUMBR_3, C.PERDBLNC, A.ACTNUMBR_1
FROM TEST.DBO.TABLE1 AS A
LEFT OUTER JOIN TEST.DBO.TABLE3 AS C
ON A.ACTINDX = C.ACTINDX)
SELECT ACTDESCR AS "ACCT NAME", CONCAT(RIGHT(ACTNUMBR_2,2), ACTNUMBR_3) AS PLU, '+#DATES+'
FROM ST
PIVOT
(
SUM(PERDBLNC)
FOR DATE1 IN ('+#DATES+')
) AS PVT
WHERE ACTNUMBR_1 = ?'
--PRINT IT TO SEE WHAT IT'S DONE--
PRINT #SQL
--EXECUTE IT--
EXEC (#SQL)

Related

Split string using delimiter that the SSRS passes - select multiple values from the parameter

I have this stored procedure where I am trying to select multiple values from the parameter Bkt. This works okay when I select one value from Bkt but Bkt has different values that I want to be able to select from in the SSRS.
For example in this screenshot you can see that there are CFI and DPR inside of Bkt. I want to be able to select both CFI and DPR in Bkt.
I need to keep the stored procedure so I am trying to split the delimited string that SSRS passes. I can't figure it out. I tried changing the delimiter by using an expression in the Parameters mapping section but I couldn't figure that out either. I am just so lost.
This is my Stored Procedure
#Press varchar(10),
#BKT varchar (10)
AS
BEGIN
SET NOCOUNT ON;
select * From (
Select FAC,SPEC_NEW.Tread_Code, Case When cosw.tread_code = spec_new.tread_code Then 'Scheduled' else 'Alternative' end AS Size, COUNT(Distinct loc.serial ) QTY ,
ROW_NUMBER() OVER (
PARTITION BY FAC, SPEC_NEW.Tread_Code
ORDER BY fac, Case When cosw.tread_code = spec_new.tread_code Then 'Scheduled' else 'Alternative' end DESC, SPEC_NEW.Tread_Code
) AS r_num
FROM [TireTrack].[dbo].[cos_work] cosw with (nolock)
Inner Join [SharedData].dbo.spec_master Spec with (nolock) On spec.spec=Cosw.SPEC
Inner Join [SharedData].dbo.spec_master SPEC_NEW with (nolock) On SPEC_NEW.ARTICLE=SPEC.article
Inner Join [DataWarehouse].[dbo].[Locator] LOC with (Nolock) ON LOC.SPEC=SPEC_NEW.SPEC
Where Cosw.FAC=#press and Loc.BKT = #bkt
GROUP BY FAC, cosw.Tread_Code, SPec_new.Tread_Code, Loc.Bkt
) as a
where r_num=1
order by FAC
END
I have another Stored Procedure where I am getting Press and Bkt
AS
BEGIN
SET NOCOUNT ON;
Select distinct FAC as Press, Loc.BKT as Bkt
FROM [TireTrack].[dbo].[cos_work], [DataWarehouse].[dbo].[Locator] LOC
END
I was using this link as a reference
Here is a conceptual example for you how to handle comma separated parameter to a stored procedure.
SQL
DECLARE #tbl TABLE (ID INT IDENTITY PRIMARY KEY, vehicleMake VARCHAR(20));
INSERT INTO #tbl (vehicleMake) VALUES
('Chevrolet'),
('Tesla'),
('Audi'),
('Nissan');
DECLARE #ParameterList VARCHAR(max) = '1,2';
-- Method #1
-- XML and XQuery
-- SQL Server 2008 onwards
DECLARE #separator CHAR(1) = ',';
;WITH rs AS
(
SELECT xmldata = TRY_CAST('<root><r>' +
REPLACE(#ParameterList, #separator, '</r><r>') + '</r></root>' AS XML)
)
SELECT tbl.*
FROM rs CROSS APPLY xmldata.nodes('/root/r/text()') AS t(c)
INNER JOIN #tbl AS tbl ON tbl.id = c.value('.','INT');
-- Method #2
-- STRING_SPLIT()
-- SQL Server 2016 onwards
SELECT tbl.*
FROM #tbl AS tbl INNER JOIN
STRING_SPLIT(#ParameterList, ',') AS ss
ON tbl.ID = ss.value;
Output
+----+-------------+
| ID | vehicleMake |
+----+-------------+
| 1 | Chevrolet |
| 2 | Tesla |
+----+-------------+

T-SQL Merge from multiple source records

I've seen several similar questions but haven't found one that answers my question.
I have a source table with many individual notes for a company
CompanyName, Notes3
Company1, "spoke with someone"
Company2, "Email no longer works"
Company1, "Moved address"
I have a destination table (vchCompanyName is unique here)
vchCompanyName, vchNotes
Company1, "started business in 2005"
Company2, null
I want to end up with
vchCompanyName, vchNotes
Company1, "started business in 2005
spoke with someone
Moved address"
Company2, "Email no longer works"
I have tried this code
WITH CTE
AS (SELECT ROW_NUMBER() OVER (PARTITION BY CompanyName, Notes3 ORDER BY CompanyName) RowNum, *
FROM CompanyContact
)
merge dCompany as target
using CTE as source
on target.vchcompanyname = source.companyname
when matched and len(source.notes3)>0 and source.RowNum = 1
then
update set target.vchnote = vchnote + CHAR(13) + source.Notes3
But get the error
The MERGE statement attempted to UPDATE or DELETE the same row more than once. This happens when a target row matches more than one source row. A MERGE statement cannot UPDATE/DELETE the same row of the target table multiple times. Refine the ON clause to ensure a target row matches at most one source row, or use the GROUP BY clause to group the source rows.
Which is accurate.
I have also tried STRING_AGG but get an undefined UDF error.
How do I change my code to run iteratively?
--EDIT--
I had tried the following update code
WITH CTE
AS (SELECT ROW_NUMBER() OVER (PARTITION BY CompanyName, Notes3 ORDER BY CompanyName) RowNum, *
FROM CompanyContact
)
UPDATE dCompany SET vchNote = vchNote +
(select CHAR(13) + cc.Notes3 from CompanyContact cc
inner JOIN dCompany dc ON dc.vchCompanyName COLLATE database_default = LEFT(cc.CompanyName,50) COLLATE database_default
inner join CTE on dc.vchCompanyName COLLATE database_default = LEFT(CTE.CompanyName,50) COLLATE database_default
WHERE LEN(cc.Notes3)>0
and RowNum = 1
);
But get the error
Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=, <, <= , >, >= or when the subquery is used as an expression.
#Chris Crawshaw, I will approach this by doing a 'union all' on the source and destination table to pick up all the notes for each company. Then using the STUFF function, it is easy to concatenate all the notes into one cell, while grouping by the induvidual company names. See the mockup below:
DECLARE #Source TABLE (CompanyName VARCHAR(20), Notes3 VARCHAR(50))
INSERT INTO #Source
SELECT 'Company1', 'spoke with someone' UNION ALL
SELECT 'Company2', 'Email no longer works' UNION ALL
SELECT 'Company1', 'Moved address'
DECLARE #Destination TABLE (vchCompanyName VARCHAR(20), vchNotes VARCHAR(500))
INSERT INTO #Destination
SELECT 'Company1', 'started business in 2005' UNION ALL
SELECT 'Company2', NULL
;WITH Temp AS (
SELECT *
FROM
(
SELECT *
FROM
#Destination D
WHERE D.vchNotes is not null
UNION ALL
SELECT *
FROM
#Source S
)h
)
update D
SET D.vchNotes=U.vchNotes
FROM #Destination D
LEFT JOIN(
SELECT t2.vchCompanyName, vchNotes=STUFF((
SELECT ',' + vchNotes
FROM Temp t1 where t1.vchCompanyName=t2.vchCompanyName
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '')
FROM
#Destination t2
GROUP BY
t2.vchCompanyName
)U ON
U.vchCompanyName=D.vchCompanyName
--TEST--
SELECT *
FROM
#Destination

Create a User defined function like SQL server 2017 STRING_AGG on earlier versions

I try to create a generic function that can be used like this example of using the new string_agg built-in function on SQL Server 2017
the inside implementation can be something like the follow
with tbl as(
select a.Id, c.Desc
from TableA a
join TableB b on b.aId = a.Id
join TableC c on c.Code = b.bCode
)
select distinct ID
, STUFF(( select ', ' + Desc from tbl t where t.ID = tbl.ID
for xml path(''),TYPE).value('.','VARCHAR(MAX)'),1,2,'') Desc
from tbl
But how to receives the field key, the field to be connected, the separator char, and the scoped select context?
Is it related to Inline or Multi-Statement Table-Valued Functions ?
Well, this is an ugly hack, I have to go and wash my hands now, but it works (in a way :-D)
CREATE FUNCTION dbo.MyStringAgg(#SelectForXmlAuto XML,#Delimiter NVARCHAR(10))
RETURNS NVARCHAR(MAX)
AS
BEGIN
RETURN STUFF((
SELECT #Delimiter + A.nd.value(N'(#*)[1]',N'nvarchar(max)')
FROM #SelectForXmlAuto.nodes(N'/*') AS A(nd)
FOR XML PATH(''),TYPE
).value(N'.',N'nvarchar(max)'),1,LEN(#Delimiter),'');
END
GO
DECLARE #tbl TABLE(GroupId INT,SomeValue NVARCHAR(10));
INSERT INTO #tbl VALUES(1,'A1'),(1,'A2'),(2,'B1'),(3,'C1'),(3,'C2'),(3,'C3');
SELECT GroupId
,dbo.MyStringAgg((SELECT SomeValue
FROM #tbl AS t2
WHERE t2.GroupId=t.GroupId
FOR XML AUTO), N', ')
FROM #tbl AS t
GROUP BY GroupId;
GO
DROP FUNCTION dbo.MyStringAgg;
The result
1 A1, A2
2 B1
3 C1, C2, C3
The parameter is a FOR XML sub-select within paranthesis. This will implicitly pass the sub-selects result as an XML into the function.
To be honest: I would not use this myself...
A query like this
SELECT GroupId
,STUFF((SELECT N', ' + SomeValue
FROM #tbl AS t2
WHERE t2.GroupId=t.GroupId
FOR XML PATH,TYPE).value(N'.','nvarchar(max)'),1,2,'')
FROM #tbl AS t
GROUP BY GroupId;
produces the same result and is almost the same amount of typing - but should be faster then calling a slow UDF...
Ok.. so with the first comment of #MichaƂTurczyn I run into this Microsoft article about CLR User-Defined Aggregate - Invoking Functions
Once I compile the code into SrAggFunc.dll, I was trying to register the aggregate in SQL Server as follows:
CREATE ASSEMBLY [STR_AGG] FROM 'C:\tmp\STR_AGG.dll';
GO
But I got the following error.
Msg 6501, Level 16, State 7, Line 1
CREATE ASSEMBLY failed because it could not open the physical file 'C:\tmp\SrAggFunc.dll': 3(The system cannot find the path specified.).
So I used this excellant part of #SanderRijken code and then change the command to
CREATE ASSEMBLY [STR_AGG]
FROM 0x4D5A90000300000004000000FF......000; --from GetHexString function
GO
and then,
CREATE AGGREGATE [STR_AGG] (#input nvarchar(200)) RETURNS nvarchar(max)
EXTERNAL NAME [STR_AGG].C_STRING_AGG;`
Now it's done.
You can see it under your Database -> Programmability on SSMS
and used like :
SELECT a.Id, [dbo].[STR_AGG](c.Desc) cDesc
FROM TableA a
JOIN TableB b on b.aId = a.Id
JOIN TableC c on c.Code = b.bCode
GROUP BY a.Id
Thanks all =)

How to retrieve multiple items with all their attributes from an EAV data model?

How could someone retrieve multiple products with all their attributes whose ids are passed as a comma separated list to a stored procedure?
Tables are designed in a simple EAV fashion like so:
tbl_products
id
title
tbl_product_attributes
product_FK
attribute_FK
attribute_value
tbl_attributes
id
title
The following stored procedure do this for just one product:
CREATE PROCEDURE get_product
#product_id NVARCHAR(MAX)
AS
BEGIN
-- Create a temp table to store values of get_product_attributes sp
CREATE TABLE #temp ( attributes NVARCHAR(MAX) );
-- Insert results of get_product_attributes sp into temp table
INSERT INTO #temp (attributes)
EXEC get_product_attributes #product_id;
-- Select product with all its attributes
SELECT id,
title,
( SELECT attributes FROM #temp ) AS attributes
FROM tbl_products
WHERE id = #product_id;
END;
But what if we want multiple product details given the product ids?
(get_product_attributes stored procedure return all attributes and their values of a specific product as json it uses dynamic sql to retrieve all attributes then return them as json; exactly like what dynamic sql part of this answer does: https://stackoverflow.com/a/13092910/5048383)
(also using sql server 2016, got access to STRING_SPLIT function and could easily transform passed ids to rows)
Generally speaking, looping is bad in SQL. For what you described, I can't imagine why it would warrant a loop. However, here is a set based method with simple joins that would eliminate everything but a single procedure.
declare #tbl_products table (
id int,
title varchar(16))
insert into #tbl_products
values
(1,'prod1'),
(2,'prod2'),
(3,'prod3'),
(4,'prod4'),
(5,'prod5')
declare #tbl_attributes table (
id int,
title varchar(16))
insert into #tbl_attributes
values
(1,'attr1'),
(2,'attr2'),
(3,'attr3'),
(4,'attr4'),
(5,'attr5')
declare #tbl_product_attributes table (
product_FK int,
attribute_FK int,
attribute_value varchar(64))
insert into #tbl_product_attributes
values
(1,5,'blah'),
(1,3,'blah blah'),
(2,1,'not sure'),
(2,3,'what should be here'),
(2,4,'but here is a'),
(3,5,'line of text'),
(3,4,'as a place holder')
declare #product_id nvarchar(4000)
set #product_id = '1,3'
if object_id ('tempdb..#staging') is not null
drop table #staging
select
prod.id
,prod.title as ProductTitle
,'Attr' + cast(attr.id as char(8)) as AttributeID
,attr.title as AttributeTitle
,prodAttributes.attribute_value as EAV_AttributeValue
into #staging
from
#tbl_products prod
inner join
#tbl_product_attributes prodAttributes
on prodAttributes.product_FK = prod.id
inner join
#tbl_attributes attr on
attr.id = prodAttributes.attribute_FK
inner join
string_split(#product_id,',') x
on x.value = prod.id
DECLARE #DynamicPivotQuery AS NVARCHAR(MAX)
DECLARE #ColumnName AS NVARCHAR(MAX)
--Get distinct values of the PIVOT Column
SELECT #ColumnName= ISNULL(#ColumnName + ',','')
+ QUOTENAME(AttributeID)
FROM (SELECT DISTINCT AttributeID FROM #staging) AS AttributeID
--Prepare the PIVOT query using the dynamic
SET #DynamicPivotQuery =
N'
SELECT ProductTitle, ' + #ColumnName + '
FROM #staging
PIVOT(min(AttributeTitle)
FOR AttributeID IN (' + #ColumnName + ')) AS PVTTable'
--Execute the Dynamic Pivot Query
EXEC sp_executesql #DynamicPivotQuery
So, you would just create the proc on the top part...
create proc yourProc(#product_id nvarchar(4000))
as
select
prod.id
,prod.title as ProductTitle
,attr.title as AttributeTitle
,prodAttributes.attribute_value as EAV_AttributeValue
from
#tbl_products prod
inner join
#tbl_product_attributes prodAttributes
on prodAttributes.product_FK = prod.id
inner join
#tbl_attributes attr on
attr.id = prodAttributes.attribute_FK
where
prod.id in (select * from string_split(#product_id,','))
NOTE: This can also be written more cleanly as a join
select
prod.id
,prod.title as ProductTitle
,attr.title as AttributeTitle
,prodAttributes.attribute_value as EAV_AttributeValue
from
#tbl_products prod
inner join
#tbl_product_attributes prodAttributes
on prodAttributes.product_FK = prod.id
inner join
#tbl_attributes attr on
attr.id = prodAttributes.attribute_FK
inner join
string_split(#product_id,',') x
on x.value = prod.id

Pivot total for column and row not showing correct value

honestly, for several days, i am trying to learn about pivot table behavior. rightnow, i am able to display sum of row and column in pivot table. Here is the code that i am trying to set
DECLARE #cols AS NVARCHAR(MAX)
DECLARE #colswithNoNulls AS NVARCHAR(MAX)
DECLARE #query AS NVARCHAR(MAX)
DECLARE #tanggal_awal DATE
DECLARE #tanggal_akhir DATE
DECLARE #print NVARCHAR(MAX)
DECLARE #querycount AS NVARCHAR(MAX)
CREATE TABLE #datatable
(
product_id int,
product_date date,
product_ammount int
)
SET #tanggal_awal = convert(DATE,'02-01-2017')
SET #tanggal_akhir = convert(DATE,DATEADD(dd,-1,(DATEADD(mm,1,#tanggal_awal))))
--SELECT DATEADD(s,-1,DATEADD(mm, DATEDIFF(m,0,GETDATE())+1,0))
INSERT INTO #datatable (product_id,product_date,product_ammount) VALUES
(1,GETDATE(),100),
(1,GETDATE(),900),
(2,DATEADD(DD,-1,GETDATE()),400),
(3,DATEADD(DD,4,GETDATE()),300),
(1,DATEADD(DD,4,GETDATE()),200),
(2,DATEADD(DD,2,GETDATE()),700),
(4,DATEADD(DD,-3,GETDATE()),1000),
(4,DATEADD(MM,1,GETDATE()),200),
(4,GETDATE(),750)
;WITH CTE (datelist,maxdate) AS
(
SELECT CONVERT(INT,(MIN(DATEPART(day,#tanggal_awal)))) datelist, CONVERT(INT,MAX(DATEPART(day,product_date))) maxdate
FROM #datatable
UNION ALL
SELECT CONVERT(INT,(DATEPART(day,datelist))), CONVERT(INT,(DATEPART(day,#tanggal_akhir)))
FROM cte
WHERE datelist < maxdate
) SELECT c.datelist
INTO #temp
FROM cte c
ORDER BY c.datelist
OPTION (maxrecursion 0)
SELECT #cols = STUFF((SELECT ',' + QUOTENAME(CONVERT(int, datelist))
FROM #temp
GROUP BY datelist
ORDER BY CONVERT(int, datelist)
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,''
)
SELECT #colswithNoNulls = STUFF((SELECT ',ISNULL(' + QUOTENAME(CONVERT(int, datelist)) +',''0'') '+ QUOTENAME(CONVERT(int, datelist))
FROM #temp
GROUP BY datelist
ORDER BY CONVERT(int, datelist)
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
SET #query =
'SELECT product_id, '+ #colswithNoNulls+', Total FROM
(
select
ISNULL((CAST(b.product_id as nvarchar(30))), ''Total'') product_id,
coalesce(b.product_ammount,0) as product_ammount,
DATEPART(dd,(convert(CHAR(10), product_date, 120))) PivotDate,
SUM(product_ammount) over (partition by b.product_id) as Total
FROM #datatable b
WHERE product_date between #tanggal_awal and #tanggal_akhir
GROUP BY product_ammount,product_date,product_id
WITH ROllup
) x
pivot
(
sum(product_ammount)
for PivotDate in (' +#cols+ ')
) p
ORDER BY CASE when (product_id = ''Total'') then 1 else 0 end, product_id'
EXECUTE sp_executesql #query ,N'#tanggal_awal DATE, #tanggal_akhir DATE', #tanggal_awal,#tanggal_akhir
IF(OBJECT_ID('tempdb.dbo.#temp','U') IS NOT NULL)
BEGIN
TRUNCATE TABLE #temp
TRUNCATE TABLE #datatable
DROP TABLE #temp
DROP TABLE #datatable
END
ELSE
BEGIN
SELECT '#temp is not created in this script' AS MESSAGE
END
as you can see , the result is show on the display. However, the total value at the very right bottom is strange since it is like doubled up exact total value like in this picture:
How to resolve this issue btw? since it was bit confusing for me. thank you for your help :)
Generally, I am not fully aware for RollUp functionality. From your PIVOT query. I have found some of the empty rows is coming up (basically subtotal rows from "With Rollup" option), so I have modified the Group by statement a little bit to achieve the expected result.
select
ISNULL((CAST(b.product_id as nvarchar(30))), 'Total') product_id,
coalesce(b.product_ammount,0) as product_ammount,
DATEPART(dd,(convert(CHAR(10), product_date, 120))) PivotDate,
SUM(product_ammount) over (partition by b.product_id) as Total
FROM #datatable b
WHERE product_date between #tanggal_awal and #tanggal_akhir
GROUP BY product_ammount,product_date,ROllup(product_id)
Kindly replace this query in PIVOT, then you will get the desired output.
Note: Sorry I am not fully aware of RollUp functionality, so I'm unable to give the right explanation.

Resources