T-SQL NULL-Friendly Query - sql

I have a table "tblSalesOrder" in Microsoft T-SQL with some sample records:
SalesOrderID OrderDate ItemID Quantity PromotionCode
====================================================================
1 2014-09-01 100 5 NULL
2 2014-09-01 120 10 ABC
3 2014-09-05 150 7 NULL
4 2014-09-08 200 15 NULL
I need to return NULL-friendly resultset for records which do not exist.
As an example, I want a monthly query for September 2014:
SELECT SalesOrderID, OrderDate, ItemID, Quantity, PromotionCode
FROM tblSalesOrder
WHERE OrderDate = BETWEEN '2014-09-01' AND '2014-09-30'
I need it to return at least 1 row for each day (i.e. 0 valued row, if the entry for that day is not available)
SalesOrderID OrderDate ItemID Quantity PromotionCode
====================================================================
1 2014-09-01 100 5 NULL
2 2014-09-01 120 10 ABC
0 2014-09-02 0 0 0
0 2014-09-03 0 0 0
0 2014-09-04 0 0 0
3 2014-09-05 150 7 NULL
0 2014-09-06 0 0 0
0 2014-09-07 0 0 0
4 2014-09-08 200 15 NULL
0 2014-09-09 0 0 0
...
...
...
0 2014-09-30 0 0 0

