2

I am currently working on a requirement that requires a hierarchical query that I cannot seem to get right.

The requirement is: For a given set of orders, find out all of their requirements, and what is replenishing those requirements. Then, in the case that a replenishment is a MAKE type (i.e., another order), find out all of its requirements and replenishments, etc etc.

Here is a dbfiddle containing all of the data and some example queries.

The result query at the end of the fiddle is essentially saying: For order x, here are all of its requirements. For each of those requirements, here is what is scheduled to replenish it.

What I need to do now, is for all make type replenishments, I need to essentially continue to repeat this process by joining to those tables, extracting what is replenishing those replenishments, so on and so forth, but do so while keeping track of the top level orders.

I would hope to turn it into a dataset that looks like this:

| Root Order | Order_Number | Requirement_ID | Replenishment_ID | Replenishment_Type | Replenishment_Detail | Replenishment_Date |
|:----------:|:------------:|:--------------:|:----------------:|:------------------:|:--------------------:|:------------------:|
|     300    |      300     |     AA-300     |       RO601      |       Bought       |          963         |      7/15/2018     |
|     300    |      300     |     AA-300     |       RO111      |        Make        |          251         |     10/23/2018     |
|     300    |      300     |     AA-300     |       RO435      |        Make        |          837         |      3/4/2018      |
|     300    |      300     |     AA-300     |       RO608      |        Make        |          850         |      4/27/2018     |
|     300    |      300     |     AA-516     |       RO734      |        Make        |          415         |      5/5/2018      |
|     300    |      300     |     AA-516     |       RO245      |       Bought       |          130         |      2/6/2018      |
|     300    |      300     |     AA-516     |       RO754      |       Bought       |          874         |      6/9/2018      |
|     300    |      300     |     AA-468     |       RO120      |        Make        |          333         |      7/28/2018     |
|     300    |      300     |     AA-468     |       RO96       |       Bought       |          279         |      6/11/2018     |
|     300    |      300     |     AA-744     |       RO576      |        Make        |          452         |      6/9/2018      |
|     300    |      300     |     AA-744     |       RO592      |       Bought       |          967         |      1/16/2018     |
|     300    |      300     |     AA-744     |       RO104      |        Make        |          232         |      1/30/2019     |
|     300    |      300     |     AA-744     |       RO169      |        Make        |          804         |      2/2/2018      |
|     300    |      130     |     AA-785     |       RO573      |        Make        |          616         |      4/1/2018      |
|     300    |      130     |     AA-785     |       RO139      |        Make        |          698         |      7/16/2018     |
|     300    |      130     |     AA-785     |       RO252      |        Make        |          190         |      8/2/2018      |
|     300    |      130     |     AA-785     |       RO561      |        Make        |          453         |      5/13/2018     |
|     300    |      130     |     AA-785     |       RO775      |        Make        |          974         |      8/7/2018      |
|     300    |      130     |     AA-171     |       RO92       |       Bought       |          493         |      4/1/2018      |
|     300    |      493     |     AA-400     |        RO4       |        Make        |          591         |      4/17/2018     |
|     300    |      493     |     AA-401     |       NULL       |        NULL        |         NULL         |        NULL        |
|     Now    |   Starting   |      From      |        The       |        Other       |                      |       Tables       |
|     300    |      591     |     AA-999     |        RO1       |       Bought       |          111         |      4/19/2019     |
|     300    |      591     |     AA-111     |        RO2       |       Bought       |          123         |      4/1/2019      |
|     300    |      591     |     AA-001     |       RO400      |        Make        |          124         |      5/1/2019      |
|     300    |      124     |     AA-313     |       RO112      |       Bought       |          102         |      7/8/2019      |
|     etc    |      etc     |       etc      |        etc       |         etc        |          etc         |         etc        |

Where you can see Order 300 had a replenishment of 130, which then had a replenishment of 493.

How can I use CONNECT_BY_ROOT and CONNECT BY PRIOR to achieve this? I have tried recursive WITH like below, but that does not produce a hierarchy.

