I want to find out the stock of each product for each month as it comes into the warehouse. Here is how far I got
MonthNumber | Year | work_area | product_id | quantity | running_total |
---|---|---|---|---|---|
1 | 2025 | NULL | NULL | NULL | NULL |
2 | 2025 | 1 | 8 | 1000.00 | 1000.00 |
3 | 2025 | NULL | NULL | NULL | NULL |
4 | 2025 | 1 | 8 | 500.00 | 1500.00 |
5 | 2025 | NULL | NULL | NULL | NULL |
6 | 2025 | NULL | NULL | NULL | NULL |
7 | 2025 | NULL | NULL | NULL | NULL |
8 | 2025 | NULL | NULL | NULL | NULL |
9 | 2025 | NULL | NULL | NULL | NULL |
10 | 2025 | NULL | NULL | NULL | NULL |
11 | 2025 | NULL | NULL | NULL | NULL |
12 | 2025 | NULL | NULL | NULL | NULL |
The reason it's null in January and in some other months cause we don't order this product every month. Ideally, the final result I want is
MonthNumber | Year | work_area | product_id | quantity | running_total |
---|---|---|---|---|---|
1 | 2025 | 1 | 8 | 0 | 0 |
2 | 2025 | 1 | 8 | 1000 | 1000 |
3 | 2025 | 1 | 8 | 0 | 1000 |
4 | 2025 | 1 | 8 | 500 | 1500 |
5 | 2025 | 1 | 8 | 0 | 1500 |
6 | 2025 | 1 | 8 | 0 | 1500 |
7 | 2025 | 1 | 8 | 0 | 1500 |
8 | 2025 | 1 | 8 | 0 | 1500 |
9 | 2025 | 1 | 8 | 0 | 1500 |
10 | 2025 | 1 | 8 | 0 | 1500 |
11 | 2025 | 1 | 8 | 0 | 1500 |
12 | 2025 | 1 | 8 | 0 | 1500 |
My query
;WITH year_order_received_cte AS (
SELECT Distinct DATEPART(YEAR, date_receive) AS year_order_receive
FROM Inv.Ordering_Receive
), all_month_in_year_received_cte AS (
SELECT DISTINCT MonthNumber, [Year]
FROM dbO.Dim_Date
WHERE [Year] in (SELECT year_order_receive FROM year_order_received_cte)
), product_arrived_each_month_cte AS (
SELECT work_area_id, t1.ordering_id, t2.product_id , date_receive, DATEPART(Month, date_receive) AS month_received, DATEPART(YEAR, date_receive) AS year_received
, t2.quantity
FROM Inv.Ordering t1
INNER JOIN Inv.Ordering_Item t2 ON t2.ordering_id = t1.ordering_id
INNER JOIN Inv.Ordering_Receive t3 ON t3.ordering_id=t2.ordering_id
WHERE received=1
), inventory_each_month_cte AS (
SELECT MonthNumber, [Year], work_area_id, product_id, quantity
, (SELECT SUM(COALESCE(quantity,0)) FROM product_arrived_each_month_cte t2 WHERE t2.month_received <= t1.month_received) AS running_total
FROM all_month_in_year_received_cte t0
LEFT OUTER JOIN product_arrived_each_month_cte t1 ON t1.month_received = t0.MonthNumber AND t1.year_received = t0.[Year]
)
SELECT *
FROM inventory_each_month_cte
ORDER BY MonthNumber
Thanks
So you want a row for each year/month for each work_area
/product_id
combination.
Your query systematically emits rows for each year/month (in all_month_in_year_received_cte
), but LEFT JOIN
s to the work_area
/product_id
pairs only at the end, thus resulting in NULL
s for the work_area
and product_id
columns.
So you'll need your all_month_in_year_received_cte
to include your possible work_area
/product_id
pairs too.
And propagate them until they reach the LEFT OUTER JOIN
.
WITH year_order_received_cte AS (
SELECT Distinct DATEPART(YEAR, date_receive) AS year_order_receive
, work_area_id, r.product_id -- ← Add this line
FROM /*Inv.*/Ordering_Receive
r JOIN Ordering_Item i ON i.ordering_id = r.ordering_id -- ← Join the table holding the work_area_id.
), all_month_in_year_received_cte AS (
SELECT DISTINCT MonthNumber, [Year]
, work_area_id, product_id -- ← Add this line
FROM dbO.Dim_Date
d JOIN year_order_received_cte o ON d.Year = o.year_order_receive -- ← Join instead of subselecting
/*
WHERE [Year] in (SELECT year_order_receive FROM year_order_received_cte)
*/
), product_arrived_each_month_cte AS (
SELECT work_area_id, t1.ordering_id, t2.product_id , date_receive, DATEPART(Month, date_receive) AS month_received, DATEPART(YEAR, date_receive) AS year_received
, t2.quantity
FROM /*Inv.*/Ordering t1
INNER JOIN /*Inv.*/Ordering_Item t2 ON t2.ordering_id = t1.ordering_id
INNER JOIN /*Inv.*/Ordering_Receive t3 ON t3.ordering_id=t2.ordering_id
WHERE received=1
), inventory_each_month_cte AS (
SELECT MonthNumber, [Year], t0.work_area_id, t0.product_id, COALESCE(quantity, 0) quantity -- ← Use the work_area_id and product_id from the left, not the right (incomplete) part of the join. And coalesce your quantity to 0.
/*
SELECT MonthNumber, [Year], work_area_id, product_id, quantity
*/
, (SELECT COALESCE(SUM(COALESCE(quantity,0)), 0) FROM product_arrived_each_month_cte t2 WHERE t2.month_received <= t0.MonthNumber AND t2.year_received = t0.year and t2.work_area_id = t0.work_area_id and t2.product_id = t0.product_id) AS running_total -- ← Here too, use the always filled month (t0 instead of t1) and DO NOT FORGET TO FILTER ON YEAR, WORK AREA, AND PRODUCT TOO if you are running on multiple of them. Finally coalesce to 0 for January if your first receive date is in february (as sum(no row) = null).
/*
, (SELECT SUM(COALESCE(quantity,0)) FROM product_arrived_each_month_cte t2 WHERE t2.month_received <= t1.month_received) AS running_total
*/
FROM all_month_in_year_received_cte t0
LEFT OUTER JOIN product_arrived_each_month_cte t1 ON t1.month_received = t0.MonthNumber AND t1.year_received = t0.[Year]
AND t1.product_id = t0.product_id AND t1.work_area_id = t0.work_area_id -- ← Add this line
)
SELECT *
FROM inventory_each_month_cte
ORDER BY MonthNumber;
(see it running in a fiddle)
As you didn't use a window function I suppose this is something new to you; you could try learning them, as they have the exact goal you're trying to reach with your subselect of "which quantity have I already received on rows for the same year / work area / product on previous months": computing rolling things (rolling sum in your case).
Thus you will be able to transform your:
(
SELECT COALESCE(SUM(COALESCE(quantity,0)), 0)
FROM product_arrived_each_month_cte t2
WHERE t2.month_received <= t0.MonthNumber
AND t2.year_received = t0.year and t2.work_area_id = t0.work_area_id and t2.product_id = t0.product_id
)
to:
SUM(COALESCE(quantity,0))
OVER
(
PARTITION BY t0.year, t0.work_area_id, t0.product_id
ORDER BY t0.MonthNumber
)
(I've put it as the second query of my fiddle)
In essence they can be considered as an aggregate over a self-join to the resultset, which is exactly what you where looking for.
By reading that SUM() OVER ()
hereabove, you probably noticed that the PARTITION BY
held the conditions of join (corresponding to the t2.… = t0.…
clauses);
The only non-explicit bit is maybe the t2.month_received <= t0.MonthNumber
part in your subselect, that has no equivalent in the windowed SUM()
: that's because this is the implicit, "by default" rule of window functions with an ORDER BY
: they run from the start of the window / group, until the current row. So SUM(…) OVER (ORDER BY MonthNumber)
is a shortcut to the more explicit SUM(…) OVER (ORDER BY MonthNumber ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW)
(with UNBOUNDED PRECEDING
meaning START
: think of going backward from current row, with no other limit than having no more row in the group).