Recursive cte with values from 2 tables - sql-server

I need some help with this please.
I would like to create a recursive query with values from a table for the anchor, multiplied by a coefficient from another table.
Let me be more explicit :
Tables structure and filling :
create table T
(
Site varchar(10) primary key,
Price money,
Year int
);
create table B
( Site varchar(10),
Coeff float,
Year int
);
insert into T values /* Each Site appears only once here */
('A', 125.10, 2003),
('B', 78.10, 2002),
('C', 23.34, 2001)
insert into B values /* Each (Site,Year) appears only once here */
('A', 12, 2003),
('A', 0.111, 2004),
('B', 0.322, 2002),
('B', 0.333, 2003),
('C', 0.555, 2001),
('C', 0.666, 2002)
My recursive formula is :
Price (n) = Price (n-1)* Coeff(n-1)
(where n is the year)
Here is my last attempt :
;WITH cte
AS (SELECT T.Site, T.Year, T.Price as RootPrice FROM T
UNION ALL
SELECT T.Site, T.Year, CAST(cte.RootPrice * B.Coeff AS MONEY) AS PriceYear
FROM T INNER JOIN cte ON T.Site = cte.Site AND T.Year = cte.Year INNER JOIN B ON cte.Year = B.Year AND cte.Site = B.Site)
SELECT * FROM cte
This cte is running endlessly. What am I missing ?
Edit :
Output needed :
Site | Price | Year
---------------------------
A | 1501.2 | 2003
A | 166.78 | 2004
B | 25.15 | 2002
B | 8.37 | 2003
C | 12.95 | 2001
C | 8.63 | 2002

This produces the output you want:
;WITH CTE AS
(
SELECT
Site = T.Site,
Year = T.Year,
Price = CONVERT(MONEY, T.Price * B.Coeff)
FROM
T AS T
INNER JOIN B AS B ON
T.Site = B.Site AND
T.Year = B.Year
UNION ALL
SELECT
Site = C.Site,
Year = C.Year + 1,
Price = CONVERT(MONEY, C.Price * B.Coeff)
FROM
CTE AS C
INNER JOIN B AS B ON
C.Site = B.Site AND
C.Year + 1 = B.Year
)
SELECT
*
FROM
CTE AS C
ORDER BY
C.Site,
C.Year
The problem with your solution is that the anchor doesn't start with the correct price, you should multiply the price with coeff on the anchor. Keep in mind that the anchor is the first set of the resulting CTE and it is included in it.
So your anchor:
SELECT
T.Site,
T.Year,
T.Price as RootPrice
FROM
T
Should start with the correct price for that year:
SELECT
Site = T.Site,
Year = T.Year,
Price = CONVERT(MONEY, T.Price * B.Coeff)
FROM
T AS T
INNER JOIN B AS B ON
T.Site = B.Site AND
T.Year = B.Year
And remove the T reference on the recursive set, since you don't need it anymore.
If you also want to see the root prices, you can tamper a little with the recursive join expression:
;WITH CTE AS
(
SELECT
Site = T.Site,
Year = T.Year,
Price = T.Price,
IsRoot = 1
FROM
#T AS T
UNION ALL
SELECT
Site = C.Site,
Year = CASE WHEN C.IsRoot = 1 THEN C.Year ELSE C.Year + 1 END,
Price = CONVERT(MONEY, C.Price * B.Coeff),
IsRoot = 0
FROM
CTE AS C
INNER JOIN #B AS B ON C.Site = B.Site
WHERE
(C.IsRoot = 1 AND C.Year = B.Year) OR
(C.IsRoot = 0 AND C.Year + 1 = B.Year)
)
SELECT
*
FROM
CTE AS C
ORDER BY
C.Site,
C.Year,
C.IsRoot DESC
/*
Results:
Site Year Price IsRoot
---------- ----------- --------------------- -----------
A 2003 125,10 1
A 2003 1501,20 0
A 2004 166,6332 0
B 2002 78,10 1
B 2002 25,1482 0
B 2003 8,3744 0
C 2001 23,34 1
C 2001 12,9537 0
C 2002 8,6272 0
*/