WITH 
  rec(Root_Order, Order_Number, Requirement_ID, Replenishment_ID, Replenishment_Type, Replenishment_Detail, Replenishment_Date) AS (
  SELECT
    Orders.Order_Number AS Root_Order,
    Orders.Order_Number,
    Requirements.Requirement_ID,
    Replenishments.Replenishment_ID,
    Replenishments.Replenishment_Type,
    Replenishments.Replenishment_Detail,
    Replenishments.Replenishment_Date

  FROM
    Orders
      LEFT JOIN Requirements ON Orders.Order_Number = Requirements.Order_Number
      LEFT JOIN Lookup ON Requirements.Requirement_ID = Lookup.Requirement_ID
      LEFT JOIN Replenishments ON Lookup.Replenishment_ID = Replenishments.Replenishment_ID

UNION ALL

  SELECT
    rec.Order_Number
    rec.Replenishment_Details,
    Requirements.Requirement_ID,
    Replenishments.Replenishment_ID,
    Replenishments.Replenishment_Type,
    Replenishments.Replenishment_Detail,
    Replenishments.Replenishment_Date

  FROM
    rec
      LEFT JOIN Requirements ON Orders.Order_Number = Requirements.Order_Number
      LEFT JOIN Lookup ON Requirements.Requirement_ID = Lookup.Requirement_ID
      LEFT JOIN Replenishments ON Lookup.Replenishment_ID = Replenishments.Replenishment_ID
  )

  CYCLE Root_Order, Order_Number, Requirement_ID, Replenishment_ID, Replenishment_Type, Replenishment_Detail, Replenishment_Date SET CYCLE TO 1 DEFAULT 0

  SELECT DISTINCT * FROM rec

Thank you

10
  • It would probably be better to show the starting data as the four base tables, to make the relationships and hierarchy clearer. (Maybe in a db<>fiddle? And simplified further?) I'm struggling a bit to see how your starting data and result are related. Your recursive CTE doesn't seem to make sense anyway though, the two branches have different projections, the anchor branch has no filter, and the recursive branch doesn't refer to rec in the join conditions (or where clause, which is doesn't have either). Commented Sep 5, 2019 at 13:46
  • My worry was making too long of a question, as I've been scolded for that before, by showing data from each table. I have a basic ER model, which I have edited the question with. I could try producing a db<fiddle> or sql fiddle. Let me add a bit more detail, to see if it helps clear things up. Additionally, I am by no means implying the recursive with works...I am lost, trying to tackle this problem. Commented Sep 5, 2019 at 13:48
  • .. and rec.Details is not defined. We can only guess ... Commented Sep 5, 2019 at 13:52
  • Yes, but we would have to deconstruct the result of your query to get the base data, so we can then create a recursive query that works from that base data... The ER probably isn't helpful as the query shows the relationships already. Commented Sep 5, 2019 at 13:52
  • Fixed @Serg. As mentioned, I am trying to spoof a lot of this. Sorry for confusion. Commented Sep 5, 2019 at 13:54

2 Answers 2

1

I think you're looking for something like:

with rec(root_order, order_number, requirement_id, replenishment_id, replenishment_type,
    replenishment_detail, replenishment_date)
as (
  -- anchor member
  select
    orders.order_number as root_order,
    orders.order_number,
    requirements.requirement_id,
    replenishments.replenishment_id,
    replenishments.replenishment_type,
    replenishments.replenishment_detail,
    replenishments.replenishment_date
  from orders
  join requirements on orders.order_number = requirements.order_number
  left join lookup on requirements.requirement_id = lookup.requirement_id
  left join replenishments on lookup.replenishment_id = replenishments.replenishment_id
  union all
  -- recursive member
  select rec.root_order,
    requirements.order_number,
    requirements.requirement_id,
    replenishments.replenishment_id,
    replenishments.replenishment_type,
    replenishments.replenishment_detail,
    replenishments.replenishment_date
  from rec
  join requirements on rec.replenishment_detail = requirements.order_number
  left join lookup on requirements.requirement_id = lookup.requirement_id
  left join replenishments on lookup.replenishment_id = replenishments.replenishment_id
)
select *
from rec
order by root_order, order_number, requirement_id;

