Oracle SQL / PLSQL: иерархический рекурсивный запрос с повторяющимися данными - PullRequest
2 голосов
/ 27 марта 2019

У меня есть рекурсивная функция ниже, которая работает очень хорошо, но теперь я обнаружил, что некоторые данные не уникальны, и мне нужен способ их обработки.

FUNCTION calc_cost (model_no_         NUMBER,
                    revision_         NUMBER,
                    sequence_no_   IN NUMBER,
                    currency_      IN VARCHAR2)
    RETURN NUMBER
IS
    qty_    NUMBER := 0;
    cost_   NUMBER := 0;
BEGIN
    SELECT NVL (new_qty, qty), purch_cost
      INTO qty_, cost_
      FROM prod_conf_cost_struct_clv
     WHERE model_no = model_no_
       AND revision = revision_
       AND sequence_no = sequence_no_
       AND (purch_curr = currency_
         OR purch_curr IS NULL);

    IF cost_ IS NULL
    THEN
        SELECT SUM (calc_cost (model_no,
                               revision,
                               sequence_no,
                               purch_curr))
          INTO cost_
          FROM prod_conf_cost_struct_clv
         WHERE model_no = model_no_
           AND revision = revision_
           AND (purch_curr = currency_
             OR purch_curr IS NULL)
           AND part_no IN (SELECT component_part
                             FROM prod_conf_cost_struct_clv
                            WHERE model_no = model_no_
                              AND revision = revision_
                              AND sequence_no = sequence_no_);
    END IF;

    RETURN qty_ * cost_;
EXCEPTION
    WHEN NO_DATA_FOUND
    THEN
        RETURN 0;
END calc_cost;

