I have some sales data and I need to dispatch the quantities in available slots stored in a separate table.
Example of sales data:
| id | qty |
|---|---|
| 1 | 1 |
| 2 | 1 |
| 3 | 1 |
| 4 | 1 |
| 5 | 3 |
| 6 | 9 |
create table sales (id serial primary key, qty int);
INSERT INTO sales (qty) VALUES (1),(1),(1),(1),(3),(9);
And here are the slots (size is cumulative):
| name | size |
|---|---|
| a | 3 |
| b | 5 |
| c | 9 |
There is an "invisible" d slot for the remaining quantities. Of course, these slots are not fixed.
create table slots (id serial primary key, name text, size int);
insert into slots (name,size) values ('a',3),('b',5),('c',9);
This is the detailed process of the calculation (and expected result):
| id | qty | used | total | slot | comment |
|---|---|---|---|---|---|
| 1 | 1 | 1 | 1 | a | |
| 2 | 1 | 1 | 2 | a | |
| 3 | 1 | 1 | 3 | a | Slot is full |
| 4 | 1 | 1 | 4 | b | |
| 5 | 3 | 1 | 5 | b | split this sale |
| 5 | cont. | 2 | 6-7 | c | |
| 6 | 9 | 2 | 8-9 | c | |
| 6 | cont. | 7 | 10-16 | d | split again |
My approach is the following:
WITH p1 AS (
select sales.*,sum(qty) over (order by sales.id) AS tot
from sales
order by sales.id
)
, p2 AS (
select p1.id
,p1.qty
, 1+COALESCE(LAG(tot) OVER (ORDER BY p1.id) ,0) AS lag
, p1.tot
from p1
)
,p3 AS (
SELECT id,qty,lag,tot
, COALESCE( (SELECT id FROM slots WHERE lag<=size ORDER BY size LIMIT 1) , 999) AS slotmin
, COALESCE( (SELECT id FROM slots WHERE tot<=size ORDER BY size LIMIT 1) , 999) AS slotmax
FROM p2
)
SELECT p3.id
,p3.qty
-- ,p3.lag
-- ,p3.tot
-- ,p3.slotmin
-- ,p3.slotmax
,COALESCE(slots.size-lag+1,p3.qty) AS qtyconsumed
,1 AS so
from p3
LEFT JOIN slots ON slots.id=p3.slotmin AND slotmin<>slotmax
UNION
SELECT p3.id
,p3.qty
-- ,p3.lag
-- ,p3.tot
-- ,p3.slotmin
-- ,p3.slotmax
,COALESCE(p3.qty-(slots.size-lag+1),0) AS qtyconsumed
,2 AS so
from p3
LEFT JOIN slots ON slots.id=p3.slotmin
WHERE slotmin<>slotmax
ORDER BY id,so
I deliberately left some commented columns to ease understanding/debugging my mental process
Which gives the following result:
id | qty | qtyconsumed | so
----+-----+-------------+----
1 | 1 | 1 | 1
2 | 1 | 1 | 1
3 | 1 | 1 | 1
4 | 1 | 1 | 1
5 | 3 | 1 | 1
| | 2 | 2
6 | 9 | 2 | 1
| | 7 | 2
(8 lignes)
The code is pretty ugly and will only work for one split: I could very well have only one sale of qty=20 that should split to 4 slots.
My question unfolds in 2 parts:
Is it possible to do that splitting in more than 2 slots without going through the mess of cursors or solutions alike? I am guessing WITH RECURSIVE but there could be something smarter.
How can I optimize this (particularly the search for the closest slot in
p3) because the real case involves millions of rows?