The anchor member is essentially your original query, except it add the root_order and I made the first join an inner one to reduce the noise a bit (a lot of the 87 rows from your original only have order_number with everything else null).

The recursive member then joins the rec.replenishment_detail (child order number) to requirements.order_number to walk down the hierarchy. It doesn't need to refer to the actual orders table again (unless you actually want some other field from that, in which case it's trivial to include it).

With your sample data that produces 65 rows of output, including:

ROOT_ORDER ORDER_NUMBER REQUIR REPLE REPLEN REPLENISHMENT_DETAIL REPLENISHM
---------- ------------ ------ ----- ------ -------------------- ----------
...
       300          130 AA-171 RO532 Make                    727 2018-05-17
       300          130 AA-171 RO92  Bought                  493 2018-04-01
       300          130 AA-785 RO573 Make                    616 2018-04-01
       300          130 AA-785 RO561 Make                    453 2018-05-13
       300          130 AA-785 RO775 Make                    974 2018-08-07
       300          130 AA-785 RO139 Make                    698 2018-07-16
       300          130 AA-785 RO252 Make                    190 2018-08-02
       300          300 AA-300 RO601 Bought                  963 2018-07-15
       300          300 AA-300 RO111 Make                    251 2018-10-23
       300          300 AA-300 RO435 Make                    837 2018-03-04
       300          300 AA-300 RO608 Make                    850 2018-04-27
       300          300 AA-468 RO96  Bought                  279 2018-06-11
       300          300 AA-468 RO120 Make                    333 2018-07-28
       300          300 AA-516 RO754 Bought                  874 2018-06-09
       300          300 AA-516 RO245 Bought                  130 2018-02-06
       300          300 AA-516 RO734 Make                    415 2018-05-05
       300          300 AA-744 RO169 Make                    804 2018-02-02
       300          300 AA-744 RO576 Make                    452 2018-06-09
       300          300 AA-744 RO592 Bought                  967 2018-01-16
       300          300 AA-744 RO104 Make                    232 2019-01-30
       300          493 AA-400 RO4   Bought                  591 2018-04-17
       300          493 AA-401                                             
...

db<>fiddle based on your original.

Notice though that it also includes the 'child' orders independently:

...
       130          130 AA-171 RO92  Bought                  493 2018-04-01
       130          130 AA-171 RO532 Make                    727 2018-05-17
       130          130 AA-785 RO775 Make                    974 2018-08-07
       130          130 AA-785 RO561 Make                    453 2018-05-13
       130          130 AA-785 RO252 Make                    190 2018-08-02
       130          130 AA-785 RO573 Make                    616 2018-04-01
       130          130 AA-785 RO139 Make                    698 2018-07-16
       130          493 AA-400 RO4   Bought                  591 2018-04-17
       130          493 AA-401                                             
...
       493          493 AA-400 RO4   Bought                  591 2018-04-17
       493          493 AA-401                                             
...

etc. You could start from a specific target order (i.e. have the anchor member have where orders.order_number = 300) but it isn't clear if that's what you want. If it isn't, and you don't want to see the lower orders on their own too, then you need a way to identify a top-level order. One way to do that might be to exclude any orders which appear as any replenishment_detail value, by adding a not exists(...) filter:

with rec(root_order, order_number, requirement_id, replenishment_id, replenishment_type,
    replenishment_detail, replenishment_date)
