Рассчитать прибыль, основанную на ценах First-In, First-Out

Скажем, у меня есть данные о покупке и продаже для некоторых SKU:

po_id | sku | purchase_date    | price | qty
----------------------------------------------
    1 | 123 | 2013-01-01 12:25 | 20.15 |   5
    2 | 123 | 2013-05-01 15:45 | 17.50 |   3
    3 | 123 | 2013-05-02 12:00 | 15.00 |   1
    4 | 456 | 2013-06-10 16:00 | 60.00 |   7

sale_id | sku | sale_date        | price | qty
------------------------------------------------
      1 | 123 | 2013-01-15 11:00 | 30.00 |   1
      2 | 123 | 2013-01-20 14:00 | 28.00 |   3
      3 | 123 | 2013-05-10 15:00 | 25.00 |   2
      4 | 456 | 2013-06-11 12:00 | 80.00 |   1

Как я могу найти маржу продаж через SQL, если они продаются в том порядке, в котором они были приобретены? Например, край для sku 123 равен

30*1 + 28*3 + 25*2 - 20.15*5 - 17.50*1

с 2 купленными в 17.50 и 1, купленные в 15.00, остались непроданными.

Ответ 1

Хороший вопрос. Подход, который я принимаю, - это рассчитать общий объем продаж. Затем вычислите кумулятивные покупки и объедините их со специальной логикой, чтобы получить правильную арифметику для комбинации:
select s.sku,
       (MarginPos - SUM(case when s.totalqty < p.cumeqty - p.qty then p.price * p.qty
                             when s.totalqty between p.cumeqty - p.qty and p.qty
                             then s.price * (s.totalqty - (p.cumeqty - p.qty))
                             else 0
                        end)
       ) as Margin
from (select s.sku, SUM(price*qty) as MarginPos, SUM(qty) as totalqty
      from sales s
     ) s left outer join
     (select p.*,
             (select SUM(p.qty) from purchase p2 where p2.sku = p.sku and p2.sale_id <= p.sale_id
             ) as cumeqty
      from purchase s
     )
     on s.sku = p.sku
group by s.sku, MarginPos

Примечание. Я не тестировал этот запрос, чтобы он мог иметь синтаксические ошибки.

Ответ 2

настройка окружающей среды

    declare @purchased table (id int,sku int,dt date,price money,qty int)
    declare @sold table (id int,sku int,dt date,price money,qty int)

    insert into @purchased
    values( 1 , 123 , '2013-01-01 12:25' , 20.15 ,   5)
        ,(2 , 123 , '2013-05-01 15:45' , 17.50 ,   3)
        ,(3 , 123 , '2013-05-02 12:00' , 15.00 ,   1)
        ,(4 , 456 , '2013-06-10 16:00' , 60.00 ,   7)

    insert into @sold
    values(1 , 123 , '2013-01-15 11:00' , 30.00 ,   1)
          ,(2 , 123 , '2013-01-20 14:00' , 28.00 ,   3)
          ,(3 , 123 , '2013-05-10 15:00' , 25.00 ,   2)
          ,(4 , 456 , '2013-06-11 12:00' , 80.00 ,   1)

Решение sqlserver должно быть...

    with cte_sold as (select sku,sum(qty) as qty, SUM(qty*price) as total_value
                      from @sold
                      group by sku
                      )
    ,cte_purchased as (select id,sku,price,qty 
                       from @purchased
                       union all select id,sku,price,qty-1 as qty 
                       from cte_purchased
                       where qty>1
                    )
    ,cte_purchased_ordened as(select ROW_NUMBER() over (partition by sku order by id,qty) as buy_order
                                    ,sku
                                    ,price
                                    ,1 as qty
                              from cte_purchased 
    )

    select P.sku
          ,S.total_value - SUM(case when P.buy_order <= S.qty then P.price else 0 end) as margin
    from cte_purchased_ordened P
    left outer join cte_sold S
    on S.sku = P.sku
    group by P.sku,S.total_value,S.qty

достигнуты результаты

    sku margin
    123 45,75
    456 20,00

тот же результат для примера sku 123 в описании проблемы...

30 * 1 + 28 * 3 + 25 * 2 - 20,15 * 5 - 17,50 * 1 = 45,75

Ответ 3

Это действительно ужасно, так как он изменяет переменную MySQL в запросах, но она вроде работает (и принимает 3 оператора):

select
  @income := sum(price*qty) as income,
  @num_bought := cast(sum(qty) as unsigned) as units
from sale
where sku = 123
;

select
  @expense := sum(expense) as expense,
  sum(units) as units
from (select
  price * least(@num_bought, qty) as expense,
  least(@num_bought, qty) as units,
  @num_bought := @num_bought - least(@num_bought, qty)
from purchase
where sku = 123 and @num_bought > 0
order by po_id
) as a
;

select round(@income - @expense, 2) as profit_margin;

Ответ 4

Это запрос Oracle, но он должен работать в любом SQL. Он упрощен и не включает в себя все необходимые вычисления. Вы можете добавить их сами. Вы увидите незначительные значения разницы как 17.50 * 3, а не 17.50 * 1:

SELECT po_sku AS sku, po_total, sale_total, (po_total-sale_total) Margin
  FROM
 (
  SELECT SUM(price*qty) po_total, sku po_sku
    FROM stack_test
  GROUP BY sku
 ) a,
 (
  SELECT SUM(price*qty) sale_total, sku sale_sku
    FROM stack_test_sale
   GROUP BY sku
 ) b
 WHERE po_sku = sale_sku
 /

SKU     PO_TOTAL    SALE_TOTAL  MARGIN
---------------------------------------------------
123     168.25      164         4.25
456     420         80          340

Вы также можете добавить раздел с помощью SKU:

SUM(price*qty) OVER (PARTITION BY sku ORDER BY sku)