В стиле SQL - 3 (распределение сумм)
Apr. 9th, 2014 07:50 pmПосмотрим на пример из жизни. В разных вариациях он встречался мне раза три как минимум.
Пусть имеется некое подобие складского учета, в котором ведутся только транзакции поступления позиций на склад и выбытия позиций со склада. Атрибутами транзакций являются:
- собственно позиция (item_id) — стол офисный, кадка с фикусом и т. п.
- стоимость одной штуки (cost)
- количество штук (quantity) — положительно для поступлений, отрицательно для выбытий
- дата (trx_date)
При этом так получилось, что для поступлений мы знаем стоимость позиций (сами же покупали), а для выбытий — нет (кладовщик выдает первую попавшуюся подходящую позицию; неизвестно, когда и по какой стоимости она закупалась).
Задача: написать отчет, показывающий стоимость наличных остатков каждой позиции. За неимением реальной информации о выбытиях считать, что позиции уходят со склада в порядке FIFO.
На примере (для одной позиции):
- Поступление 5 штук по цене 1001 руб.
- Поступление 4 штук по цене 1010 руб.
- Выбытие 3 штук
цена поступило выбыло осталось 1001 5 3 2 1010 4 0 4 - Выбытие 3 штук
цена поступило выбыло осталось 1001 5 3+2 0 1010 4 1 3 - Поступление 2 штук по цене 1100 руб.
цена поступило выбыло осталось 1001 5 3+2 0 1010 4 1 3 1100 2 0 2
Итак, отчет должен показать 3×1010 + 2×1100 = 5230.
Лирическое отступление. Числа в примерах лучше подбирать так, чтобы по результату можно было сразу сказать, откуда он взялся. 5230 читается справа налево: осталось 0 от первого поступления, 3 от второго и 2 от третьего, 5 — контрольная цифра. А вот попробуйте понять, что такое 2000 при цене поступлений 1000 и 500.
Процедурное решение вполне очевидно. Например, можно сделать так:
create table transactions( item_id number, quantity number, cost number, trx_date date ); create or replace type trx_rec is object( item_id number, quantity number, cost number ); / create or replace type trx_tab is table of trx_rec; / -- транзакции из примера insert into transactions values(1, +5, 1001, to_date('01.02.2014','DD.MM.YYYY')); insert into transactions values(1, +4, 1010, to_date('02.02.2014','DD.MM.YYYY')); insert into transactions values(1, -3, null, to_date('05.02.2014','DD.MM.YYYY')); insert into transactions values(1, -3, null, to_date('07.02.2014','DD.MM.YYYY')); insert into transactions values(1, +2, 1100, to_date('09.02.2014','DD.MM.YYYY')); -- добавим еще вторую позицию insert into transactions values(2, +3, 300, to_date('03.02.2014','DD.MM.YYYY')); insert into transactions values(2, -1, null, to_date('04.02.2014','DD.MM.YYYY')); set serveroutput on declare trx trx_tab := trx_tab(); qty_so_far number; begin -- читаем поступления в массив (в порядке дат) for i in ( select t.item_id, t.quantity, t.cost from transactions t where t.cost is not null order by t.trx_date ) loop trx.extend; trx(trx.count) := trx_rec(i.item_id, i.quantity, i.cost); end loop; -- обработка выдач for i in ( select t.item_id, sum(t.quantity) quantity from transactions t where t.cost is null group by t.item_id ) loop qty_so_far := -i.quantity; for n in 1 .. trx.count loop if trx(n).item_id = i.item_id and trx(n).quantity > 0 then if trx(n).quantity >= qty_so_far then trx(n).quantity := trx(n).quantity - qty_so_far; exit; else qty_so_far := qty_so_far - trx(n).quantity; trx(n).quantity := 0; end if; end if; end loop; end loop; -- результат for i in ( select t.item_id, sum(t.cost * t.quantity) cost from table(trx) t group by t.item_id order by t.item_id ) loop dbms_output.put_line(i.item_id||': '||i.cost); end loop; end; /
Небольшая оптимизация состоит в том, чтобы списывать сразу общую сумму, а не возиться с каждым выбытием отдельно. Чтобы не засорять код сверх меры, я все-таки перепоручил заключительную часть — группировку сумм по позициям — SQL, обратившись к коллекции как к таблице (select from table()
).