Следующий критерий, где этоСбой функции ...part_no in (select component_part....

Пример данных:

rownum., model_no, revision, sequence_no, part_no, component_part, level, cost, purch_curr, qty

 1. 62, 1, 00, XXX, ABC, 1, null, null, 1
 2. 62, 1, 10, ABC, 123, 2, null, null, 1
 3. 62, 1, 20, 123, DEF, 3, null, null, 1
 4. 62, 1, 30, DEF, 456, 4, 100, GBP, 1
 5. 62, 1, 40, DEF, 789, 4, 50, GBP, 1
 6. 62, 1, 50, DEF, 024, 4, 20, GBP, 1
 7. 62, 1, 60, ABC, 356, 2, null, null, 2
 8. 62, 1, 70, 356, DEF, 3, null, null, 3
 9. 62, 1, 80, DEF, 456, 4, 100, GBP, 1
 10. 62, 1, 90, DEF, 789, 4, 50, EUR, 1
 11. 62, 1, 100, DEF, 024, 4, 20, GBP, 1

Если бы я должен был передать следующие значения в параметры функции: model_no, revision, sequence_no (игнорировать валюту, поскольку это не такотношение к вопросу):

62, 1, 20

Я хочу, чтобы он суммировал строки ТОЛЬКО 4-6 = 170, однако он суммирует строки 4-6 И 9-11 = 340.

В конечном итогеэта функция будет использоваться в запросе SQL ниже:

    SELECT LEVEL,
           SYS_CONNECT_BY_PATH (sequence_no, '->') PATH,
           calc_cost (model_no,
                      revision,
                      sequence_no,
                      'GBP')
               total_gbp
      FROM prod_conf_cost_struct_clv
     WHERE model_no = 62
       AND revision = 1
CONNECT BY PRIOR component_part = part_no
       AND PRIOR model_no = 62
       AND PRIOR revision = 1
START WITH sequence_no = 20
  ORDER BY sequence_no

Как вы можете видеть, это также может привести к проблеме component_part = part_no.

UPDATE

В дополнение к предоставленным ответам я подумал, что расширю исходный вопрос, чтобы рассмотреть также валюту и кол-во элементов.Я обновил пример данных, включив в него валюту и кол-во.

Если бы я должен был передать следующие значения в параметры функции: model_no, revision, sequence_no, currency:

Input: 62, 1, 70, EUR 
Expected Cost Output: 150

Input: 62, 1, 60, EUR 
Expected Cost Output: 300

Input: 62, 1, 60, GBP
Expected Cost Output: 720

Любая помощьбудет очень признателен.

Спасибо заранее.

Ответы [ 3 ]

2 голосов
/ 03 мая 2019

Примечание: если у вас возникают проблемы с запуском MATCH_RECOGNIZE, это может быть связано с тем, что вы используете (не слишком) старую версию SQL * Developer. Попробуйте последнюю версию или используйте взамен SQL * Navigator, TOAD или SQL * Plus. Проблема в "?" символ, который сбивает с толку SQL * Developer, поскольку это символ, который JDBC использует для переменных связывания.

У вас проблема с моделью данных. А именно, дочерние записи в вашей таблице prod_conf_cost_struct_cvl не явно связаны с их родительскими строками. Вот почему сборка «DEF» вызывает проблемы. Без явной связи невозможно точно рассчитать данные.

Вы должны исправить эту модель данных и добавить parent_sequence_no к каждой записи, чтобы (например) вы могли сказать, что sequence_no 80 - это дочерний элемент sequence_no 70, а не дочерний элемент sequence_no 20.

Однако, поскольку я не могу предположить, что у вас есть время или полномочия для изменения вашей модели данных, я отвечу на вопрос с моделью данных как есть.

Прежде всего, давайте добавим QTY и PURCH_CURR к вашим образцам данных.

with prod_conf_cost_struct_clv ( model_no, revision, sequence_no, part_no, component_part, lvl, cost, qty, purch_curr ) as
( 
SELECT 62, 1, 00, 'XXX', 'ABC', 1, null, 1, 'GBP' FROM DUAL UNION ALL
SELECT 62, 1, 10, 'ABC', '123', 2, null, 1, 'GBP' FROM DUAL UNION ALL
SELECT 62, 1, 20, '123', 'DEF', 3, null, 1, 'GBP' FROM DUAL UNION ALL
SELECT 62, 1, 30, 'DEF', '456', 4, 100, 1, 'GBP' FROM DUAL UNION ALL
SELECT 62, 1, 40, 'DEF', '789', 4, 50, 1, 'GBP' FROM DUAL UNION ALL
SELECT 62, 1, 50, 'DEF', '024', 4, 20, 1, 'GBP' FROM DUAL UNION ALL
SELECT 62, 1, 60, 'ABC', '356', 2, null, 1, 'GBP' FROM DUAL UNION ALL
SELECT 62, 1, 70, '356', 'DEF', 3, null, 1, 'GBP' FROM DUAL UNION ALL
SELECT 62, 1, 80, 'DEF', '456', 4, 100, 1, 'GBP' FROM DUAL UNION ALL
SELECT 62, 1, 90, 'DEF', '789', 4, 50, 1, 'GBP' FROM DUAL UNION ALL
SELECT 62, 1, 100, 'DEF', '024', 4, 20, 1, 'GBP' FROM DUAL )
select * from prod_conf_cost_struct_clv;
+----------+----------+-------------+---------+----------------+-----+------+-----+------------+
| MODEL_NO | REVISION | SEQUENCE_NO | PART_NO | COMPONENT_PART | LVL | COST | QTY | PURCH_CURR |
+----------+----------+-------------+---------+----------------+-----+------+-----+------------+
|       62 |        1 |           0 | XXX     | ABC            |   1 |      |   1 | GBP        |
|       62 |        1 |          10 | ABC     | 123            |   2 |      |   1 | GBP        |
|       62 |        1 |          20 | 123     | DEF            |   3 |      |   1 | GBP        |
|       62 |        1 |          30 | DEF     | 456            |   4 |  100 |   1 | GBP        |
|       62 |        1 |          40 | DEF     | 789            |   4 |   50 |   1 | GBP        |
|       62 |        1 |          50 | DEF     | 024            |   4 |   20 |   1 | GBP        |
|       62 |        1 |          60 | ABC     | 356            |   2 |      |   1 | GBP        |
|       62 |        1 |          70 | 356     | DEF            |   3 |      |   1 | GBP        |
|       62 |        1 |          80 | DEF     | 456            |   4 |  100 |   1 | GBP        |
|       62 |        1 |          90 | DEF     | 789            |   4 |   50 |   1 | GBP        |
|       62 |        1 |         100 | DEF     | 024            |   4 |   20 |   1 | GBP        |
+----------+----------+-------------+---------+----------------+-----+------+-----+------------+

ПРИМЕЧАНИЕ: вы не показываете, как несколько валют будут представлены в ваших тестовых данных, поэтому моя обработка этого вопроса в этом ответе может быть неправильной.

ОК, поэтому первое, что нам нужно сделать, это выяснить значение parent_sequence_no (которое действительно должно быть в вашей таблице - см. Выше). Поскольку его нет в вашей таблице, нам нужно его вычислить. Мы вычислим его как sequence_no строки, имеющей наибольшее значение sequence_no, которое меньше текущей строки, и значение level (которое я назвал lvl, чтобы избежать использования ключевого слова Oracle) на единицу меньше текущая строка.

Чтобы найти это значение эффективно, мы можем использовать функциональность MATCH_RECOGNIZE, чтобы описать, как должна выглядеть родительская строка для каждого дочернего элемента.

Мы назовем набор результатов с этим новым parent_sequence_no столбцом corrected_hierarchy.

with prod_conf_cost_struct_clv ( model_no, revision, sequence_no, part_no, component_part, lvl, cost, qty, purch_curr ) as
( 
SELECT 62, 1, 00, 'XXX', 'ABC', 1, null, 1, 'GBP' FROM DUAL UNION ALL
SELECT 62, 1, 10, 'ABC', '123', 2, null, 1, 'GBP' FROM DUAL UNION ALL
SELECT 62, 1, 20, '123', 'DEF', 3, null, 1, 'GBP' FROM DUAL UNION ALL
SELECT 62, 1, 30, 'DEF', '456', 4, 100, 1, 'GBP' FROM DUAL UNION ALL
SELECT 62, 1, 40, 'DEF', '789', 4, 50, 1, 'GBP' FROM DUAL UNION ALL
SELECT 62, 1, 50, 'DEF', '024', 4, 20, 1, 'GBP' FROM DUAL UNION ALL
SELECT 62, 1, 60, 'ABC', '356', 2, null, 1, 'GBP' FROM DUAL UNION ALL
SELECT 62, 1, 70, '356', 'DEF', 3, null, 1, 'GBP' FROM DUAL UNION ALL
SELECT 62, 1, 80, 'DEF', '456', 4, 100, 1, 'GBP' FROM DUAL UNION ALL
SELECT 62, 1, 90, 'DEF', '789', 4, 50, 1, 'GBP' FROM DUAL UNION ALL
SELECT 62, 1, 100, 'DEF', '024', 4, 20, 1, 'GBP' FROM DUAL )
-- Step 1: correct for your data model problem, which is the fact that child rows
-- (e.g., operations 30-50) are not *explicitly* linked to their parent rows (e.g.,
-- operation 20)
, corrected_hierarchy ( model_no, revision, parent_sequence_no, sequence_no, part_no, component_part, lvl, cost, qty, purch_curr ) AS
(
SELECT *
FROM   prod_conf_cost_struct_clv c
MATCH_RECOGNIZE (
  PARTITION BY model_no, revision
  ORDER BY sequence_no desc
  MEASURES (P.sequence_no) AS parent_sequence_no,
           c.sequence_no AS sequence_no, c.part_no as part_no, c.component_part as component_part, c.lvl as lvl, c.cost as cost, c.qty as qty, c.purch_curr as purch_curr
  ONE ROW PER MATCH
  AFTER MATCH SKIP TO NEXT ROW
  -- C => child row
  -- S* => zero or more siblings or children of siblings that might be 
  --           between child and its parent
  -- P? => parent row, which may not exist (e.g., for the root operation)
  PATTERN (C S* P?)
  DEFINE
    C AS 1=1,
    S AS S.lvl >= C.lvl,
    P AS P.lvl = C.lvl - 1 AND P.component_part = C.part_no
)
ORDER BY model_no, revision, sequence_no )
SELECT * FROM corrected_hierarchy;
+----------+----------+--------------------+-------------+---------+----------------+-----+------+-----+------------+
| MODEL_NO | REVISION | PARENT_SEQUENCE_NO | SEQUENCE_NO | PART_NO | COMPONENT_PART | LVL | COST | QTY | PURCH_CURR |
+----------+----------+--------------------+-------------+---------+----------------+-----+------+-----+------------+
|       62 |        1 |                    |           0 | XXX     | ABC            |   1 |      |   1 | GBP        |
|       62 |        1 |                  0 |          10 | ABC     | 123            |   2 |      |   1 | GBP        |
|       62 |        1 |                 10 |          20 | 123     | DEF            |   3 |      |   1 | GBP        |
|       62 |        1 |                 20 |          30 | DEF     | 456            |   4 |  100 |   1 | GBP        |
|       62 |        1 |                 20 |          40 | DEF     | 789            |   4 |   50 |   1 | GBP        |
|       62 |        1 |                 20 |          50 | DEF     | 024            |   4 |   20 |   1 | GBP        |
|       62 |        1 |                  0 |          60 | ABC     | 356            |   2 |      |   1 | GBP        |
|       62 |        1 |                 60 |          70 | 356     | DEF            |   3 |      |   1 | GBP        |
|       62 |        1 |                 70 |          80 | DEF     | 456            |   4 |  100 |   1 | GBP        |
|       62 |        1 |                 70 |          90 | DEF     | 789            |   4 |   50 |   1 | GBP        |
|       62 |        1 |                 70 |         100 | DEF     | 024            |   4 |   20 |   1 | GBP        |
+----------+----------+--------------------+-------------+---------+----------------+-----+------+-----+------------+

Теперь вы можете остановиться прямо здесь, если хотите. Все, что вам нужно сделать, это использовать логику corrected_hierarchy в вашей функции calc_cost, заменив

    and part_no in (
      select component_part
      ...

с

    and parent_sequence_no = sequence_no_

Но, как указал @Def, вам действительно не нужна функция PL / SQL для того, что вы пытаетесь сделать.

То, что вы, похоже, пытаетесь сделать, - это распечатать иерархическую ведомость материалов, в которой стоимость уровня каждого элемента (стоимость уровня равна стоимости прямого и косвенного подкомпонентов элемента).

Вот запрос, который делает это, собирая все вместе:

with prod_conf_cost_struct_clv ( model_no, revision, sequence_no, part_no, component_part, lvl, cost, qty, purch_curr ) as
( 
SELECT 62, 1, 00, 'XXX', 'ABC', 1, null, 1, 'GBP' FROM DUAL UNION ALL
SELECT 62, 1, 10, 'ABC', '123', 2, null, 1, 'GBP' FROM DUAL UNION ALL
SELECT 62, 1, 20, '123', 'DEF', 3, null, 1, 'GBP' FROM DUAL UNION ALL
SELECT 62, 1, 30, 'DEF', '456', 4, 100, 1, 'GBP' FROM DUAL UNION ALL
SELECT 62, 1, 40, 'DEF', '789', 4, 50, 1, 'GBP' FROM DUAL UNION ALL
SELECT 62, 1, 50, 'DEF', '024', 4, 20, 1, 'GBP' FROM DUAL UNION ALL
SELECT 62, 1, 60, 'ABC', '356', 2, null, 1, 'GBP' FROM DUAL UNION ALL
SELECT 62, 1, 70, '356', 'DEF', 3, null, 1, 'GBP' FROM DUAL UNION ALL
SELECT 62, 1, 80, 'DEF', '456', 4, 100, 1, 'GBP' FROM DUAL UNION ALL
SELECT 62, 1, 90, 'DEF', '789', 4, 50, 1, 'GBP' FROM DUAL UNION ALL
SELECT 62, 1, 100, 'DEF', '024', 4, 20, 1, 'GBP' FROM DUAL )
-- Step 1: correct for your data model problem, which is the fact that child rows
-- (e.g., operations 30-50) are not *explicitly* linked to their parent rows (e.g.,
-- operation 20)
, corrected_hierarchy ( model_no, revision, parent_sequence_no, sequence_no, part_no, component_part, lvl, cost, qty, purch_curr ) AS
(
SELECT *
FROM   prod_conf_cost_struct_clv c
MATCH_RECOGNIZE (
  PARTITION BY model_no, revision
  ORDER BY sequence_no desc
  MEASURES (P.sequence_no) AS parent_sequence_no,
           c.sequence_no AS sequence_no, c.part_no as part_no, c.component_part as component_part, c.lvl as lvl, c.cost as cost, c.qty as qty, c.purch_curr as purch_curr
  ONE ROW PER MATCH
  AFTER MATCH SKIP TO NEXT ROW
  PATTERN (C S* P?)
  DEFINE
    C AS 1=1,
    S AS S.lvl >= C.lvl,
    P AS P.lvl = C.lvl - 1 AND P.component_part = C.part_no
)
ORDER BY model_no, revision, sequence_no ),
sequence_hierarchy_costs as (
SELECT model_no,
       revision,
       min(sequence_no) sequence_no,
       purch_curr,
       sum(h.qty * h.cost) hierarchy_cost
FROM corrected_hierarchy h
WHERE 1=1
connect by model_no = prior model_no
and        revision = prior revision
and        parent_sequence_no = prior sequence_no
group by model_no, revision, connect_by_root sequence_no, purch_curr )
SELECT level,
       sys_connect_by_path(h.sequence_no, '->') path,
       shc.hierarchy_cost
FROM corrected_hierarchy h 
INNER JOIN sequence_hierarchy_costs shc ON shc.model_no = h.model_no and shc.revision = h.revision and shc.sequence_no = h.sequence_no and shc.purch_curr = h.purch_curr
WHERE h.model_no = 62
and   h.revision = 1
START WITH h.sequence_no = 20
connect by h.model_no = prior h.model_no
and        h.revision = prior h.revision
and        h.parent_sequence_no = prior h.sequence_no;
+-------+----------+----------------+
| LEVEL |   PATH   | HIERARCHY_COST |
+-------+----------+----------------+
|     1 | ->20     |            170 |
|     2 | ->20->30 |            100 |
|     2 | ->20->40 |             50 |
|     2 | ->20->50 |             20 |
+-------+----------+----------------+

Вы можете видеть, что было бы намного проще, если бы parent_sequence_no были в вашей модели данных для начала.

1 голос
/ 05 мая 2019

Предполагая, что столбец sequence_no строго следует первому глубокому обходу дерева, отсутствующее дочернее / родительское отношение может быть восстановлено двумя способами. Сначала мы можем найти родителя sequence_no для каждого ребенка или найти открытый интервал sequence_no для детей родителя. Использование данных, представленных в OP (без столбца валюты)

with prod_conf_cost_struct_clv (model_no, revision, sequence_no, part_no, component_part, lvl, cost) as
( 
SELECT 62, 1, 00, 'XXX', 'ABC', 1, null FROM DUAL UNION ALL
SELECT 62, 1, 10, 'ABC', '123', 2, null FROM DUAL UNION ALL
SELECT 62, 1, 20, '123', 'DEF', 3, null FROM DUAL UNION ALL
SELECT 62, 1, 30, 'DEF', '456', 4, 100  FROM DUAL UNION ALL
SELECT 62, 1, 40, 'DEF', '789', 4, 50 FROM DUAL UNION ALL
SELECT 62, 1, 50, 'DEF', '024', 4, 20 FROM DUAL UNION ALL
SELECT 62, 1, 60, 'ABC', '356', 2, null FROM DUAL UNION ALL
SELECT 62, 1, 70, '356', 'DEF', 3, null FROM DUAL UNION ALL
SELECT 62, 1, 80, 'DEF', '456', 4, 100 FROM DUAL UNION ALL
SELECT 62, 1, 90, 'DEF', '789', 4, 50 FROM DUAL UNION ALL
SELECT 62, 1, 100, 'DEF', '024', 4, 20 FROM DUAL )
, hier as(
SELECT  model_no, revision, sequence_no, part_no, component_part, lvl, cost
 , (SELECT nvl(min(b.sequence_no), 2147483647/*max integer*/) 
    FROM prod_conf_cost_struct_clv b 
    WHERE a.lvl <> b.lvl-1
    AND a.sequence_no < b.sequence_no) child_bound_s_n
 , (SELECT max(b.sequence_no) 
    FROM prod_conf_cost_struct_clv b 
    WHERE a.lvl = b.lvl+1
    AND a.sequence_no > b.sequence_no) parent_s_n
FROM prod_conf_cost_struct_clv a
)
SELECT model_no, revision, sequence_no,parent_s_n,child_bound_s_n, part_no, component_part, lvl, cost
FROM hier;

Дети ряда, скажем, SEQUENCE_NO = 20 находятся в (SEQUENCE_NO, CHILD_BOUND_S_N) открытом интервале (20, 60).

MODEL_NO REVISION SEQUENCE_NO   PARENT_S_N  CHILD_BOUND_S_N PART_NO COMPONENT_PART  LVL COST
62       1            0                             20      XXX     ABC             1   
62       1           10          0                  30      ABC     123             2   
62       1           20         10                  60      123     DEF             3   
62       1           30         20                  40      DEF     456             4   100
62       1           40         20                  50      DEF     789             4    50
62       1           50         20                  60      DEF     024             4    20
62       1           60          0                  80      ABC     356             2   
62       1           70         60          2147483647      356     DEF             3   
62       1           80         70                  90      DEF     456             4   100
62       1           90         70                 100      DEF     789             4    50
62       1          100         70          2147483647      DEF     024             4    20

Чтобы минимизировать изменения в исходной функции calc_cost, здесь лучше подходит второй способ. Итак, опять без данных валюты

CREATE FUNCTION calc_cost(
    model_no_ number, 
    revision_ number, 
    sequence_no_ in number
    --, currency_ in varchar2
  ) return number 
  is
    qty_ number := 0;
    cost_ number := 0;
    lvl_ number := 0;
  begin

    select 1 /*nvl(new_qty, qty)*/, cost, lvl
      into qty_, cost_, lvl_
    from prod_conf_cost_struct_clv
    where model_no = model_no_
      and revision = revision_
      and sequence_no = sequence_no_
      --and (purch_curr = currency_ or purch_curr is null)
      ;

    if cost_ is null then 
      select sum(calc_cost(model_no, revision, sequence_no/*, purch_curr*/)) into cost_ 
      from prod_conf_cost_struct_clv 
      where model_no = model_no_
        and revision = revision_
        --and (purch_curr = currency_ or purch_curr is null)
        and sequence_no > sequence_no_  
        and sequence_no < (SELECT nvl(min(b.sequence_no), 2147483647) 
                      FROM prod_conf_cost_struct_clv b 
                      WHERE lvl_ <> b.lvl-1
                      AND sequence_no_ < b.sequence_no);
    end if;
    return qty_ * cost_;
  exception when no_data_found then 
    return 0;
  end calc_cost;

и применение к данным выше

SELECT calc_cost(62,1,20) FROM DUAL;

CALC_COST(62,1,20)
170

Использование в запросе иерархии

with hier as(
 SELECT  model_no, revision, sequence_no, part_no, component_part, lvl, cost
   ,(SELECT nvl(min(b.sequence_no), 2147483647) 
     FROM prod_conf_cost_struct_clv b 
     WHERE a.lvl <> b.lvl-1
     AND a.sequence_no < b.sequence_no) child_bound_s_n
 FROM prod_conf_cost_struct_clv a
)
select level, sys_connect_by_path(sequence_no, '->') path, 
     calc_cost(model_no, revision, sequence_no) total_gbp
from hier
where model_no = 62
  and revision = 1
connect by sequence_no > prior sequence_no 
   and sequence_no < prior child_bound_s_n
  and prior model_no = 62
  and prior revision = 1
start with sequence_no = 20
order by sequence_no;

LEVEL   PATH    TOTAL_GBP
1   ->20        170
2   ->20->30    100
2   ->20->40    50
2   ->20->50    20
0 голосов
/ 29 марта 2019

Вам действительно нужна функция?Кажется, что вы на самом деле ищете вычисление детали и каждого из ее компонентов (и рекурсивно их компонентов).Попробуйте это:

SELECT sub.root_part, sum(price) AS TOTAL_PRICE
FROM (SELECT CONNECT_BY_ROOT t.part_no AS ROOT_PART, price
      FROM (SELECT DISTINCT model_no, revision, part_no, component_part, price
            FROM prod_conf_cost_struct_clv
            WHERE model_no = 62
            AND revision = 1 )t
      CONNECT BY PRIOR component_part = part_no
      --START WITH part_no = '123'
      ) sub
GROUP BY sub.root_part;

Я закомментировал START WITH, но вы можете вставить его обратно, если вы действительно ищете только этот один идентификатор.

...