Reputation: 627
I'm trying to find a set based method to do the following:
Supose I have only one kind of product in my warehouse. This product comes in boxes of different sizes. I have to fulfill the orders using my stock, spliting the containers as needed to achieve the exact ordered quantity. Both orders and stock items have to be processed FIFO.
As an example, here are some values and the solution (computed by hand).
+---------+-------+ +-----------+-------+
| OrderId | OQtty | | Container | CQtty |
+---------+-------+ +-----------+-------+
| 1 | 87000 | | 1 | 10000 |
| 2 | 26500 | | 2 | 10000 |
| 3 | 12000 | | 3 | 10000 |
| 4 | 43600 | | 4 | 10000 |
| 5 | 3000 | | 5 | 10000 |
| | | | 6 | 5000 |
| | | | 7 | 10000 |
| | | | 8 | 10000 |
| | | | 9 | 10000 |
| | | | 10 | 10000 |
| | | | 11 | 10000 |
| | | | 12 | 10000 |
| | | | 13 | 10000 |
| | | | 14 | 2500 |
| | | | 15 | 2500 |
| | | | 16 | 10000 |
| | | | 17 | 10000 |
| | | | 18 | 10000 |
| | | | 19 | 10000 |
| | | | 20 | 10000 |
+---------+-------+ +-----------+-------+
+---------+-----------+-------+---------+
| OrderId | Container | CQtty | Running |
+---------+-----------+-------+---------+
| 1 | 1 | 10000 | 10000 |
| 1 | 2 | 10000 | 20000 |
| 1 | 3 | 10000 | 30000 |
| 1 | 4 | 10000 | 40000 |
| 1 | 5 | 10000 | 50000 |
| 1 | 6 | 5000 | 55000 |
| 1 | 7 | 10000 | 65000 |
| 1 | 8 | 10000 | 75000 |
| 1 | 9 | 10000 | 85000 |
| 1 | 10 | 2000 | 87000 |
| 2 | 10 | 8000 | 8000 |
| 2 | 11 | 10000 | 18000 |
| 2 | 12 | 8500 | 26500 |
| 3 | 12 | 1500 | 1500 |
| 3 | 13 | 10000 | 11500 |
| 3 | 14 | 500 | 12000 |
| 4 | 14 | 2000 | 2000 |
| 4 | 15 | 2500 | 4500 |
| 4 | 16 | 10000 | 14500 |
| 4 | 17 | 10000 | 24500 |
| 4 | 18 | 10000 | 34500 |
| 4 | 19 | 9100 | 43600 |
| 5 | 19 | 900 | 900 |
| 5 | 20 | 2100 | 3000 |
+---------+-----------+-------+---------+
Edit: I forgot to mention this is green field sql server 2012. Thanks
Upvotes: 1
Views: 340
Reputation: 10918
WITH
O AS (SELECT *,SUM(OQtty) OVER(ORDER BY OrderId) Ort FROM @Orders),
C AS (SELECT *,SUM(CQtty) OVER(ORDER BY Container) Crt FROM @Containers)
SELECT
OrderId, Container, CQtty,
CASE WHEN Crt < Ort THEN Crt-Ort+OQtty ELSE Oqtty END AS Running
FROM O
INNER JOIN C ON Crt > Ort-OQtty AND Crt < Ort+CQtty
ORDER BY OrderId, Container
Demo: http://sqlfiddle.com/#!6/cb038/1
This is a Partition Alignment problem. It's the same type of problem as trying to match the countries of North and South America with the countries of Europe and Africa based on which ones have Atlantic coastlines directly East-West from each other. The solution should be agnostic to which group is actually East and which group is actually West. All that is needed is this logic:
If the difference between the latitudes of the southern extremes of two countries is less than the latitudinal 'height' of either country, then they overlap
Likewise, in this problem the 'containers' and 'orders' are fully interchangeable. The only thing that matters is the difference between the running totals. If ABS(Crt - Ort) < MAX(OQtty, CQtty), then that container pairs with that order.
Upvotes: 1
Reputation: 1271023
This can be done using window functions, but it is a bit tricky. The reason it can be done is because you are counting the total quantity. In your example, part of container 8 goes to order 1 and part to order 2. Believe or not, this is much easier than if part of container 8 goes to order 1 . . . and the rest is wasted.
The idea is simple. Do a cumulative sum on the containers and orders. Then use a non-equijoin to find the containers used for each order -- this is where the cumulative sums overlaps. Then do some more finagling to handle the boundaries. Here is an idea of how to do this:
with oq as (
select o.*,
sum(oqty) over (order by orderid) - oqty as sumoqty_start,
sum(oqty) over (order by orderid) as sumoqty_end
from orders o
),
cq as (
select c.*,
sum(cqty) over (order by container) - cty as sumcqty_start,
sum(cqty) over (order by container) as sumcqty_end
from contains c
),
oc as (
select o.orderid, o.sumoqty_start, o.sumoqty_end,
c.container, c.sumcqty_start, c.sumcty_end
from oq join
cq
on oc.sumoqty_start < sumcqty_end and
oc.sumoqty_end > sumcqty_start
)
select o.orderid, c.containerid,
((case when sumcqty_end < sumoqty_end then sumcqty_end else sumoqty_end end) -
(case when sumcqty_start > sumoqty_start then sumcqty_start else sumoqty_start end)
) as OrderContainerAmount
from oc
order by 1, 2;
I think this is right. Without a SQL Fiddle, I'm not able to test it.
Upvotes: 0