Related

Referencing the current row outer apply column within separate outer join

Recently I've been tasked with creating a report that outputs sales information by Date of Business and Hour of the Day.
Here is the query I have currently written.
WITH CTE AS
(
SELECT 0 AS Count
UNION ALL
SELECT Count + 1
FROM CTE
WHERE Count + 1 <= 23
),
ALLDATES AS
(
SELECT CONVERT(datetime, #startDate) AS [DOB]
UNION ALL
SELECT DATEADD(DAY, 1, [DOB])
FROM AllDates
WHERE [DOB] < #endDate
)
SELECT D.DOB, A.Count AS [Hour], CONCAT(A.Count, ':00') AS [DisplayHour]
, B.OrderModeName, COALESCE(B.Sales_Total, 0) AS [Sales]
, COALESCE(B.Comps, 0) AS Comps, COALESCE(B.Promos, 0) AS Promos
FROM CTE AS A
OUTER APPLY (SELECT DOB FROM ALLDATES) D
LEFT OUTER JOIN (
SELECT DATEPART(HH, ItemDetail.TransactionTime) AS [Hour]
, OrderMode.OrderModeName, SUM(ItemDetail.GrossPrice) Sales_Total
, SUM(CompAmount) AS Comps, SUM(PromoAmount) AS Promos
FROM ItemDetail
INNER JOIN OrderMode ON OrderMode.OrderModeID = ItemDetail.OrderModeID
WHERE ItemDetail.DOB = D.DOB /*NEED HELP HERE*/ AND LocationID IN (
SELECT LocationID
FROM LocationGroupMember
WHERE LocationGroupID = '#locationGroupID'
)
GROUP BY ItemDetail.DOB, DATEPART(HH, ItemDetail.TransactionTime), OrderMode.OrderModeName
) AS B
ON A.Count = B.Hour
ORDER BY D.DOB, A.Count
Where I am struggling is being able to reference the current row's DOB column that is coming from the OUTER APPLY.
I have tried WHERE ItemDetail.DOB = D.DOB, however I receive an error that the identifier can't be bound. Am I correct that in understanding that the outer applied data is not visible to the subquery within the join?
Here is an example of the output I'm expecting:
DOB | Hour | Display Hour | OrderModeName | Sales | Comps | Promos
1/8/2020 | 17 | 17:00 | Order | 163.17 | 0 | 0 <-- Sales for Hour and Order Mode present
1/8/2020 | 23 | 23:00 | | 0 | 0 | 0 <-- No sales at all for a given hour
Thanks in advance for any direction and advice!
The basic pattern here is to CROSS JOIN to define the result "grain" and then LEFT JOIN the fact table to populate the rows for which data exists. EG
WITH CTE AS
(
SELECT 0 AS Count
UNION ALL
SELECT Count + 1
FROM CTE
WHERE Count + 1 <= 23
),
ALLDATES AS
(
SELECT CONVERT(datetime, #startDate) AS [DOB]
UNION ALL
SELECT DATEADD(DAY, 1, [DOB])
FROM AllDates
WHERE [DOB] < #endDate
),
ALLHOURS as
(
SELECT D.DOB, A.Count AS [Hour], CONCAT(A.Count, ':00') AS [DisplayHour]
FROM CTE AS A
CROSS JOIN ALLDATES D
),
ITEM_SUMMARY as
(
SELECT DOB, DATEPART(HH, ItemDetail.TransactionTime) AS [Hour], OrderMode.OrderModeName, SUM(ItemDetail.GrossPrice) Sales_Total, SUM(CompAmount) AS Comps, SUM(PromoAmount) AS Promos
FROM ItemDetail
INNER JOIN OrderMode ON OrderMode.OrderModeID = ItemDetail.OrderModeID
AND LocationID IN (SELECT LocationID FROM LocationGroupMember WHERE LocationGroupID = #locationGroupID)
where DOB >= #startDate
and DOB < #endDate
GROUP BY ItemDetail.DOB, DATEPART(HH, ItemDetail.TransactionTime), OrderMode.OrderModeName
)
select ALLHOURS.DOB,
ALLHOURS.Count AS [Hour],
CONCAT(ALLHOURS.Count, ':00') AS [DisplayHour],
ITEM_SUMMARY.OrderModeName,
COALESCE(ITEM_SUMMARY.Sales_Total, 0) AS [Sales],
COALESCE(ITEM_SUMMARY.Comps, 0) AS Comps,
COALESCE(ITEM_SUMMARY.Promos, 0) AS Promos
from ALLHOURS
LEFT OUTER JOIN ITEM_SUMMARY
on ITEM_SUMMARY.DOB = ALLHOURS.DOB
and ITEM_SUMMARY.Hour = ALLHOURS.Hour

Dynamic update for different colums using between two date like calendar date

CREATE PROCEDURE [dbo].[CalendarMonthly]
(
#FROMDATE VARCHAR(25)
)
AS
BEGIN
SET NOCOUNT ON
DECLARE #ADate DATETIME
DECLARE #MonthCount INT
SET #ADate = #FROMDATE
SET #MonthCount = (SELECT DAY(EOMONTH(#ADate)))
DECLARE #tmpTable TABLE
(
TRoomID INT, DAY1 INT, DAY2 INT, DAY3 INT, DAY4 INT, DAY5 INT, DAY6 INT, DAY7 INT
)
INSERT INTO #tmpTable
SELECT RM.ROOMID,
0,0,0,0,0,0,0
FROM RoomMaster AS RM
LEFT JOIN RoomTypes AS RT ON RM.RoomTypeID = RT.RoomTypeID
WHERE RM.RoomMasterStatus <> 99
DECLARE #RoomID INT
DECLARE #ForDate DATE
DECLARE #dtFromDate DATE
DECLARE #dtToDate DATE
SET #dtFromDate = CONVERT(DATE,#FromDate)
SET #ForDate = #dtFromDate
SET #dtToDate = CONVERT(DATE,CONVERT(VARCHAR,YEAR(#dtFromDate)) +'-'+ CONVERT(VARCHAR,MONTH(DATEADD(M,1,#dtFromDate)))+'-1')
SET #dtToDate = DATEADD(D,-1,#dtToDate)
DECLARE #DayCount INT
WHILE #ForDate <= #dtToDate
BEGIN
SET #DayCount = DAY(#ForDate)
IF #DayCount = 1
BEGIN
-- Checkin
UPDATE #tmpTable SET DAY1 = 1
FROM #tmpTable TT
JOIN RoomCheckinDetails AS RCD ON RCD.RoomID = TT.TRoomID
JOIN RoomCheckinMaster AS RCM ON RCM.CheckinID = RCD.CheckinID
WHERE CONVERT(DATE,RCD.CheckinDate) = #ForDate
AND RCD.RoomID = TT.TRoomID
-- Expected Checkin
UPDATE #tmpTable SET DAY1 = 8
FROM #tmpTable TT
JOIN RoomBookingDetails AS RBD ON RBD.RoomID = TT.TRoomID
JOIN RoomBookingMaster AS RBM ON RBM.ReservationID = RBD.ReservationID
WHERE CONVERT(DATE,RBD.ExpectedCheckinDate) = #ForDate
AND RBD.RoomID = TT.TRoomID
END
ELSE IF #DayCount = 2
.
.
.
.
.
.
---upto Day count 7
SET #ForDate = DATEADD(Day,1,#ForDate)
END
SELECT * FROM #tmpTable
END
My question is:
ex: checkindate = 04/06/2017, checoutdate = 9/06/2017 in compare two date I have update to columns day1 to columns day6 values is 0.
Instead of creating a temporary table and using a cursor to update each set of columns, this is a set based solution that avoids all of that.
For the first part, if you just need 7 days then you can use a simple values tally table with a common table expression and the Table Value Constructor (Transact-SQL):
declare #fromdate date = '20170605';
;with dates as (
select
[Date]=convert(date,dateadd(day,rn-1,#fromdate))
, rn
from (values (1),(2),(3),(4),(5),(6),(7)) t(rn)
)
Otherwise, you can generate an adhoc table of dates using stacked ctes in a common table expression like this:
declare #fromdate date = '20170605';
declare #thrudate date = dateadd(day,6,#fromdate)
;with n as (select n from (values(0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) t(n))
, dates as (
select top (datediff(day, #fromdate, #thrudate)+1)
[Date]=convert(date,dateadd(day,row_number() over(order by (select 1))-1,#fromdate))
, rn = row_number() over(order by (select 1))
from n as deka cross join n as hecto cross join n as kilo
cross join n as tenK cross join n as hundredK
order by [Date]
)
Then cross join the dates with roommaster, and left join both the checkin and booking tables to see if a room has a reservation or is currently occupied:
, cte as (
select
rm.roomid
, d.rn
, Value = case
when rcd.roomid is not null then 1
when rbd.roomid is not null then 8
else 0
end
from dates d
cross join roommaster rm
left join roomcheckindetails rcd
on rm.roomid = rcd.roomid
and d.date >= rcd.checkindate
and d.date <= rcd.checkoutdate
left join roombookingdetails rbd
on rm.roomid = rbd.roomid
and d.date >= rbd.expectedcheckindate
and d.date <= rbd.expectedcheckoutdate
where rm.roommasterstatus <> 99
)
Then for the last piece, you can use conditional aggregation or pivot() (pick one) like so:
select
roomid
, Day1 = min(case when rn = 1 then value end)
, Day2 = min(case when rn = 2 then value end)
, Day3 = min(case when rn = 3 then value end)
, Day4 = min(case when rn = 4 then value end)
, Day5 = min(case when rn = 5 then value end)
, Day6 = min(case when rn = 6 then value end)
, Day7 = min(case when rn = 7 then value end)
from cte
group by roomid
select
roomid
, Day1 = [1]
, Day2 = [2]
, Day3 = [3]
, Day4 = [4]
, Day5 = [5]
, Day6 = [6]
, Day7 = [7]
from cte
pivot (min(value) for rn in ([1],[2],[3],[4],[5],[6],[7]))p
rextester demo with conditional aggregation: http://rextester.com/RUJ98491
rextester demo with pivot(): http://rextester.com/YNKU89188
both return the same results for my demo data:
+--------+------+------+------+------+------+------+------+
| roomid | Day1 | Day2 | Day3 | Day4 | Day5 | Day6 | Day7 |
+--------+------+------+------+------+------+------+------+
| 2 | 1 | 1 | 1 | 1 | 0 | 0 | 0 |
| 3 | 0 | 8 | 8 | 8 | 8 | 0 | 0 |
+--------+------+------+------+------+------+------+------+
Number and Calendar table reference:
Generate a set or sequence without loops - 2 - Aaron Bertrand
The "Numbers" or "Tally" Table: What it is and how it replaces a loop - Jeff Moden
Creating a Date Table/Dimension in sql Server 2008 - David Stein
Calendar Tables - Why You Need One - David Stein
Creating a date dimension or calendar table in sql Server - Aaron Bertrand

SQL alternative to inner join

I have a table which has data that appears as:
Staion Date Temperature
A 2015-07-31 8
B 2015-07-31 6
C 2015-07-31 8
A 2003-02-21 4
B 2003-02-21 7
C 2003-02-21 7
For each date I need to create arrays so that it has the following combination:
c1 = (A + B)/2, c2 = (A + B + C)/3 and c3 = (B + C)/2
Right I am doing three different inner join on the table itself and doing a final inner join to achieve the following as result:
Date c1 c2 c3
2015-07-31 7 7.33 7
2003-02-21 5.5 6 7
Is there a cleaner way to do this?
No need for a JOIN, you could simply use a GROUP BY and an aggregation function:
WITH CTE AS
(
SELECT [Date],
MIN(CASE WHEN Staion = 'A' THEN Temperature END) A,
MIN(CASE WHEN Staion = 'B' THEN Temperature END) B,
MIN(CASE WHEN Staion = 'C' THEN Temperature END) C
FROM dbo.YourTable
GROUP BY [date]
)
SELECT [Date],
(A+B)/2 c1,
(A+B+C)/3 c2,
(B+C)/2 c3
FROM CTE;
SUM Function is very useful in such cases:
SELECT
c1 = SUM(CASE WHEN Staion IN ('A', 'B') THEN Temperature ELSE 0 END) / 2,
c2 = SUM(Temperature) / 3,
c3 = SUM(CASE WHEN Staion IN ('B', 'C') THEN Temperature ELSE 0 END) / 2,
[Date]
FROM dbo.Table
GROUP BY [Date]
You can do it with just two joins and almost literally the formulas you've provided:
declare #t table (Station char(1) not null,[Date] date not null, Temperature int not null)
insert into #t(Station,[Date],Temperature) values
('A','20150731',8),
('B','20150731',6),
('C','20150731',8),
('A','20030221',4),
('B','20030221',7),
('C','20030221',7)
select
B.[Date],
c1 = (A.Temperature + B.Temperature)/2.0,
c2 = (A.Temperature + B.Temperature + C.Temperature)/3.0,
c3 = (B.Temperature + C.Temperature)/2.0
from
#t B
inner join
#t A
on
B.[Date] = A.[Date]
inner join
#t C
on
B.[Date] = C.[Date]
where
A.Station = 'A' and
B.Station = 'B' and
C.Station = 'C'
Result:
Date c1 c2 c3
---------- --------------- ----------- ----------
2015-07-31 7.000000 7.333333 7.000000
2003-02-21 5.500000 6.000000 7.000000
You can use pivot and calculation on pivoted data as below:
select [Date], c1 = (A+B)/2.0, c2 = (A+B+C)/3.0, C3 = (B+C)/2.0 from
( select * from #yourstation ) s
pivot (max(temparature) for station in ([A], [B], [C])) p
Your input table:
create table #yourStation (station char(1), date date, Temparature int)
insert into #yourStation (station, date, Temparature) values
('A','2015-07-31', 8 )
,('B','2015-07-31', 6 )
,('C','2015-07-31', 8 )
,('A','2003-02-21', 4 )
,('B','2003-02-21', 7 )
,('C','2003-02-21', 7 )

SQL Server - Running Total with Carry Forward

Needs some help on the following:
Table #Data contains the Opening and Closing Stock for a product over 5 days
Table #BackData contains some post dated transactions
How can i Update the table #Data with a Running Total including a carry forward
CREATE TABLE #Data (
Prod VARCHAR(20)
,SDate DATE
,OStock INT
,CStock INT
)
CREATE TABLE #BackData (
Prod VARCHAR(20)
,SDate DATE
,CStock INT
)
INSERT INTO #Data
SELECT 'p1', '2016-06-06', 10, 10
UNION ALL
SELECT 'p1', '2016-06-07', 10, 14
UNION ALL
SELECT 'p1', '2016-06-08', 14, 13
UNION ALL
SELECT 'p1', '2016-06-09', 13, 13
UNION ALL
SELECT 'p1', '2016-06-10', 13, 11
INSERT INTO #BackData
SELECT 'p1', '2016-06-06', 2
UNION ALL
SELECT 'p1', '2016-06-07', 4
UNION ALL
SELECT 'p1', '2016-06-09', -1
UNION ALL
SELECT 'p1', '2016-06-10', -2
DROP TABLE #Data
DROP TABLE #BackData
Desired Output :
Prod| SDate |OStock |CStock|
p1 |2016-06-06 |10 |12 |
p1 |2016-06-07 |12 |16 |
p1 |2016-06-08 |16 |16 |
p1 |2016-06-09 |16 |15 |
p1 |2016-06-10 |15 |13 |
EDIT
This is what i had managed to write before i got the answer, using two updates because the actual table had too many columns to use in a single query.
UPDATE D
SET D.CStock = FL.NewCStock
FROM #Data D
INNER JOIN (
SELECT DT.Prod
,DT.SDate
,SUM(IIF(RwNm = 1, DT.CStock, 0) + ISNULL(BD.CStock, 0)) OVER (
PARTITION BY DT.Prod ORDER BY DT.SDate ROWS UNBOUNDED PRECEDING
) NewCStock
FROM (
SELECT Prod
,SDate
,CStock
,ROW_NUMBER() OVER (
PARTITION BY Prod ORDER BY SDate
) AS RwNm
FROM #Data
) DT
LEFT JOIN #BackData BD ON DT.Prod = DT.Prod
AND BD.SDate = DT.SDate
) FL ON D.Prod = FL.Prod
AND D.SDate = FL.SDate
UPDATE D
SET D.OStock = PV.NewOStock
FROM #Data D
INNER JOIN (
SELECT Prod
,SDate
,ISNULL(LAG(CStock) OVER (
PARTITION BY Prod ORDER BY SDate
), CStock) AS NewOStock
FROM #Data
) PV ON D.Prod = PV.Prod
AND D.SDate = PV.SDate
You can use the following query to UPDATE:
;WITH ToUpdate AS (
SELECT d1.OStock, d1.CStock,
COALESCE(LAG(d2.CStock2) OVER (PARTITION BY d2.Prod
ORDER BY d2.SDate),
d1.OStock) AS OStock2,
d2.CStock2
FROM #Data AS d1
JOIN (
SELECT d.Prod, d.SDate, d.OStock, d.CStock,
COALESCE(t.newCStock,
LAG(t.newCStock) OVER (PARTITION BY d.Prod
ORDER BY d.SDate)) AS CStock2
FROM #Data AS d
LEFT JOIN (
SELECT bd.Prod, bd.SDate,
drn.CStock + SUM(bd.CStock) OVER (PARTITION BY bd.Prod
ORDER BY bd.SDate) AS newCStock
FROM #BackData AS bd
INNER JOIN (
SELECT Prod, CStock,
ROW_NUMBER() OVER (PARTITION BY Prod ORDER BY SDate) AS rn
FROM #Data
) AS drn ON bd.Prod = drn.Prod AND drn.rn = 1
) AS t ON t.Prod = d.Prod AND t.SDate = d.SDate
) AS d2 ON d1.Prod = d2.Prod AND d1.SDate= d2.SDate
)
UPDATE ToUpdate
SET OStock = OStock2,
CStock = CStock2
This looks awfully convoluted, but I couldn't think of anything simpler.
Demo here
You can rebuild values in #Data table with the help of recursive CTE:
;WITH cte AS (
SELECT top 1 d.Prod,
d.SDate,
d.OStock,
d.OStock + b.CStock as CStock
FROM #Data d
LEFT JOIN #BackData b
ON b.Prod = d.Prod and b.SDate = d.SDate
ORDER BY d.SDate ASC
UNION ALL
SELECT c.Prod,
DATEADD(day,1,c.SDate),
c.CStock,
c.CStock + ISNULL(b.CStock,0)
FROM cte c
INNER JOIN #Data d
ON d.Prod = c.Prod AND d.SDate = DATEADD(day,1,c.SDate)
OUTER APPLY (SELECT CStock FROM #BackData b WHERE b.Prod = d.Prod and b.SDate = d.SDate) as b
)
SELECT *
FROM cte
Output:
Prod SDate OStock CStock
-------------------- ---------- ----------- -----------
p1 2016-06-06 10 12
p1 2016-06-07 12 16
p1 2016-06-08 16 16
p1 2016-06-09 16 15
p1 2016-06-10 15 13
To update #Data:
UPDATE d
SET OStock = c.OStock, CStock = c.CStock
FROM #Data d
INNER JOIN cte c
ON c.Prod = d.Prod AND c.SDate = d.SDate
Shouldn't the result be like :
Prod SDate OStock CStock
p1 2016-06-06 10 12
p1 2016-06-07 12 20 (#Data CStock 14 + #BakData 2 + 4)
p1 2016-06-08 20 19 (#Data CStock 13 + #BakData 2 + 4)
p1 2016-06-09 19 18 (#Data CStock 13 + #BakData 2 + 4 - 1)
p1 2016-06-10 18 14 (#Data CStock 11 + #BakData 2 + 4 - 1 -2)
This query will produce the above result
update d
set OStock = d.OStock + a.OAdj,
CStock = d.CStock + a.CAdj
from #Data d
cross apply
(
select OAdj = sum(case when Oflag = 1 then x.CStock else 0 end),
CAdj = sum(x.CStock)
from
(
select *, Oflag = case when x.SDate = d.SDate then 0 else 1 end
from #BackData x
where x.Prod = d.Prod
and x.SDate <= d.SDate
) x
) a
Based on your expected output, it seems that you are re-calculating the daily Opening / Closing balance based on the figure from 2016-06-06
Here is a solution that will gives you your expected output.
; with
cte as
(
select Prod, SDate, OStock, CStock,
rn = row_number() over (partition by Prod order by SDate)
from #Data
),
adj as
(
select Prod, SDate, CStock
from cte
where rn = 1
union all
select Prod, SDate, CStock
from #BackData
)
update d
set OStock = coalesce(o.OStock, d.OStock),
CStock = c.CStock
from #Data d
cross apply
(
select OStock = sum(x.CStock)
from adj x
where x.Prod = d.Prod
and x.SDate < d.SDate
) o
cross apply
(
select CStock = sum(x.CStock)
from adj x
where x.Prod = d.Prod
and x.SDate <= d.SDate
) c
Result :
p1 2016-06-06 10 12
p1 2016-06-07 12 16
p1 2016-06-08 16 16
p1 2016-06-09 16 15
p1 2016-06-10 15 13

GROUP BY in subquery T-SQL

I am trying to make a table like this:
ProductName | SalesByDate | TotalSalesUntilDate
A | 5 | 15
B | 10 | 30
C | 20 | 25
D | 18 | 43
SalesByDate means the number of product sold for each product on the input date and TotalSalesUntilDate indicates the number of product sold for each product from the first date of the month until the input date (example of input date: 17 March 2010)
I wrote this query using subquery:
select p.ProductName, A.SalesByDate,
(select(SUM(case when (pd.Date between '01' and #Date)
then s.SalesByDate else 0 end))
from Period pd
inner join Sales s on pd.TimeID = s.TimeID
full join Product p on s.ProductID = p.ProductID) as TotalSalesUntilDate
from Product p join
(select s.ProductID, pd.Date, s.SalesByDate
from Period pd join Sales s on pd.TimeID = s.TimeID) A on
p.ProductID = A.ProductID where #Date = A.Date
but I got the result:
ProductName | SalesByDate | TotalSalesUntilDate
A | 5 | 113
B | 10 | 113
C | 20 | 113
D | 18 | 113
which the TotalSalesUntilDate shows the number of product sold from the first date of the month until the input date but for all product without separation for each product.
So when I tried to change the query to like this (adding GROUP BY p.ProductID before "as TotalSalesUntilDate"):
select p.ProductName, A.SalesByDate,
(select(SUM(case when (pd.Date between '01' and #Date)
then s.SalesByDate else 0 end))
from Period pd
inner join Sales s on pd.TimeID = s.TimeID
full join Product p on s.ProductID = p.ProductID
group by p.ProductID) as TotalSalesUntilDate
from Product p join
(select s.ProductID, pd.Date, s.SalesByDate
from Period pd join Sales s on pd.TimeID = s.TimeID) A on
p.ProductID = A.ProductID where #Date = A.Date
and when I execute this query, I got this error message:
"Msg 512, Level 16, State 1, Procedure SalesMTDSubQuery, Line 7
Subquery returned more than 1 value. This is not permitted when the
subquery follows =, !=, <, <= , >, >= or when the subquery is used as
an expression."
Since I'm new in SQL and still learning, but I don't understand how to solve this. Any help will be appreciated. Thank you.
In the #Date variable we are storing the date:
SELECT DISTINCT PT.[ProductName]
,SUM(IIF(PD.[Date] = #Date, SL.[SalesByDate], 0))
,SUM(IIF(PD.[Date] BETWEEN '01' AND #Date, SL.[SalesByDate], 0))
FROM #Product PT
INNER JOIN #Sales SL
ON PT.[ProductID] = SL.[ProductID]
INNER JOIN #Period PD
ON SL.[TimeID] = PD.[TimeID]
GROUP BY PT.[ProductName]
Result:
Full code:
DECLARE #Period TABLE
(
[TimeID] TINYINT
,[Date] CHAR(2)
)
INSERT INTO #Period([TimeID], [Date])
VALUES (1,'01')
,(2,'02')
,(3,'03')
,(4,'04')
,(5,'05')
,(6,'06')
,(7,'07')
,(8,'08')
,(9,'09')
,(10,'10')
,(11,'11')
,(12,'12')
,(13,'13')
,(14,'14')
,(15,'15')
DECLARE #Product TABLE
(
[ProductID] TINYINT
,[ProductName] CHAR(1)
)
INSERT INTO #Product( [ProductID], [ProductName])
VALUES (1,'A')
,(2,'B')
,(3,'C')
,(4,'D')
DECLARE #Sales TABLE
(
[TimeID] TINYINT
,[ProductID] TINYINT
,[SalesByDate] TINYINT
)
INSERT INTO #Sales ([TimeID], [ProductID], [SalesByDate])
VALUES (1, 1, 10)
,(1, 4, 20)
,(7, 2, 10)
,(7, 3, 5)
,(15, 1, 5)
,(15, 2, 10)
,(15, 3, 15)
,(15, 4, 18)
,(19, 2, 15)
,(20, 3, 2)
,(22, NULL, 2)
,(1, 4, 5)
,(7, 2, 10)
,(15, 3, 5)
DECLARE #Date CHAR(2) = '15'
SELECT DISTINCT PT.[ProductName]
,SUM(IIF(PD.[Date] = #Date, SL.[SalesByDate], 0))
,SUM(IIF(PD.[Date] BETWEEN '01' AND #Date, SL.[SalesByDate], 0))
FROM #Product PT
INNER JOIN #Sales SL
ON PT.[ProductID] = SL.[ProductID]
INNER JOIN #Period PD
ON SL.[TimeID] = PD.[TimeID]
GROUP BY PT.[ProductName]
EDIT:
If you need to use sub-query, this is how your example can works:
SELECT PT.[ProductName]
,SUM(SL.[SalesByDate])
,DataSource.[TotalSalesByDate]
FROM #Product PT
INNER JOIN #Sales SL
ON PT.[ProductID] = SL.[ProductID]
INNER JOIN #Period PD
ON SL.[TimeID] = PD.[TimeID]
INNER JOIN
(
SELECT S.[ProductID]
,SUM(S.[SalesByDate]) AS [TotalSalesByDate]
FROM #Sales S
INNER JOIN #Period P
ON S.[TimeID] = P.[TimeID]
WHERE P.[Date] BETWEEN '01' AND #Date
GROUP BY S.[ProductID]
) AS DataSource
ON PT.[ProductID] = DataSource.[ProductID]
WHERE PD.[Date] = #Date
GROUP BY PT.[ProductName]
,DataSource.[TotalSalesByDate]
First, in the Table Period you must have dates, not '01','02' so you can use BETWEEN. Or you can use 1,2,3 ... but they have to be numbers.
So, we suppose that in table Table Period you have numbers for dates (I make this remark, because you use 01, instead of 1 which assumes string value. The query itself is relatively easy:
SELECT
p.ProductName,
SUM(CASE WHEN s.TimeID = 10 THEN s.SalesByDate ELSE 0 END) as SalesByDate,
SUM(CASE WHEN s.TimeID = 10 THEN 0 ELSE s.SalesByDate END) as TotalSalesUntilDate
FROM
Product p
INNER JOIN Salse s ON p.ProductID = s.ProductID
WHERE
s.TimeID BETWEEN 1 AND 10
GROUP BY p.ProductName;
You take Sales for each date. If this is a selected date then add sales to column SalesByDate, else add then to column TotalSalesUntilDate. You group by ProductName to calculate SUM. And select only dates which are in the desired period in WHERE clause. We assume that this query is started only for a specific month (because we use only date element - i.e. 1,2,... not month).
This will show only Products with sales. If you want to see all Products list use LEFT JOIN instead of INNER JOIN.

Resources