master..spt_values is a table in all microsoft sql databases containing 2506 rows, by cross joining, it will have 2506*2506 rows to calculate dates between from and to. Other tables can be used as well, this is just a table used to create the dates. A calendar table would be even easier to use.
The EXCEPT will remove all dates already in use. Then by combining the rows from tblSalesOrder and CTE with union all, empty days will be filled with the required hardcoded values:
DECLARE #from date = '2014-09-01'
DECLARE #to date = '2014-09-30'
;WITH CTE as
(
SELECT top (case when #to < #from then 0 else datediff(day, #from, #to) + 1 end)
dateadd(day, row_number() over (order by (select 1)) - 1, #from) OrderDate
FROM
master..spt_values t1
CROSS JOIN
master..spt_values t2
EXCEPT
SELECT
OrderDate
FROM
tblSalesOrder
)
SELECT
0 SalesOrderID, OrderDate, 0 ItemID, 0 Quantity, '0' PromotionCode
FROM
CTE
UNION ALL
SELECT
SalesOrderID, OrderDate, ItemID, Quantity, PromotionCode
FROM
tblSalesOrder
ORDER BY
OrderDate, SalesOrderId

You can join the a date parameter in an empty select and coalesce the values:
select coalesce(t.SalesOrderID, 0) SalesOrderID
, coalesce(t.OrderDate, d.OrderDate) OrderDate
, coalesce(t.ItemID, 0) ItemID
, coalesce(t.Quantity, 0) Quantity
, coalesce(t.PromotionCode, 0) PromotionCode
from (select #dateParameter OrderDate) d
left
outer
join ( SELECT SalesOrderID, OrderDate, ItemID, Quantity, PromotionCode
FROM tblSalesOrder
) t
on t.OrderDate = d.OrderDate

DECLARE #startDate date= '20140901'
,#endDate date = '20140930';
WITH Calendar as (
SELECT #startDate as OrderDate
UNION ALL
SELECT DATEADD(DAY, 1, OrderDate) as OrderDate
FROM Calendar
WHERE OrderDate < #endDate
)
SELECT coalesce(t.SalesOrderID, 0) SalesOrderID
, coalesce(t.OrderDate, Calendar.OrderDate) OrderDate
, coalesce(t.ItemID, 0) ItemID
, coalesce(t.Quantity, 0) Quantity
, CASE WHEN t.OrderDate IS NULL THEN '0' ELSE t.PromotionCode END as PromotionCode FROM Calendar
LEFT JOIN tblSalesOrder t ON Calendar.OrderDate = t.OrderDate
ORDER BY Calendar.OrderDate, t.SalesOrderID
OPTION (MAXRECURSION 0);

Related

Fill up date gap by month

I have table of products and their sales quantity in months.
Product Month Qty
A 2018-01-01 5
A 2018-02-01 3
A 2018-05-01 5
B 2018-08-01 10
B 2018-10-01 12
...
I'd like to first fill in the data gap between each product's min and max dates like below:
Product Month Qty
A 2018-01-01 5
A 2018-02-01 3
A 2018-03-01 0
A 2018-04-01 0
A 2018-05-01 5
B 2018-08-01 10
B 2018-09-01 0
B 2018-10-01 12
...
Then I would need to perform an accumulation of each product's sales quantity by month.
Product Month total_Qty
A 2018-01-01 5
A 2018-02-01 8
A 2018-03-01 8
A 2018-04-01 8
A 2018-05-01 13
B 2018-08-01 10
B 2018-09-01 10
B 2018-10-01 22
...
I fumbled over the "cross join" clause, however it seems to generate some unexpected results for me. Could someone help to give a hint how I can achieve this in SQL?
Thanks a lot in advance.
I think a recursive CTE is a simple way to do this. The code is just:
with cte as (
select product, min(mon) as mon, max(mon) as end_mon
from t
group by product
union all
select product, dateadd(month, 1, mon), end_mon
from cte
where mon < end_mon
)
select cte.product, cte.mon, coalesce(qty, 0) as qty
from cte left join
t
on t.product = cte.product and t.mon = cte.mon;
Here is a db<>fiddle.
Hi i think this example can help you and perform what you excepted :
CREATE TABLE #MyTable
(Product varchar(10),
ProductMonth DATETIME,
Qty int
);
GO
CREATE TABLE #MyTableTempDate
(
FullMonth DATETIME
);
GO
INSERT INTO #MyTable
SELECT 'A', '2019-01-01', 214
UNION
SELECT 'A', '2019-02-01', 4
UNION
SELECT 'A', '2019-03-01', 50
UNION
SELECT 'B', '2019-01-01', 214
UNION
SELECT 'B', '2019-02-01', 10
UNION
SELECT 'C', '2019-04-01', 150
INSERT INTO #MyTableTempDate
SELECT '2019-01-01'
UNION
SELECT '2019-02-01'
UNION
SELECT '2019-03-01'
UNION
SELECT '2019-04-01'
UNION
SELECT '2019-05-01'
UNION
SELECT '2019-06-01'
UNION
SELECT '2019-07-01';
------------- FOR NEWER SQL SERVER VERSION > 2005
WITH MyCTE AS
(
SELECT T.Product, T.ProductMonth AS 'MMonth', T.Qty
FROM #MyTable T
UNION
SELECT T.Product, TD.FullMonth AS 'MMonth', 0 AS 'Qty'
FROM #MyTable T, #MyTableTempDate TD
WHERE NOT EXISTS (SELECT 1 FROM #MyTable TT WHERE TT.Product = T.Product AND TD.FullMonth = TT.ProductMonth)
)
-- SELECT * FROM MyCTE;
SELECT Product, MMonth, Qty, SUM( Qty) OVER(PARTITION BY Product ORDER BY Product
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) as 'TotalQty'
FROM MyCTE
ORDER BY Product, MMonth ASC;
DROP TABLE #MyTable
DROP TABLE #MyTableTempDate
I have other way to perform this in lower SQL Server Version (like 2005 and lower)
It's a SELECT on SELECT if it's your case let me know and i provide some other example.
You can create the months with a recursive CTE
DECLARE #MyTable TABLE
(
ProductID CHAR(1),
Date DATE,
Amount INT
)
INSERT INTO #MyTable
VALUES
('A','2018-01-01', 5),
('A','2018-02-01', 3),
('A','2018-05-01', 5),
('B','2018-08-01', 10),
('B','2018-10-01', 12)
DECLARE #StartDate DATE
DECLARE #EndDate DATE
SELECT #StartDate = MIN(Date), #EndDate = MAX(Date) FROM #MyTable
;WITH dates AS (
SELECT #StartDate AS Date
UNION ALL
SELECT DATEADD(Month, 1, Date)
FROM dates
WHERE Date < #EndDate
)
SELECT A.ProductID, d.Date, COALESCE(Amount,0) AS Amount, COALESCE(SUM(Amount) OVER(PARTITION BY A.ProductID ORDER BY A.ProductID, d.Date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW),0) AS Total
FROM
(
SELECT ProductID, MIN(date) as DateStart, MAX(date) as DateEnd
FROM #MyTable
GROUP BY ProductID -- As I read in your comments that you need different min and max dates per product
) A
JOIN dates d ON d.Date >= A.DateStart AND d.Date <= A.DateEnd
LEFT JOIN #MyTable T ON A.ProductID = T.ProductID AND T.Date = d.Date
ORDER BY A.ProductID, d.Date
Try this below
IF OBJECT_ID('tempdb..#Temp') IS NOT NULL
DROP TABLE #Temp
;WITH CTE(Product,[Month],Qty)
AS
(
SELECT 'A','2018-01-01', 5 UNION ALL
SELECT 'A','2018-02-01', 3 UNION ALL
SELECT 'A','2018-05-01', 5 UNION ALL
SELECT 'B','2018-08-01', 10 UNION ALL
SELECT 'D','2018-10-01', 12
)
SELECT ct.Product,[MonthDays],ct.Qty
INTO #Temp
FROM
(
SELECT c.Product,[Month],
ISNULL(Qty,0) AS Qty
FROM CTE c
)ct
RIGHT JOIN
(
SELECT -- This code is to get month data
CONVERT(VARCHAR(10),'2018-'+ RIGHT('00'+CAST(MONTH(DATEADD(MM, s.number, CONVERT(DATETIME, 0)))AS VARCHAR),2) +'-01',120) AS [MonthDays]
FROM master.dbo.spt_values s
WHERE [type] = 'P' AND s.number BETWEEN 0 AND 11
)DT
ON dt.[MonthDays] = ct.[Month]
SELECT
MAX(Product)OVER(ORDER BY [MonthDays])AS Product,
[MonthDays],
ISNULL(Qty,0) Qty,
SUM(ISNULL(Qty,0))OVER(ORDER BY [MonthDays]) As SumQty
FROM #Temp
Result
Product MonthDays Qty SumQty
------------------------------
A 2018-01-01 5 5
A 2018-02-01 3 8
A 2018-03-01 0 8
A 2018-04-01 0 8
A 2018-05-01 5 13
A 2018-06-01 0 13
A 2018-07-01 0 13
B 2018-08-01 10 23
B 2018-09-01 0 23
D 2018-10-01 12 35
D 2018-11-01 0 35
D 2018-12-01 0 35
First of all, i would divide month and year to get easier with statistics.
I will give you an example query, not based on your table but still helpful.
--here i create the table that will be used as calendar
Create Table MA_MonthYears (
Month int not null ,
year int not null
PRIMARY KEY ( month, year) )
--/////////////////
-- here i'm creating a procedure to fill the ma_monthyears table
declare #month as int
declare #year as int
set #month = 1
set #year = 2015
while ( #year != 2099 )
begin
insert into MA_MonthYears(Month, year)
select #month, #year
if #month < 12
set #month=#month+1
else
set #month=1
if #month = 1
set #year = #year + 1
end
--/////////////////
--here you are the possible result you are looking for
select SUM(Ma_saledocdetail.taxableamount) as Sold, MA_MonthYears.month , MA_MonthYears.year , item
from MA_MonthYears left outer join MA_SaleDocDetail on year(MA_SaleDocDetail.DocumentDate) = MA_MonthYears.year
and Month(ma_saledocdetail.documentdate) = MA_MonthYears.Month
group by MA_SaleDocDetail.Item, MA_MonthYears.year , MA_MonthYears.month
order by MA_MonthYears.year , MA_MonthYears.month

MsSQL LEFT JOIN with double GROUP BY

I am trying currently using MsSQL for a product shipping DB. I have spent a long time trying to write a SQL query to get the amount of products going to each delivery location in an area, per date, in a 4 day period beginning today.
In an area means that there is another location that is parent to that location.
The tables concerned are Products and Locations and are structured as follows
Products
ProductID DeliveryDate DestinationID
1 2018-10-05 1
2 2018-10-05 2
3 2018-10-05 3
4 2018-10-06 1
5 2018-10-06 5
Locations
LocationID OwnerID
1 4
2 4
3 4
4 Null
5 6
6 Null
The output desired is as follows
DeliveryDate Destination ProductCount
2018-10-04 1 Null
2018-10-04 2 Null
2018-10-04 3 Null
2018-10-05 1 1
2018-10-05 2 1
2018-10-05 3 1
2018-10-06 1 1
2018-10-06 2 Null
2018-10-06 3 Null
2018-10-07 1 Null
2018-10-07 2 Null
2018-10-07 3 Null
What I have tried so far is this
DECLARE #startdate DATETIME
,#enddate DATETIME
SET #startdate = convert(varchar, '2018-10-04 00:00:00', 102)
SET #enddate = convert(varchar, '2018-10-07 00:00:00', 102)
;WITH DateArray
AS (
SELECT #startdate DateVal
UNION ALL
SELECT DateVal + 1
FROM DateArray
WHERE DateVal + 1 <= #enddate
)
SELECT * FROM
(SELECT da.DateVal AS DeliveryDate
FROM DateArray da) a
LEFT JOIN
(SELECT ISNULL(COUNT(p.ProductID),0) AS ProductCount,
DeliveryDate,
ISNULL(p.DestinationID,'') AS Destination
FROM Product p
AND p.DestinationID IN(SELECT LocationID FROM Locations WHERE OwnerID = 4)
GROUP BY p.DestinationID, p.DeliveryDate) AS b
ON b.DeliveryDate = a.DeliveryDate
The result ensures that all dates are present even if the ProductCount is null, however, not every Destination is shown if count is null. Shown below:
DeliveryDate Destination ProductCount
2018-10-04 Null Null
2018-10-05 1 1
2018-10-05 2 1
2018-10-05 3 1
2018-10-06 1 1
2018-10-07 Null Null
I have spent two days stubbornly trying to figure this out with many online SQL resources and scouring StackOverFlow but with no luck.
I recommend creating a numbers table or a calendar table, but you recursive query suffices for small sets.
DECLARE
#startdate DATETIME = '2018-10-04 00:00:00',
#enddate DATETIME = '2018-10-07 00:00:00';
WITH
DateArray AS
(
SELECT #startdate DateVal
UNION ALL
SELECT DateVal + 1 FROM DateArray WHERE DateVal + 1 <= #enddate
)
SELECT
DateArray.DateVal AS DeliveryDate,
Locations.LocationID AS Destination,
COUNT(Products.ProductID) AS ProductCount
FROM
DateArray
CROSS JOIN
(
SELECT * FROM Locations WHERE OwnerID = 4
)
Locations
LEFT JOIN
Products
ON Products.DeliveryDate = DateArray.DateVal
AND Products.DestinationID = Locations.LocationID
GROUP BY
DateArray.DateVal,
Locations.LocationID
Or...
SELECT
DateArray.DateVal AS DeliveryDate,
Locations.LocationID AS Destination,
Products.ProductCount AS ProductCount
FROM
DateArray
CROSS JOIN
(
SELECT * FROM Locations WHERE OwnerID = 4
)
Locations
LEFT JOIN
(
SELECT DeliveryDate, DestinationID, COUNT(*) AS ProductCount
FROM Products
GROUP BY DeliveryDate, DestinationID
)
Products
ON Products.DeliveryDate = DateArray.DateVal
AND Products.DestinationID = Locations.LocationID

Data according to time

I have table1 in this data is
ID Name StartDate EndDate
1 Paris 2014-02-01 00:00:00.000 2014-02-28 23:59:59.000
2 UK 2014-02-01 00:00:00.000 2014-02-28 23:59:59.000
3 France 2014-02-01 00:00:00.000 2014-02-28 23:59:59.000
and sp is
ALTER procedure [dbo].[spdata]
#fromdate datetime,
#todate datetime,
#Region varchar(50)
as
Select (Select Sum(Convert(int,SF)) from RVU inner dbo.VI vh on RVU.FID = vh.FID WHERE vh.No = Q.No and ID in (
Select ID from RU WHERE CAST(StartDate as date)>= CAST(#fromdate as date) and CAST(EndDate as date)<= CAST(#todate as date)
)) as SF
from (
Select
S.Name,
S.No,
SUM(Case when s.Vme='Car' then total else 0 end) as CAR,
SUM(Case when s.Vme='Tin' then total else 0 end) as Tin,
SUM(Case when s.Vme='Cake' then total else 0 end) as Cake,
SUM(Case when s.Vme='Flow' then total else 0 end) as Flow,
SUM(Case when s.Vme='Unit' then total else 0 end) as Unit,
SUM(total) total ,
MAX(S.Speed) Speed
from (
Select vh.Name as Name,vh.No as No,VV.Vame,count(VV.Vme) as total, RV.SF as MA,
RV.Speed from VVU VV inner join RVU RV on VV.MID=RV.ID inner join RU RU on RV.ID=RU.ID
left join dbo.VI vh on RV.FID = vh.FID WHERE CAST(RU.StartDate as date)>= CAST(#fromdate as date) and CAST(RU.EndDate as date)<= CAST(#todate as date) and
RU.Name_C= #Name_C AND Vme <> '' Group By vh.Name, vh.No, VV.Vme, RV.SF,
RV.Speed ) S GROUP BY s.RegNo, s.Name) Q
from that sp when i enter parameters DATA IS
[spdata] '2016-07-01 00:00:00.000', '2016-07-31 23:59:59.000', 'pARIS'
Name No CAR Tin Cake Flow Unit total Speed SF
John 412 0 0 12 0 5 17 82 60
Mike 48 2 1 5 1 3 9 160 464
ACNme 438 0 1 5 2 3 11 10 264
XYZ 248 0 1 5 3 3 12 60 244
now i want when i change time '2016-07-01 00:00:00.000', '2016-07-31 23:59:59.000',
like this
'2016-07-01 02:02:00.000', '2016-07-31 12:59:59.000',
then records also reflect on this time means according to date plus time data will be display
Don't cast your StartDate , EndDate , #fromdate , #todate as Date.
`Alter procedure [dbo].[spdata]
#fromdate datetime,
#todate datetime,
#Region varchar(50)
as
Select (Select Sum(Convert(int,SF)) from RVU inner dbo.VI vh on RVU.FID = vh.FID WHERE vh.No = Q.No and ID in (
Select ID from RU WHERE StartDate >= #fromdate and EndDate <=#todate
)) as SF
from (
Select
S.Name,
S.No,
SUM(Case when s.Vme='Car' then total else 0 end) as CAR,
SUM(Case when s.Vme='Tin' then total else 0 end) as Tin,
SUM(Case when s.Vme='Cake' then total else 0 end) as Cake,
SUM(Case when s.Vme='Flow' then total else 0 end) as Flow,
SUM(Case when s.Vme='Unit' then total else 0 end) as Unit,
SUM(total) total ,
MAX(S.Speed) Speed
from (
Select vh.Name as Name,vh.No as No,VV.Vame,count(VV.Vme) as total, RV.SF as MA,
RV.Speed from VVU VV inner join RVU RV on VV.MID=RV.ID inner join RU RU on RV.ID=RU.ID
left join dbo.VI vh on RV.FID = vh.FID WHERE RU.StartDate >= #fromdate and RU.EndDate <= #todate and
RU.Name_C= #Name_C AND Vme <> '' Group By vh.Name, vh.No, VV.Vme, RV.SF,
RV.Speed
) S GROUP BY s.RegNo, s.Name) Q`

sql server allocate payment to items

I have been scratching my head with this one for an hour still cannot seem to figure out a way to allocate Payment amount of $30 to the rows in the following table.
Given that i have the following items. Negative amount means the customer is in debt and owes us that amount. Now given that customer pays $30. We need to allocate that to the item.
ItemId amount sDATE
BD98E890-C7F8-47F4-9125-A68A88DD178D -10 2016-01-04 00:00:00.000
7E047DE6-0DB7-4EDB-A751-C43BBD4610E5 -20 2016-01-05 00:00:00.000
5004AE1F-2A15-47E5-96FF-69A6C7D35521 -10 2016-01-06 00:00:00.000
for a payment of $30 the output should look like.
itemId BeforeAllocation AfterAllocation LeftToAllocate sDate
BD98E890-C7F8-47F4-9125-A68A88DD178D -10 0 30 2016-01-04 00:00:00.000
7E047DE6-0DB7-4EDB-A751-C43BBD4610E5 -20 0 20 2016-01-05 00:00:00.000
5004AE1F-2A15-47E5-96FF-69A6C7D35521 -10 -10 0 2016-01-06 00:00:00.000
and if customer is paying partial amount for exmaple $25 the output should be.
itemId BeforeAllocation AfterAllocation LeftToAllocate sDate
BD98E890-C7F8-47F4-9125-A68A88DD178D -10 0 25 2016-01-04 00:00:00.000
7E047DE6-0DB7-4EDB-A751-C43BBD4610E5 -20 -5 15 2016-01-05 00:00:00.000
5004AE1F-2A15-47E5-96FF-69A6C7D35521 -10 -10 0 2016-01-06 00:00:00.000
Code:
Create table #temp(ItemId UNIQUEIDENTIFIER , amount INT, sDATE DATETIME)
INSERT INTO #temp
( ItemId,
amount,
sDATE )
VALUES ( NEWID(),-10,'2016-01-04' ),
( NEWID(),-20,'2016-01-05' ),
( NEWID(),-10,'2016-01-06' )
SELECT * FROM (
SELECT 'BD98E890-C7F8-47F4-9125-A68A88DD178D' itemId, -10 BeforeAllocation, 0 AfterAllocation, 30 LeftToAllocate, '2016-01-04 00:00:00.000' sDate
UNION
SELECT '7E047DE6-0DB7-4EDB-A751-C43BBD4610E5' itemId, -20 BeforeAllocation, 0 AfterAllocation, 20 LeftToAllocate, '2016-01-05 00:00:00.000' sDate
UNION
SELECT '5004AE1F-2A15-47E5-96FF-69A6C7D35521' itemId, -10 BeforeAllocation, -10 AfterAllocation,0 LeftToAllocate, '2016-01-06 00:00:00.000' sDate
)s
ORDER BY sdate
SELECT * FROM (
SELECT 'BD98E890-C7F8-47F4-9125-A68A88DD178D' itemId, -10 BeforeAllocation, 0 AfterAllocation, 25 LeftToAllocate, '2016-01-04 00:00:00.000' sDate
UNION
SELECT '7E047DE6-0DB7-4EDB-A751-C43BBD4610E5' itemId, -20 BeforeAllocation, -5 AfterAllocation, 15 LeftToAllocate, '2016-01-05 00:00:00.000' sDate
UNION
SELECT '5004AE1F-2A15-47E5-96FF-69A6C7D35521' itemId, -10 BeforeAllocation, -10 AfterAllocation,0 LeftToAllocate, '2016-01-06 00:00:00.000' sDate
)s
ORDER BY sdate
Calculate an expression BeforeAllocationRT, to record the running total of BeforeAllocation (i.e. the sum of BeforeAllocation for this row and all preceding rows). If you're using SQL Server 2012 or later you can use window functions, otherwise you need a clumsy subexpression - see this question for exact instructions.
Calculate an expression for the amount to allocate
SELECT Allocation = CASE
WHEN BeforeAllocationRT + #Payment > 0
-- pay full amount for this item
THEN -#BeforeAllocation
WHEN BeforeAllocationRT + #Payment > #BeforeAllocation
-- pay partial amount for this item
THEN -(#BeforeAllocationRT+#Payment)
ELSE
-- pay nothing for this item
0
END
, ...
Calculate expressions for AfterAllocation and LeftToAllocate.
SELECT
AfterAllocation = BeforeAllocation + Allocation
,LeftToAllocate = CASE WHEN BeforeAllocationRT+#Payment>0 THEN #Payment-BeforeAllocationRT ELSE 0 END
,...
Combine steps 1,2,3 using CTEs or subexpressions.
Disclaimer: I don't have access to a SQL Server instance right now, so none of this is tested.
try this,
DECLARE #PaidAmout INT = 30
;WITH CTE AS
(
SELECT ItemId, amount, sDATE, ROW_NUMBER() OVER(ORDER BY sDATE) AS RowNo
FROM #temp
),
Amount AS
(
SELECT ItemId,
RowNo,
amount AS BeforeAllocation,
0 AS AfterAllocation,
#PaidAmout AS LeftToAllocate,
sDATE,
#PaidAmout + Amount AS LeftAmount
FROM CTE
WHERE RowNo = 1
UNION ALL
SELECT c.ItemId,
c.RowNo,
c.amount AS BeforeAllocation,
CASE WHEN a.LeftAmount + c.Amount < 0 THEN a.LeftAmount + c.Amount ELSE 0 END AS AfterAllocation,
CASE WHEN a.LeftAmount < 0 THEN 0 ELSE a.LeftAmount END AS LeftToAllocate,
c.sDATE,
a.LeftAmount + c.Amount AS LeftAmount
FROM CTE c
INNER JOIN Amount a ON a.RowNo + 1 = c.RowNo
)
SELECT ItemId,
BeforeAllocation,
AfterAllocation,
LeftToAllocate,
sDATE
FROM Amount

Select SUM by each year in SQL query

I have these two tables:
Expense table
id expense expense_date product_id
1 10 2012-01-03 1
2 10 2014-02-01 2
3 10 2014-02-03 1
4 10 2012-07-03 1
Product table
product_id product_name purchase_date
1 car 2010-02-01
2 bike 2014-03-01
I would like to achieve the result to something like this by summing the expenses grouping by product_id where the the expense_date is between the purchase_date to it's next year:
Year Expense Product_name
1 0 car 2010-02-01 to 2011-02-01
2 10 car 2011-02-01 to 2012-02-01
3 10 car 2012-02-01 to 2013-02-01
4 0 car 2013-02-01 to 2014-02-01
5 10 car 2014-02-01 to 2015-02-01
1 10 bike 2014-03-01 to 2015-03-01
CREATE VIEW dbo.vwMaxExpenseDate
AS
SELECT product_id, MAX(expense_date) AS 'max_expense_date'
FROM Expense
GROUP BY product_id
DECLARE #PossibleYearRange TABLE
(
product_id INT,
YearStart DATETIME,
YearEnd DATETIME
);
WITH CTE
AS
(
SELECT p.product_id, max_expense_date, purchase_date, 1 As Number
FROM Product p
LEFT JOIN vwMaxExpenseDate e
ON p.product_id = e.product_id
UNION ALL
SELECT product_id, max_expense_date, purchase_date, Number + 1
FROM CTE
WHERE Number <= (YEAR(max_expense_date) - YEAR(purchase_date))
)
INSERT INTO #PossibleYearRange
(
product_id,
YearStart,
YearEnd
)
SELECT product_id,
CONVERT(DATETIME, CONVERT(VARCHAR(20), YEAR(purchase_date) + Number - 1) + '-'
+ CONVERT(VARCHAR(20), MONTH(purchase_date)) + '-'
+ CONVERT(VARCHAR(20), DAY(purchase_date))) AS 'YearStart',
CONVERT(DATETIME, CONVERT(VARCHAR(20), YEAR(purchase_date) + Number) + '-'
+ CONVERT(VARCHAR(20), MONTH(purchase_date)) + '-'
+ CONVERT(VARCHAR(20), DAY(purchase_date))) AS 'YearEnd'
FROM CTE
ORDER BY product_id ASC, NUMBER ASC
SELECT MAX(p.product_id) AS product_id, MAX(product_name) AS product_name, YearStart, YearEnd, COALESCE(SUM(expense), 0) AS TotalExpensePerYear
FROM #PossibleYearRange p
LEFT JOIN Expense e
ON p.product_id = e.product_id AND
expense_date BETWEEN YearStart AND YearEnd
INNER JOIN Product d
ON p.product_id = d.product_id
GROUP BY YearStart, YearEnd
ORDER BY MAX(p.product_id) ASC
Hope this helps! Thanks.
declare #BeginsAt as datetime
declare #numOf as int
set #BeginsAt = (select min(purchase_date) from Products)
set #BeginsAt = dateadd(year,datediff(year,0,#BeginsAt),0) -- force to 1st of Year
set #numOf = (year(getdate()) - year(#BeginsAt))+1
;with YearRange (id, StartAt, StopAt)
as (
select 1 as id, #BeginsAt, dateadd(Year,1,#BeginsAt)
union all
select (id + 1) , dateadd(Year,1,StartAt) , dateadd(Year,1,StopAt)
from YearRange
where (id + 1) <= #numOf
)
select
y.id
, coalesce(e.expense,0) expense
, p.product_name
, y.startAt
, dateadd(day,-1,y.StopAt)
from YearRange Y
left join Products P on Y.StopAt between P.purchase_date AND (select max(StopAt) from YearRange)
left join Expenses E on E.expense_date >= Y.StartAt and E.expense_date < Y.StopAt
and E.product_id = P.product_id
See this SQLFiddle demo