as (
  -- anchor member
  select
    orders.order_number as root_order,
    orders.order_number,
    requirements.requirement_id,
    replenishments.replenishment_id,
    replenishments.replenishment_type,
    replenishments.replenishment_detail,
    replenishments.replenishment_date
  from orders
  join requirements on orders.order_number = requirements.order_number
  left join lookup on requirements.requirement_id = lookup.requirement_id
  left join replenishments on lookup.replenishment_id = replenishments.replenishment_id
  where not exists (
    select *
    from replenishments
    where replenishment_detail = orders.order_number
  )
  union all
  -- recursive member
  select rec.root_order,
    requirements.order_number,
    requirements.requirement_id,
    replenishments.replenishment_id,
    replenishments.replenishment_type,
    replenishments.replenishment_detail,
    replenishments.replenishment_date
  from rec
  join requirements on rec.replenishment_detail = requirements.order_number
  left join lookup on requirements.requirement_id = lookup.requirement_id
  left join replenishments on lookup.replenishment_id = replenishments.replenishment_id
)
select *
from rec
order by root_order, order_number, requirement_id;

which now only gets 54 rows and excludes the 130/493/etc. root-order rows above.

db<>fiddle


Since you actually asked about a hierarchical query rather than a recursive one, here's how you could do that:

with cte (order_number, requirement_id, replenishment_id, replenishment_type,
    replenishment_detail, replenishment_date, is_root_order)
as (
  select
    orders.order_number,
    requirements.requirement_id,
    replenishments.replenishment_id,
    replenishments.replenishment_type,
    replenishments.replenishment_detail,
    replenishments.replenishment_date,
    case when exists (
      select *
      from replenishments
      where replenishment_detail = orders.order_number
    ) then 'N' else 'Y' end
  from orders
  join requirements on orders.order_number = requirements.order_number
  left join lookup on requirements.requirement_id = lookup.requirement_id
  left join replenishments on lookup.replenishment_id = replenishments.replenishment_id
)
select connect_by_root(order_number) as root_order,
  order_number, requirement_id, replenishment_id, replenishment_type,
  replenishment_detail, replenishment_date
from cte
start with is_root_order = 'Y'
connect by order_number = prior replenishment_detail;

The CTE is again pretty much your original query, with a case expression and exists clause to decide if each order is a 'root' one, as before - but now as a flag rather than a filter. The hierarchical query is then fairly straightforward, using that flag in its starts with clause.

Yet another db<>fiddle.

