Oracle
 sql >> Base de Dados >  >> RDS >> Oracle

Consulta SQL FIFO com grupo por

Opção 1


Este é provavelmente um trabalho para PL/SQL. Começando com os tipos de dados para saída:
CREATE TYPE supply_details_obj AS OBJECT(
  product_id  NUMBER,
  quantity    NUMBER,
  supplier_id NUMBER,
  customer_id NUMBER
);

CREATE TYPE supply_details_tab AS TABLE OF supply_details_obj;

Então podemos definir uma função pipeline para ler o INVENTORY_IN e INVENTORY_OUT tabelas uma linha de cada vez e mesclar as duas mantendo um total atual do estoque ou quantidade restante a ser fornecida:
CREATE FUNCTION assign_suppliers_to_customers (
  i_product_id IN INVENTORY_IN.PRODUCT_ID%TYPE
)
RETURN supply_details_tab PIPELINED
IS
  v_supplier_id  INVENTORY_IN.SUPPLIER_ID%TYPE;
  v_customer_id  INVENTORY_OUT.CUSTOMER_ID%TYPE;
  v_quantity_in  INVENTORY_IN.IN_QUANTITY%TYPE   := NULL;
  v_quantity_out INVENTORY_OUT.OUT_QUANTITY%TYPE := NULL;
  v_cur_in       SYS_REFCURSOR;
  v_cur_out      SYS_REFCURSOR;
BEGIN
  OPEN v_cur_in FOR
    SELECT in_quantity, supplier_id
    FROM   INVENTORY_IN
    WHERE  product_id = i_product_id
    ORDER BY inv_timestamp;

  OPEN v_cur_out FOR
    SELECT out_quantity, customer_id
    FROM   INVENTORY_OUT
    WHERE  product_id = i_product_id
    ORDER BY inv_timestamp;

  LOOP
    IF v_quantity_in IS NULL THEN
      FETCH v_cur_in INTO v_quantity_in, v_supplier_id;
      IF v_cur_in%NOTFOUND THEN
        v_supplier_id := NULL;
      END IF;
    END IF;
    IF v_quantity_out IS NULL THEN
      FETCH v_cur_out INTO v_quantity_out, v_customer_id;
      IF v_cur_out%NOTFOUND THEN
        v_customer_id := NULL;
      END IF;
    END IF;

    EXIT WHEN v_cur_in%NOTFOUND AND v_cur_out%NOTFOUND;

    IF v_quantity_in > v_quantity_out THEN
      PIPE ROW(
        supply_details_obj(
          i_product_id,
          v_quantity_out,
          v_supplier_id,
          v_customer_id
        )
      );
      v_quantity_in  := v_quantity_in - v_quantity_out;
      v_quantity_out := NULL;
    ELSE
      PIPE ROW(
        supply_details_obj(
          i_product_id,
          v_quantity_in,
          v_supplier_id,
          v_customer_id
        )
      );
      v_quantity_out := v_quantity_out - v_quantity_in;
      v_quantity_in  := NULL;
    END IF;
  END LOOP;
END;
/

Então, para os dados de amostra:
CREATE TABLE INVENTORY_IN ( ID, INV_TIMESTAMP, PRODUCT_ID, IN_QUANTITY, SUPPLIER_ID ) AS
SELECT 0, TIMESTAMP '2021-03-09 00:00:00', 101,  20, 0 FROM DUAL UNION ALL
SELECT 1, TIMESTAMP '2021-03-10 01:00:00', 101, 100, 4 FROM DUAL UNION ALL
SELECT 2, TIMESTAMP '2021-03-11 02:00:00', 101,  50, 3 FROM DUAL UNION ALL
SELECT 3, TIMESTAMP '2021-03-14 01:00:00', 101,  10, 2 FROM DUAL;

CREATE TABLE INVENTORY_OUT ( ID, INV_TIMESTAMP, PRODUCT_ID, OUT_QUANTITY, CUSTOMER_ID ) AS
SELECT 1, TIMESTAMP '2021-03-10 02:00:00', 101, 30, 1 FROM DUAL UNION ALL
SELECT 2, TIMESTAMP '2021-03-11 01:00:00', 101, 40, 2 FROM DUAL UNION ALL
SELECT 3, TIMESTAMP '2021-03-12 01:00:00', 101, 80, 1 FROM DUAL;

A pergunta:
SELECT product_id,
       supplier_id,
       customer_id,
       SUM( quantity ) AS quantity
FROM   TABLE( assign_suppliers_to_customers( 101 ) )
GROUP BY
       product_id,
       supplier_id,
       customer_id
ORDER BY
       MIN( inv_timestamp )

Saídas:

Opção 2


Uma consulta SQL (muito) complicada:
WITH in_totals ( ID, INV_TIMESTAMP, PRODUCT_ID, IN_QUANTITY, SUPPLIER_ID, TOTAL_QUANTITY ) AS (
  SELECT i.*,
         SUM( in_quantity ) OVER ( PARTITION BY product_id ORDER BY inv_timestamp )
  FROM   inventory_in i
),
out_totals ( ID, INV_TIMESTAMP, PRODUCT_ID, OUT_QUANTITY, CUSTOMER_ID, TOTAL_QUANTITY ) AS (
  SELECT o.*,
         SUM( out_quantity ) OVER ( PARTITION BY product_id ORDER BY inv_timestamp )
  FROM   inventory_out o
),
split_totals ( product_id, inv_timestamp, supplier_id, customer_id, quantity ) AS (
  SELECT i.product_id,
         MIN( COALESCE( LEAST( i.inv_timestamp, o.inv_timestamp ), i.inv_timestamp ) )
           AS inv_timestamp,
         i.supplier_id,
         o.customer_id,
         SUM(
           COALESCE(
             LEAST(
               i.total_quantity - o.total_quantity + o.out_quantity,
               o.total_quantity - i.total_quantity + i.in_quantity,
               i.in_quantity,
               o.out_quantity
             ),
             0
           )
         )
  FROM   in_totals i
         LEFT OUTER JOIN
         out_totals o
         ON (   i.product_id = o.product_id
            AND i.total_quantity - i.in_quantity <= o.total_quantity
            AND i.total_quantity >= o.total_quantity - o.out_quantity )
  GROUP BY
         i.product_id,
         i.supplier_id,
         o.customer_id
  ORDER BY
         inv_timestamp
),
missing_totals ( product_id, inv_timestamp, supplier_id, customer_id, quantity ) AS (
  SELECT i.product_id,
         i.inv_timestamp,
         i.supplier_id,
         NULL,
         i.in_quantity - COALESCE( s.quantity, 0 )
  FROM   inventory_in i
         INNER JOIN (
           SELECT product_id,
                  supplier_id,
                  SUM( quantity ) AS quantity
           FROM   split_totals
           GROUP BY product_id, supplier_id
         ) s
         ON (   i.product_id = s.product_id
            AND i.supplier_id = s.supplier_id )
  ORDER BY i.inv_timestamp
)
SELECT product_id, supplier_id, customer_id, quantity
FROM   (
  SELECT product_id, inv_timestamp, supplier_id, customer_id, quantity
  FROM   split_totals
  WHERE  quantity > 0
  UNION ALL
  SELECT product_id, inv_timestamp, supplier_id, customer_id, quantity
  FROM   missing_totals
  WHERE  quantity > 0
  ORDER BY inv_timestamp
);

Que, para os dados de exemplo acima, gera:

db<>fiddle aqui