(I've just realised that this is pretty much what @StewAshton said to do; my CTE is essentially his 'substitute your joins' step. The only other real difference is that he's moved the flag calculation directly into the starts with clause, which might actually be slightly more efficient as it doesn't have to hit the replenishments table again...)

I generally prefer the recursive CTE approach, but the hierarchical one is attractive here just for its brevity. You might want to compare the performance of both approaches against your real data though.

Sign up to request clarification or add additional context in comments.

7 Comments

This might be outside the scope of the original question, but I'd like to ask for your assistance because, this looks great. The definition of a "top-level" order would be some subset of the Orders table. If, for the purposes of this example, I wanted my top-level to be ONLY orders that started with 2, how is that identifiable in your query?
@JerryM. - with where to_char(orders.order_number) like '2%' in the anchor member; db<>fiddle. With your data that gets 16 rows for root order 200. Not sure it entirely solves the repeated-child issue though, unless your real data is quite different.
This is embarrassing but at first I did not understand the issue with child_orders, but now I do. If I identify a top-level order via the where as you specified, does that eliminate the repeated-child issue? I am testing in the fiddle and it does not seem so...
It does it you explicitly identify a top-level order - db<>fiddle - or several specific orders, but will struggle with a pattern.
@JerryM. - I've added a version that should avoid the problem.
|
1

Starting from your input:

with data (
  Order_Number , Requirement_ID , Replenishment_ID ,
  Replenishment_Type , Replenishment_Detail , Replenishment_Date
) as (
  select 300,'AA-300','RO601' ,'Bought',  963, to_date('15-Jul-18','dd-Mon-rr') from dual union all    
  select 300,'AA-300','RO111' ,'Make',  251, to_date('23-Oct-18','dd-Mon-rr') from dual union all    
  select 300,'AA-300','RO435' ,'Make',  837, to_date('4-Mar-18','dd-Mon-rr') from dual union all    
  select 300,'AA-300','RO608' ,'Make',  850, to_date('27-Apr-18','dd-Mon-rr') from dual union all    
  select 300,'AA-516','RO734' ,'Make',  415, to_date('5-May-18','dd-Mon-rr') from dual union all    
  select 300,'AA-516','RO245' ,'Bought',  130, to_date('6-Feb-18','dd-Mon-rr') from dual union all    
  select 300,'AA-516','RO754' ,'Bought',  874, to_date('9-Jun-18','dd-Mon-rr') from dual union all    
  select 300,'AA-468','RO120' ,'Make',  333, to_date('28-Jul-18','dd-Mon-rr') from dual union all    
  select 300,'AA-468','RO96' ,'Bought',  279, to_date('11-Jun-18','dd-Mon-rr') from dual union all    
  select 300,'AA-744','RO576' ,'Make',  452, to_date('9-Jun-18','dd-Mon-rr') from dual union all    
  select 300,'AA-744','RO592' ,'Bought',  967, to_date('16-Jan-18','dd-Mon-rr') from dual union all    
  select 300,'AA-744','RO104' ,'Make',  232, to_date('30-Jan-19','dd-Mon-rr') from dual union all    
  select 300,'AA-744','RO169' ,'Make',  804, to_date('2-Feb-18','dd-Mon-rr') from dual union all    
  select 500,'AA-100','RO567' ,'Make',  725, to_date('22-Mar-18','dd-Mon-rr') from dual union all    
  select 500,'AA-100','RO90' ,'Bought',  240, to_date('14-Mar-18','dd-Mon-rr') from dual union all    
  select 500,'AA-100','RO202' ,'Bought',  185, to_date('26-Feb-18','dd-Mon-rr') from dual union all    
  select 500,'AA-823','RO764' ,'Bought',  629, to_date('15-Oct-18','dd-Mon-rr') from dual union all    
  select 500,'AA-823','RO434' ,'Make',  314, to_date('27-Jun-18','dd-Mon-rr') from dual union all    
  select 500,'AA-823','RO752' ,'Bought',  504, to_date('25-Apr-18','dd-Mon-rr') from dual union all    
  select 500,'AA-823','RO204' ,'Make',  847, to_date('9-Jul-18','dd-Mon-rr') from dual union all    
  select 500,'AA-239','RO367' ,'Bought',  652, to_date('14-Feb-18','dd-Mon-rr') from dual union all    
  select 500,'AA-239','RO732' ,'Bought',  561, to_date('3-Oct-18','dd-Mon-rr') from dual union all    
  select 130,'AA-785','RO573' ,'Make',  616, to_date('1-Apr-18','dd-Mon-rr') from dual union all    
  select 130,'AA-785','RO139' ,'Make',  698, to_date('16-Jul-18','dd-Mon-rr') from dual union all    
  select 130,'AA-785','RO252' ,'Make',  190, to_date('2-Aug-18','dd-Mon-rr') from dual union all    
  select 130,'AA-785','RO561' ,'Make',  453, to_date('13-May-18','dd-Mon-rr') from dual union all    
  select 130,'AA-785','RO775' ,'Make',  974, to_date('7-Aug-18','dd-Mon-rr') from dual union all    
  select 130,'AA-171','RO92' ,'Bought',  493, to_date('1-Apr-18','dd-Mon-rr') from dual union all    
  select 200,'AA-171','RO532' ,'Make',  727, to_date('17-May-18','dd-Mon-rr') from dual union all    
  select 200,'AA-337','RO29' ,'Make',  402, to_date('1-Jun-18','dd-Mon-rr') from dual union all    
  select 200,'AA-337','RO725' ,'Make',  892, to_date('9-Mar-18','dd-Mon-rr') from dual union all    
  select 200,'AA-533','RO216' ,'Bought',  637, to_date('1-Jun-18','dd-Mon-rr') from dual union all    
  select 100,'AA-100', NULL , NULL, NULL, NULL from dual union all    
  select 100,'AA-100','RO438' ,'Make',  125, to_date('19-Mar-18','dd-Mon-rr') from dual union all    
  select 493,'AA-400','RO4', 'Bought',  591, to_date('17-Apr-18','dd-Mon-rr') from dual union all    
  select 493,'AA-401', NULL , NULL, NULL, NULL from dual
)
select connect_by_root(order_number) root_order, data.*, level lvl
from data
start with order_number not in (
  select replenishment_detail from data where replenishment_detail is not null
)
connect by order_number = prior replenishment_detail
order siblings by order_number, replenishment_detail;

ROOT_ORDER ORDER_NUMBER REQUIR REPLE REPLEN REPLENISHMENT_DETAIL REPLENISHMENT_DATE         LVL
---------- ------------ ------ ----- ------ -------------------- ------------------- ----------
       100          100 AA-100 RO438 Make                    125 2018-03-19 00:00:00          1
       100          100 AA-100                                                                1
       200          200 AA-337 RO29  Make                    402 2018-06-01 00:00:00          1
       200          200 AA-533 RO216 Bought                  637 2018-06-01 00:00:00          1
       200          200 AA-171 RO532 Make                    727 2018-05-17 00:00:00          1
       200          200 AA-337 RO725 Make                    892 2018-03-09 00:00:00          1
       300          300 AA-516 RO245 Bought                  130 2018-02-06 00:00:00          1
       300          130 AA-785 RO252 Make                    190 2018-08-02 00:00:00          2
       300          130 AA-785 RO561 Make                    453 2018-05-13 00:00:00          2
       300          130 AA-171 RO92  Bought                  493 2018-04-01 00:00:00          2
       300          493 AA-400 RO4   Bought                  591 2018-04-17 00:00:00          3
       300          493 AA-401                                                                3
       300          130 AA-785 RO573 Make                    616 2018-04-01 00:00:00          2
       300          130 AA-785 RO139 Make                    698 2018-07-16 00:00:00          2
       300          130 AA-785 RO775 Make                    974 2018-08-07 00:00:00          2
       300          300 AA-744 RO104 Make                    232 2019-01-30 00:00:00          1
       300          300 AA-300 RO111 Make                    251 2018-10-23 00:00:00          1
       300          300 AA-468 RO96  Bought                  279 2018-06-11 00:00:00          1
       300          300 AA-468 RO120 Make                    333 2018-07-28 00:00:00          1
       300          300 AA-516 RO734 Make                    415 2018-05-05 00:00:00          1
       300          300 AA-744 RO576 Make                    452 2018-06-09 00:00:00          1
       300          300 AA-744 RO169 Make                    804 2018-02-02 00:00:00          1
       300          300 AA-300 RO435 Make                    837 2018-03-04 00:00:00          1
       300          300 AA-300 RO608 Make                    850 2018-04-27 00:00:00          1
       300          300 AA-516 RO754 Bought                  874 2018-06-09 00:00:00          1
       300          300 AA-300 RO601 Bought                  963 2018-07-15 00:00:00          1
       300          300 AA-744 RO592 Bought                  967 2018-01-16 00:00:00          1
       500          500 AA-100 RO202 Bought                  185 2018-02-26 00:00:00          1
       500          500 AA-100 RO90  Bought                  240 2018-03-14 00:00:00          1
       500          500 AA-823 RO434 Make                    314 2018-06-27 00:00:00          1
       500          500 AA-823 RO752 Bought                  504 2018-04-25 00:00:00          1
       500          500 AA-239 RO732 Bought                  561 2018-10-03 00:00:00          1
       500          500 AA-823 RO764 Bought                  629 2018-10-15 00:00:00          1
       500          500 AA-239 RO367 Bought                  652 2018-02-14 00:00:00          1
       500          500 AA-100 RO567 Make                    725 2018-03-22 00:00:00          1
       500          500 AA-823 RO204 Make                    847 2018-07-09 00:00:00          1

in the WITH DATA clause, substitute your joins. The sort will put all the rows for each "root" order together, but within each "root" the hierarchy will go "depth first" so you can see the direct relationship between levels.

Best regards, Stew Ashton

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.