Saturday, September 3, 2016

Try something new ...

As always, and I know and many have said, preached never (funny is the saying) say never and add, always to that.
Well, the always part, I draw my motivation to write one of these from being inspired by catch-phrases from TV shows, and this one comes from 'House of cards' quite a depiction of deceitfulness and portrait portrayal of the American dream. That's not the point here, anyway.

The point here is just 'try something new'.

And I would humbly like to offer credit to the seed for the idea to be writing a blog entry, to my boss, whom I admire about how beautifully he reads what people want to do, and leading to bringing in the positive in them.

In our regular pursuit to live, we often forget to embrace the brightness of the unknown from which emerges the institution of innovativeness. Not a suggestion to view the following as a new discovery of sorts technically, but yes I want to perceive it to be, of an idea, to expect more from oneself, stretching (I first thought of calling it stretch, but I don't believe in stretches, just living the idea, so the strike off) by trying multiple avenues even after you have attained the basic task of surviving or merely servicing.

I would like to add, to express the idea, about Darwinian theory for the survival of the fittest brought more often than we would imagine having heard of, but don't you think life, intelligent (we call ourselves) life as we know, evolution took millions to billions of years before it could see one. I take 2 things out of this, one of which is the time. We as individuals are not at such liberty of, and second more important in my eye is the part where Darwin chose to state the process as 'fittest', not the best. Once I heard it as biologist explanation of evolution, that our bodies are not built perfect, but just how it is built works and does not break down on its own. We can always argue about flaws, potent to our lives with minimalistic disturbances, but that I see to serve to our advantage towards the point.

So make something that can withstand time and tide, does not crumble under its own burden, and also think about, during the process of building that can there be better ways to accomplishing the task better than in all conceivable way. Have you tried to test the offering to break it down, identify the limits, is it multi-fold better than what you have started with, and that you have not only fulfilled the requirement as mere completion of the task in the simplest of execution.

Now, I have recently encountered with the task, yes the article is being self-righteous, for which I am requesting for pardon, but I say so as to have had the first-hand experience off, not a bystander. Back to the task ...

Starting with the ask, which was in a technical space of 'Configure to Order' process, to generate item descriptions based on the components selected. Since my exposure and experience was in Oracle, thence the source. In Oracle, there already exists a provision (let's call it guideline by Oracle) to do this task, with a limitation i.e. a situation where multiple selections with similar attributes forming the assembly cannot all be represented in the description.

In other more technical terms, configured items' description is built based on descriptive elements shared between the assembly item and its components, but if there exists more than one component have different values for those elements, especially that could have for non-mutually exclusive options under an option class, only one value passes up to the assembly, which could appear to be random. The solution offered in standard Oracle, uses MAX to identify that single choice for common elements shared by component items.

The answer was simple, at least at first. That, let's use LISTAGG to represent all element values. And the answer is LISTAGG, it was only the journey which happened to be even more interesting and what it revealed. It would not only have to be just aggregate the list, but the list has to be unique too, it needs to be ordered (at least by some means) which I chose it to be dependent on components' item number in the assembly structure.

This essentially gave me the solution, after struggling for several hours to understand the intricate details of how Oracle have had build the solution (I am calling guideline, here). I admired for several hours before and still do, how in a single SQL UPDATE statement, all the intricacies of rolling up of element value weaving over base models' assembly structure, are built into. Additionally, how efficient the solutions' response for the database call is, which is only a few hundred milliseconds.

I could have stopped here, I didn't, partly because I am never ( ;) ) easily satisfied, or least always have a knack of trying to find the efficiency of what is getting built, which often helps and make me explore and learn. More importantly, the timing of the solution was least to say fairly high.

Even though I chose the same methodology as it was earlier built on, that of using a single UPDATE SQL statement to have solution run for, it was tremendously costly (up to 200 times), and of course, cost here is time to run each time for every configured item when the description is generated.

The SQL statement when running as SELECT, was fairly effective, offering the result in similar few hundred millisecond response, which goes like this ...

select
 cat_grp,
 mdev_assembly_ele_seq,
 mdev_comp_ele_name,
    Decode (
     ele_aggr,
     0, MAX (ele_val_applied),
        LISTAGG (ele_val_applied, '.') 
   WITHIN GROUP (order by mdev_assembly_ele_seq)
        ) ele_val,
 (LISTAGG (ele_val_applied, '.') 
  WITHIN GROUP (order by mdev_assembly_ele_seq)
 ) future_val,
    MAX (ele_val_applied) current_val,
    config_desc
from (
    Select distinct
     de_regEx.segment1 cat_grp,
     mdev_assembly.element_sequence mdev_assembly_ele_seq,
        bbom.assembly_item_id cfg_bom_assembly_item_id,
        mdev_comp.element_name mdev_comp_ele_name,
        nvl( regexp_count (
    cg_assembly.attribute3,
    '((,|^)'|| mdev_comp.element_name ||'(,|$)|^\[ALL\]$)'
   ),
  0 ) ele_aggr , 
        --mdev_comp.element_value ele_val_current,
        DECODE (mdev_comp.element_name,
            de_regEx.ele_name,
            REGEXP_REPLACE (
                mdev_comp.element_value
                , de_regEx.pattern, de_regEx.replacement_string
            ) ,            
         mdev_comp.element_value
        ) ele_val_applied, 
        --de_regEx.pattern,
  --de_regEx.replacement_string,
  --de_regEx.group_element,
  --de_regEx.match_element, 
  --de_regEx.item_catalog_group_id,
        msib_assembly.description config_desc,
        null
    From
        bom_bill_of_materials bbom, --- bi
        mtl_system_items_b msib_assembly,
        mtl_item_catalog_groups_b cg_assembly,
        bom_inventory_components bic, --- bc1
        mtl_descr_element_values mdev_comp, --- v
        mtl_descr_element_values mdev_assembly, ---- i
        bom_inventory_components bic_m_cls, --- bc2
        bom_bill_of_materials bbom_m_cls, --- bi2
        bom_dependent_desc_elements bdde_cls, --- be
        (select
         regexp_replace (flv.meaning, '(.*)\.(.*)', '\2' ) match_element,
         micgb.item_catalog_group_id,
            msib.inventory_item_id,
            micgb.segment1,
            flv.meaning group_element,
         flv.description pattern,
            flv.tag replacement_string,
            regexp_replace (flv.lookup_code, '#*[[:digit:]]*', '' ) ele_name
        from
            fnd_lookup_values flv,
          mtl_item_catalog_groups_b micgb,
         mtl_system_items_b msib
        where 1=1
        and flv.lookup_type = 'XYZ_LOOKUP'
        and sysdate between NVL(flv.start_date_active, sysdate)
   and nvl(flv.end_date_active, sysdate)
        and flv.enabled_flag = 'Y'
        and (
         (
                regexp_replace (flv.meaning, '((.*)\.(.*)|(^\.(.*)))' , '\4' )
     is null 
                and micgb.segment1 like 
     regexp_replace (flv.meaning, '((.*)\.(.*)|(^\.(.*)))', '\2' )
            ) Or
            (
             regexp_replace (flv.meaning, '((.*)\.(.*)|(^\.(.*)))' , '\4' )
     is not null 
            )
            )
        and msib.item_catalog_group_id = micgb.item_catalog_group_id
        and msib.organization_id = p_master_org
        ) de_regEx,
        dual
    where 1=1
        and bbom.assembly_item_id = p_item_id
        and bbom.organization_id = p_org_id
        and bbom.assembly_item_id = msib_assembly.inventory_item_id
        and bbom.organization_id = msib_assembly.organization_id
        and msib_assembly.item_catalog_group_id = cg_assembly.item_catalog_group_id
        and mdev_assembly.inventory_item_id = de_regEx.inventory_item_id (+)
        and mdev_assembly.element_name = de_regEx.match_element (+)
        and bbom.alternate_bom_Designator IS NULL
        and bbom.source_bill_sequence_id = bic.bill_sequence_id 
        and bic.component_item_id = mdev_comp.inventory_item_id
        and bbom.assembly_item_id = mdev_assembly.inventory_item_id
        and mdev_comp.element_name = mdev_assembly.element_name 
        and ABS(bic.model_comp_seq_id) = bic_m_cls.component_sequence_id
        and bic_m_cls.bill_sequence_id = bbom_m_cls.common_bill_sequence_id
        and bbom.organization_id = bbom_m_cls.organization_id
        and bbom_m_cls.source_bill_sequence_id = bdde_cls.bill_sequence_id
        and bic_m_cls.bill_sequence_id = bdde_cls.bill_sequence_id
        and mdev_comp.element_name = bdde_cls.element_name
) 
Group by
    cat_grp, 
    mdev_comp_ele_name, 
    mdev_assembly_ele_seq,
    ele_aggr,
    config_desc
order by config_desc, mdev_assembly_ele_seq


But the same, when embedded to work as UPDATE ran into several 10's of seconds, which was not acceptable to me. I don't shy away from asking for help, as I don't consider myself to possess deep technological know-how. Thus, extended my hand to ask for help to the technical team. Kept tossing and turning overnight and weekend trying to figure out what could possibly make it efficient.
Two reasons there, one even if I ask for someone to help, I can't usually give up to keep trying on my own as well, also many times technical teams are thinly built with more emphasis on time to delivery than on the quality. If it works it's out of the queue - isn't that sounds like the course of nature. This by no means is to mean any disregard to anyone efforts, it seems to me this is how the nature of the enterprise mostly have had become.

Here, goes the UPDATE statement -

UPDATE MTL_DESCR_ELEMENT_VALUES i
 SET i.element_value = (
   select 
    NVL(
     decode (
      ele_aggr,
      0, MAX (ele_val_applied),
      LISTAGG (ele_val_applied, '.')
       WITHIN GROUP (order by mx_bic_item_num)
     ) 
    , i.element_value)
    ele_val
   from (
    select distinct
     max(bic.item_num) mx_bic_item_num,
     ...
     ...
     ...
     and mdev_comp.element_name = bdde_cls.element_name
    Group by
     mdev_assembly.element_name,
     nvl( regexp_count (
       cg_assembly.attribute3,
       '((,|^)'|| mdev_comp.element_name ||'(,|$)|^\[ALL\]$)'
      ),
     0 ),
     DECODE (mdev_comp.element_name,
      de_regEx.ele_name,
      REGEXP_REPLACE (
       mdev_comp.element_value
       , de_regEx.pattern, de_regEx.replacement_string
      ) ,            
      mdev_comp.element_value
     ) 
   ) 
   where mdev_assembly_ele_name = i.element_name
   Group by
    mdev_assembly_ele_name,
    ele_aggr
  )
 WHERE i.inventory_item_id = p_params.p_item_id;


During this tossing and turning part, I even led myself to believe that I was able to reduce the execution time down to original timeline (around '00 ms) when doing some changes while tossing and turning over (that too). Well, the bubble pretty soon (and I am glad, soon) blew on my face, and back to square one, with a long execution time, just that it was under 10 sec, but non-acceptable.

The thought that struck me was, if the SELECT is fast enough, why not to use it to the advantage and transform the solution, instead of single DML, let it be a combination of SQL and PLSQL solution.

It resulted into SELECTing with BULK COLLECT (added advantage does not hurt) and using FORALL to UPDATE the final result back to the database. Something like this ...

/*BULK COLLECT*/
select 
 mdev_assembly_ele_name,
  decode (
   ele_aggr,
   0, MAX (ele_val_applied),
   LISTAGG (ele_val_applied, '.') WITHIN GROUP (order by mx_bic_item_num)
  ) 
 ele_val
BULK COLLECT INTO l_ConfigCompDEs
from (
 select distinct 
 ...
 ...
 ...
) 
Group by
 mdev_assembly_ele_name,
 ele_aggr;

/*Followed by FORALL*/
 
FORALL indx IN l_ConfigCompDEs.FIRST .. l_ConfigCompDEs.COUNT
 UPDATE MTL_DESCR_ELEMENT_VALUES i
  SET i.element_value = nvl(l_ConfigCompDEs (indx).ele_val, i.element_value)
 WHERE i.inventory_item_id = p_params.p_item_id
 AND i.element_name = l_ConfigCompDEs (indx).ele_name;



The part that I am still not able to comprehend is and will most certainly appreciate someone enlightening me for my mistake on, why a single DML statement running everything within the database was costly, far so many folds than using a PL-SQL combination which involves 'context-switching', so to speak.

Finally, to share the results, the net solution is 33% faster than before I started with the problem statement.
And the learning curve leading me to affirm my belief on, not all battles can be won by same kinda strategy. Every scenario is unique to be tried upon, and experience to be gained from and journey to be lived, but only if you aspire to do something new, challenge your boundaries and wanted to do away from your routine.

Saturday, February 27, 2016

Keeping Scores ....

Why do we care about technicality ...
I would like to classify the definition for the work in two contexts.
Going to a school to acquire skill, does that come down to people seeing it as a stamp for skillfulness. It's complicated than of how could we measure people's skill to their being suitable for being capable of conducting a job.
People often say and want to be 'not judge'-d, then what could possibly be a way we find of the measure of skills, keeping aside certifiability of an institution being appropriate for, of the measure.
Can we say does that, or can we bring it down to findings of newer means of assessing someone's capability leaving behind the need to carrying certificate ?

Prejudice - maybe it is our prejudice of our association to ... institutions, the emotion bond, after all being human is commonly expressed as about of emotional being ...

Phrase "love" in many ways may be overrated, well let's say when they are being explicit stated only, or even of the want to be listened to. This reminds of a read - if you like a flower you might take it away from its roots when you love it, you would nurture it without in ITS entirety of acceptance in return.

People getting caught up on the things that are going to happen to them, rather than perseverance on the continuance of our job - someone (would have) said 'life goes on', definitely without us, so shall we as well as our jobs / action shall continue our existence and not referencing only about survival, and honestly just while writing this, I felt existence does carry both meanings - to be godly and being evil. Those are the extremes of classifications to action one conducts', but as pretty much every culture talked about balances and co-existences of the yin-and-yang of nature, and we so 'being' being part of it so do we.

I watch too much TV - another reference I would mention is of 'Suits' is what attributed to this expression.

And the other important background and more so the task of why here ! is an event that I recently been through of completing (accomplishing / achieving) a task of investigating of facts to establish sanctity of the similarity, and I like to put it as findings 'absence of' differences (IF ANY), by my nature whatsoever there may be quantified more precisely.

And it is this task search help me learn, a little trick with "Connect by" prior.

The task was, or rather can be if simply broken to pieces, of where there was a need to have "Bill of Materials" structures' hierarchically, well this is the simple part (saying simple because I would invariably find a straight answer over the net to this for specific tree for a single organization), well I can bet and consider myself egotistical (not wanting to say pride - this is because I want someone to point to me an existing article solving bill-of-material for multiple organizations) in the complexity arising from it, where organization is not predetermined and source of input, let's say the primary initiating factor would provide with to sequence a few such hierarchies as a single output - and then to be used or join with another factor for achieving the final job.

After I learned it now, could say the answer being quite simple, maybe for some sound to be off in plain sight. A point, I want to emphasize could not find a single article providing the direct answer hence, here is it to offer all of it one place -

In CONNECT BY, PRIOR phrase can be used as many times as you want - here is how:
 CONNECT BY PRIOR ASMBLY.COMPONENT_ITEM_ID = ASMBLY.ASSEMBLY_ITEM_ID 
    AND PRIOR ASMBLY.ORGANIZATION_ID = ASMBLY.ORGANIZATION_ID


Since the building block of actual answer is having defined -
first each single assembly, and then running that structure to construct a hierarchy.
This is to achieve when there is a need for getting the hierarchy of all organization being prevented from intermingling.

The query for the complete and all bill of materials for all organizations - along with some of the nice and interesting enough keyword that is handy with connect by.

 SELECT
  LEVEL BILL_LEVEL,
  NVL(MODL_ITEM,COMPITEM)MODEL_TREE_REF,
  ASMBLY.*,
  CONNECT_BY_ROOT ASMBLYITEM ROOT,
        CONNECT_BY_ROOT ASSEMBLY_ITEM_ID ROOT_ID,
  SYS_CONNECT_BY_PATH(LPAD(ITEM_NUM,5,'0'), ' ') ITEM_NUM_PATH,
  CONNECT_BY_ROOT ASLY_MODL_ITEM || SYS_CONNECT_BY_PATH(COMPITEM, '~') COMP_PATH,
  SYS_CONNECT_BY_PATH(NVL(BASE_ITEM_ID,COMPONENT_ITEM_ID), '~') ITEM_ID_PATH,
        ROWNUM BILL_TREE_SEQ
 FROM (
  SELECT
   MP.ORGANIZATION_CODE,
   MSIB_ASLY.SEGMENT1 ASMBLYITEM,
   COMP.SEGMENT1 COMPITEM,
   BIC.COMPONENT_QUANTITY,
            BIC.COMPONENT_YIELD_FACTOR,
   COMP.DESCRIPTION COMPDESC,
   COMP.ITEM_TYPE,
   COMP.INVENTORY_ITEM_STATUS_CODE COMP_STATUS,
   COMP.BOM_ITEM_TYPE,
   BIC.ITEM_NUM,
   BIC.OPERATION_SEQ_NUM,
   BIC.CHANGE_NOTICE,
   BBOM.ORGANIZATION_ID,
   COMP.BASE_ITEM_ID,
            MODL.SEGMENT1 MODL_ITEM,
            ASLY_MODL.SEGMENT1 ASLY_MODL_ITEM,
   BBOM.ASSEMBLY_ITEM_ID,
   BIC.COMPONENT_ITEM_ID,
   BIC.DISABLE_DATE,
   BBOM.CREATION_DATE
  FROM
   BOM_BILL_OF_MATERIALS BBOM,
   BOM_INVENTORY_COMPONENTS BIC,
   MTL_SYSTEM_ITEMS_B MSIB_ASLY,
   MTL_SYSTEM_ITEMS_B COMP,
   MTL_SYSTEM_ITEMS_B MODL,
   MTL_SYSTEM_ITEMS_B ASLY_MODL,
   MTL_PARAMETERS MP
  WHERE 1=1
  AND BBOM.BILL_SEQUENCE_ID = BIC.BILL_SEQUENCE_ID
  AND BIC.DISABLE_DATE IS NULL /*Effective components only*/
  AND BBOM.ASSEMBLY_ITEM_ID = MSIB_ASLY.INVENTORY_ITEM_ID
  AND BBOM.ORGANIZATION_ID = MSIB_ASLY.ORGANIZATION_ID
  AND MSIB_ASLY.ORGANIZATION_ID = COMP.ORGANIZATION_ID
  AND BIC.COMPONENT_ITEM_ID = COMP.INVENTORY_ITEM_ID
  AND COMP.ORGANIZATION_ID = MODL.ORGANIZATION_ID (+)
  AND COMP.BASE_ITEM_ID = MODL.INVENTORY_ITEM_ID (+)
  AND MSIB_ASLY.ORGANIZATION_ID = ASLY_MODL.ORGANIZATION_ID (+)
  AND MSIB_ASLY.BASE_ITEM_ID = ASLY_MODL.INVENTORY_ITEM_ID (+)
  AND MSIB_ASLY.ORGANIZATION_ID = MP.ORGANIZATION_ID
  AND COMP.BOM_ITEM_TYPE = 4
  ) ASMBLY
 START WITH ASMBLY.ASSEMBLY_ITEM_ID = :ITEM
 CONNECT BY PRIOR ASMBLY.COMPONENT_ITEM_ID = ASMBLY.ASSEMBLY_ITEM_ID 
    AND PRIOR ASMBLY.ORGANIZATION_ID = ASMBLY.ORGANIZATION_ID

I also expressed that the initiating criteria contain the decision for the organization and consists of which (items') hierarchies to be built and that initiating factor in itself posed certain challenges of maintaining the hierarchy clean - for me it was sales order containing a configured model comprising of several ATO and PTO structures.

WITH ORD_LINES AS
(
 SELECT
  OEL.LINE_NUMBER,
  OEL.OPTION_NUMBER,
        OEL.COMPONENT_NUMBER,
  OEL.ORDERED_ITEM,
  OEL.ORDERED_QUANTITY,
  OEL.INVENTORY_ITEM_ID,
  OEL.ITEM_TYPE_CODE,
  MSIB.ITEM_TYPE,
  OEL.SHIPPABLE_FLAG,
  OEL.FLOW_STATUS_CODE,
  OEL.SOURCE_TYPE_CODE,
        OEL.SHIP_FROM_ORG_ID,
        OEL.TOP_MODEL_LINE_ID,
        MDL_OEL.ATTRIBUTE1 CONVEYOR_NUM,
        MDL_OEL.ORDERED_ITEM TOP_MODEL_ITEM,
  NULL
 FROM
  OE_ORDER_LINES_ALL OEL,
  OE_ORDER_LINES_ALL MDL_OEL,
  MTL_SYSTEM_ITEMS_B MSIB
 WHERE 1=1
    AND OEL.TOP_MODEL_LINE_ID = MDL_OEL.LINE_ID
 AND OEL.SHIP_FROM_ORG_ID = MSIB.ORGANIZATION_ID
 AND OEL.INVENTORY_ITEM_ID = MSIB.INVENTORY_ITEM_ID
 AND OEL.SHIPPABLE_FLAG = 'Y'
 AND OEL.HEADER_ID = :HDR
 AND OEL.LINE_NUMBER = 1 
 ORDER BY
  OEL.LINE_NUMBER,
  OEL.OPTION_NUMBER,
  NULL
),
BILL_STRUCT AS 
(
 SELECT
  LEVEL BILL_LEVEL,
  NVL(MODL_ITEM,COMPITEM)MODEL_TREE_REF,
  ASMBLY.*,
  CONNECT_BY_ROOT ASMBLYITEM ROOT,
        CONNECT_BY_ROOT ASSEMBLY_ITEM_ID ROOT_ID,
  SYS_CONNECT_BY_PATH(LPAD(ITEM_NUM,5,'0'), ' ') ITEM_NUM_PATH,
  CONNECT_BY_ROOT ASLY_MODL_ITEM || SYS_CONNECT_BY_PATH(COMPITEM, '~') COMP_PATH,
  SYS_CONNECT_BY_PATH(NVL(BASE_ITEM_ID,COMPONENT_ITEM_ID), '~') ITEM_ID_PATH,
        ROWNUM BILL_TREE_SEQ
 FROM (
  SELECT
   MP.ORGANIZATION_CODE,
   MSIB_ASLY.SEGMENT1 ASMBLYITEM,
   COMP.SEGMENT1 COMPITEM,
   BIC.COMPONENT_QUANTITY,
            BIC.COMPONENT_YIELD_FACTOR,
   COMP.DESCRIPTION COMPDESC,
   COMP.ITEM_TYPE,
   COMP.INVENTORY_ITEM_STATUS_CODE COMP_STATUS,
   COMP.BOM_ITEM_TYPE,
   BIC.ITEM_NUM,
   BIC.OPERATION_SEQ_NUM,
   BIC.CHANGE_NOTICE,
   BBOM.ORGANIZATION_ID,
   COMP.BASE_ITEM_ID,
            MODL.SEGMENT1 MODL_ITEM,
            ASLY_MODL.SEGMENT1 ASLY_MODL_ITEM,
   BBOM.ASSEMBLY_ITEM_ID,
   BIC.COMPONENT_ITEM_ID,
   BIC.DISABLE_DATE,
   BBOM.CREATION_DATE
  FROM
   BOM_BILL_OF_MATERIALS BBOM,
   BOM_INVENTORY_COMPONENTS BIC,
   MTL_SYSTEM_ITEMS_B MSIB_ASLY,
   MTL_SYSTEM_ITEMS_B COMP,
   MTL_SYSTEM_ITEMS_B MODL,
   MTL_SYSTEM_ITEMS_B ASLY_MODL,
   MTL_PARAMETERS MP
  WHERE 1=1
  AND BBOM.BILL_SEQUENCE_ID = BIC.BILL_SEQUENCE_ID
  AND BIC.DISABLE_DATE IS NULL /*Effective components only*/
  AND BBOM.ASSEMBLY_ITEM_ID = MSIB_ASLY.INVENTORY_ITEM_ID
  AND BBOM.ORGANIZATION_ID = MSIB_ASLY.ORGANIZATION_ID
  AND MSIB_ASLY.ORGANIZATION_ID = COMP.ORGANIZATION_ID
  AND BIC.COMPONENT_ITEM_ID = COMP.INVENTORY_ITEM_ID
  AND COMP.ORGANIZATION_ID = MODL.ORGANIZATION_ID (+)
  AND COMP.BASE_ITEM_ID = MODL.INVENTORY_ITEM_ID (+)
  AND MSIB_ASLY.ORGANIZATION_ID = ASLY_MODL.ORGANIZATION_ID (+)
  AND MSIB_ASLY.BASE_ITEM_ID = ASLY_MODL.INVENTORY_ITEM_ID (+)
  AND MSIB_ASLY.ORGANIZATION_ID = MP.ORGANIZATION_ID
  AND COMP.BOM_ITEM_TYPE = 4
  ) ASMBLY,
        ORD_LINES
    WHERE 1=1 
    AND ASMBLY.ORGANIZATION_ID = ORD_LINES.SHIP_FROM_ORG_ID
 START WITH ASMBLY.ASSEMBLY_ITEM_ID = ORD_LINES.INVENTORY_ITEM_ID
 CONNECT BY PRIOR ASMBLY.COMPONENT_ITEM_ID = ASMBLY.ASSEMBLY_ITEM_ID 
    AND PRIOR ASMBLY.ORGANIZATION_ID = ASMBLY.ORGANIZATION_ID
    AND PRIOR ORD_LINES.LINE_NUMBER = ORD_LINES.LINE_NUMBER
    AND PRIOR ORD_LINES.OPTION_NUMBER = ORD_LINES.OPTION_NUMBER
    AND PRIOR ORD_LINES.COMPONENT_NUMBER = ORD_LINES.COMPONENT_NUMBER (+)
 ORDER SIBLINGS BY ASMBLY.ITEM_NUM
)
SELECT
    ORD_LINES.*,
 BILL_STRUCT.* 
FROM
 ORD_LINES, BILL_STRUCT
WHERE 1=1 
AND ORD_LINES.INVENTORY_ITEM_ID = BILL_STRUCT.ROOT_ID (+)
AND ORD_LINES.SHIP_FROM_ORG_ID = BILL_STRUCT.ORGANIZATION_ID (+)
ORDER BY LINE_NUMBER, OPTION_NUMBER , ITEM_NUM_PATH

Some of the interesting finds for me were of OPTION_NUMBER and especially COMPONENT_NUMBER, and it optionally being populated, hence the need of an outer join.

Enjoy and keep sharing... The point where I asked for existing article on this same solution is with a pinch of salt, but my ask from the community is to teach me - how to perform better searches for faster answers.

Friday, January 1, 2016

Thieves are good ...

(First) Theft is not ...
Well poetically speaking, there are several expression in praise for those who have been accused of taking (certain "things") ...

The point, about my admiration here is of the idea for the skill of achieving the task by making use of the resources available to all (in general) potentially in plane sight or not so difficult to obtain, but crafting an elegance to fulfill the want, (I considered writing 'need', but felt that sounded imprudent), especially with the intent of keeping others away from harms way, during the act and for some "from the act too". I am discounting the fact that of the material aspect of belonging involved in the action, which people to more popular belief these days is often give greater importance of the act.

Some of you might find this expression to be oxymoron-ish, this is what I have had already earlier once heard of while offering contradictory thought to that of, I would say, a commonly expected notion (from the "word"). - So pardon me of hurting your feelings, on that ground.

Back - to thought-charting ...
The background of this inspiration, came from recent revisit to a series 'White collar'.
And of-course people landed on this page would prefer to get their solution, (in the course will) also be sharing in the analogy built with all the prologue.

Topic-

Compare record attributes from the same table (technical heading)

What an association ebsWiki, technical piece could be with the admiration to honing the skill in lets say, not commonly exploited - that's about it.

So, I recently figured out a new keyword in Oracle "unpivot". With its usage, quite easily long rows of data can be transposed, exposing the value by attributes, thus enabling an effective comparison between two sets of similar information - i.e. comparing two records field by field.

My need arose in context of, so the guiding solution, and of-course can invariably be used in other situations is, from the requirement of comparing 'inventory item data' in e-Business suite. Depending on which Oracle apps version you are on, this primary table for Inventory module has 200 - 400 attributes for each record that be of interest - I would even opt for this solution with mere 50+ fields in question.

So far I have mostly been exporting the records in excel and additionally come up with formula to compare two inventory item records, say for example. This also needed even after all this, a struggle to dig for differences.

What if - each inventory item records (with say 350 fields) is represented as attribute-value pair, along with its' key (item-org combination), the consumption of such layout makes it much more simpler. Thus, the only significant task, is to primarily achieve this transposition.

SELECT 
    ID_KEY1, ID_KEY2, 
    B_B_COL, B_B_VAL, LAST_UPDATE_DATE B_LAST_UPDATE_DATE,
    SEGMENT1
FROM 
    (SELECT INVENTORY_ITEM_ID ID_KEY1, ORGANIZATION_ID ID_KEY2, LAST_UPDATE_DATE, SEGMENT1,
        ENABLED_FLAG, ATTRIBUTE_CATEGORY, ATTRIBUTE9, CUSTOMER_ORDER_FLAG, SO_TRANSACTIONS_FLAG, RETURNABLE_FLAG, TO_CHAR(HAZARD_CLASS_ID) HAZARD_CLASS_ID, ENFORCE_SHIP_TO_LOCATION_CODE, TO_CHAR(RECEIVE_CLOSE_TOLERANCE) RECEIVE_CLOSE_TOLERANCE, TO_CHAR(SOURCE_TYPE) SOURCE_TYPE, TO_CHAR(UNIT_VOLUME) UNIT_VOLUME, TO_CHAR(CUM_MANUFACTURING_LEAD_TIME) CUM_MANUFACTURING_LEAD_TIME, REPETITIVE_PLANNING_FLAG, TO_CHAR(VARIABLE_LEAD_TIME) VARIABLE_LEAD_TIME, DEFAULT_INCLUDE_IN_ROLLUP_FLAG, TO_CHAR(PREPROCESSING_LEAD_TIME) PREPROCESSING_LEAD_TIME, TO_CHAR(FIXED_DAYS_SUPPLY) FIXED_DAYS_SUPPLY, TO_CHAR(ENGINEERING_DATE) ENGINEERING_DATE, TO_CHAR(SECONDARY_SPECIALIST_ID) SECONDARY_SPECIALIST_ID, TO_CHAR(WARRANTY_VENDOR_ID) WARRANTY_VENDOR_ID, CYCLE_COUNT_ENABLED_FLAG, TO_CHAR(MRP_PLANNING_CODE) MRP_PLANNING_CODE, CONTAINER_TYPE_CODE, GLOBAL_ATTRIBUTE_CATEGORY, GLOBAL_ATTRIBUTE9, USAGE_ITEM_FLAG, ORDERABLE_ON_WEB_FLAG, BULK_PICKED_FLAG, FINANCING_ALLOWED_FLAG, TO_CHAR(DUAL_UOM_DEVIATION_LOW) DUAL_UOM_DEVIATION_LOW, CREATE_SUPPLY_FLAG, TO_CHAR(CURRENT_PHASE_ID) CURRENT_PHASE_ID, TO_CHAR(SO_AUTHORIZATION_FLAG) SO_AUTHORIZATION_FLAG, TO_CHAR(DAYS_MAX_INV_WINDOW) DAYS_MAX_INV_WINDOW, ATTRIBUTE17, ATTRIBUTE26, CHILD_LOT_VALIDATION_FLAG, TO_CHAR(MATURITY_DAYS) MATURITY_DAYS, RECIPE_ENABLED_FLAG, GLOBAL_ATTRIBUTE15
        FROM MTL_SYSTEM_ITEMS_B MSIB
        WHERE 1=1
  /*Record limiting factor*/
        AND EXISTS (SELECT 1 FROM
            MTL_SYSTEM_ITEMS_INTERFACE MSII WHERE 1=1
            AND process_flag NOT IN 7 
            AND MSIB.INVENTORY_ITEM_ID = MSII.INVENTORY_ITEM_ID
            AND MSIB.ORGANIZATION_ID = MSII.ORGANIZATION_ID
            )
    ) 
UNPIVOT INCLUDE NULLS
    (b_B_VAL
        FOR b_B_COL
            IN (
        ENABLED_FLAG, ATTRIBUTE_CATEGORY, ATTRIBUTE9, CUSTOMER_ORDER_FLAG, SO_TRANSACTIONS_FLAG, RETURNABLE_FLAG, HAZARD_CLASS_ID, ENFORCE_SHIP_TO_LOCATION_CODE, RECEIVE_CLOSE_TOLERANCE, SOURCE_TYPE, UNIT_VOLUME, CUM_MANUFACTURING_LEAD_TIME, REPETITIVE_PLANNING_FLAG, VARIABLE_LEAD_TIME, DEFAULT_INCLUDE_IN_ROLLUP_FLAG, PREPROCESSING_LEAD_TIME, FIXED_DAYS_SUPPLY, ENGINEERING_DATE, SECONDARY_SPECIALIST_ID, WARRANTY_VENDOR_ID, CYCLE_COUNT_ENABLED_FLAG, MRP_PLANNING_CODE, CONTAINER_TYPE_CODE, GLOBAL_ATTRIBUTE_CATEGORY, GLOBAL_ATTRIBUTE9, USAGE_ITEM_FLAG, ORDERABLE_ON_WEB_FLAG, BULK_PICKED_FLAG, FINANCING_ALLOWED_FLAG, DUAL_UOM_DEVIATION_LOW, CREATE_SUPPLY_FLAG, CURRENT_PHASE_ID, SO_AUTHORIZATION_FLAG, DAYS_MAX_INV_WINDOW, ATTRIBUTE17, ATTRIBUTE26, CHILD_LOT_VALIDATION_FLAG, MATURITY_DAYS, RECIPE_ENABLED_FLAG, GLOBAL_ATTRIBUTE15
            )
    );


Some explanation, if you still care --- :), and obviously the illustrated solution derivation.
  • Since there are too many fields to work with, so ideally it is important to break them in sizable chuck. With no particular important to specific nature of attributes / their classifications for grouping, I just picked up sets of 50 - 52 fields at a time.
  • Significance of breaking such a huge matrices is also required for their in-comprehendible need of memory (if otherwise, all of them put together), especially when the data-table in question is basis of the module, hence the volume.
  • NOTE - It might still remain huge memory hog. Important lesson that I learnt by distributing the 350+ fields and recombining by a use of "UNION ALL", although I might be running the same query on the same table over and over, is significant performance saving. The sample in play offered benefit of over factor of 200 folds. 
  • Now, the other important factor, where your individual requirement pops-in, is to try to restrict the overall number of records being churned from the base table you are operating upon. Well, if I do not find anything else, "time" is always the savior, in ebs standard structures.
  • Another key to note in restructuring the information, is the base rule of Oracle, to keep datatype consistent while representing data under the same column. A beautiful keyword of "TO_CHAR" come quite handy in delivery.
The above code happen to be, stage 1 where only one of (7) groups of field-sets (chuck) are restructured.

UNION ALL (stage 2)
  • This might appear lame and as it actually is, only due to the size and repetitive nature of this entire Query, considered putting it here.

SELECT 
    ID_KEY1, ID_KEY2,
    B_COL, B_VAL, LAST_UPDATE_DATE B_LAST_UPDATE_DATE,
    SEGMENT1
FROM 
    (SELECT INVENTORY_ITEM_ID ID_KEY1, ORGANIZATION_ID ID_KEY2, LAST_UPDATE_DATE, SEGMENT1,
        <"FIELD-SET 1, enlaced with TO_CHAR as necessary">
        FROM MTL_SYSTEM_ITEMS_B MSIB
        WHERE 1=1
        AND EXISTS (SELECT 1 FROM
            MTL_SYSTEM_ITEMS_INTERFACE MSII WHERE 1=1
            AND MSIB.INVENTORY_ITEM_ID = MSII.INVENTORY_ITEM_ID
            AND MSIB.ORGANIZATION_ID = MSII.ORGANIZATION_ID
            )
    ) 
UNPIVOT INCLUDE NULLS
    (B_VAL
        FOR B_COL
            IN (
   <"FIELD-SET 1, only the field labels">
            )
    )
UNION ALL /*n*/
SELECT 
    ID_KEY1, ID_KEY2,
    B_COL, B_VAL, LAST_UPDATE_DATE B_LAST_UPDATE_DATE,
    SEGMENT1
FROM 
    (SELECT INVENTORY_ITEM_ID ID_KEY1, ORGANIZATION_ID ID_KEY2, LAST_UPDATE_DATE, SEGMENT1,
        <"FIELD-SET n, enlaced with TO_CHAR as necessary">
        FROM MTL_SYSTEM_ITEMS_B MSIB
        WHERE 1=1
        AND EXISTS (SELECT 1 FROM
            MTL_SYSTEM_ITEMS_INTERFACE MSII WHERE 1=1
            AND MSIB.INVENTORY_ITEM_ID = MSII.INVENTORY_ITEM_ID
            AND MSIB.ORGANIZATION_ID = MSII.ORGANIZATION_ID
            )
    ) 
UNPIVOT INCLUDE NULLS
    (B_VAL
        FOR B_COL
            IN (
   <"FIELD-SET n, only the field labels">
            )
    )

One of the usages of comparing the inventory item data for me was not directly with itself but with its interface table. Hence, the phase (stage) 3, extrapolated here and then establishing the comparison gives the following (look to) illustration.


WITH ITEM_IFACE_VAL AS 
(SELECT 
    ID_KEY1, ID_KEY2, TRANSACTION_ID,
    COL, VAL, LAST_UPDATE_DATE, PROCESS_FLAG,
    NULL
FROM 
    (SELECT INVENTORY_ITEM_ID ID_KEY1, ORGANIZATION_ID ID_KEY2, LAST_UPDATE_DATE, TRANSACTION_ID, PROCESS_FLAG,
        <"FIELD-SET 1, enlaced with TO_CHAR as necessary">
        FROM MTL_SYSTEM_ITEMS_INTERFACE
        WHERE 1=1
        AND process_flag NOT IN 7 
    ) 
UNPIVOT
    (val
        FOR col
            IN (
   <"FIELD-SET 1, only the field labels">
            )
    )
UNION ALL /*n*/
SELECT 
    ID_KEY1, ID_KEY2, TRANSACTION_ID,
    COL, VAL, LAST_UPDATE_DATE, PROCESS_FLAG,
    NULL
FROM 
    (SELECT INVENTORY_ITEM_ID ID_KEY1, ORGANIZATION_ID ID_KEY2, LAST_UPDATE_DATE, TRANSACTION_ID, PROCESS_FLAG,
        <"FIELD-SET n, enlaced with TO_CHAR as necessary">
        FROM MTL_SYSTEM_ITEMS_INTERFACE
        WHERE 1=1
        AND process_flag NOT IN 7 
    ) 
UNPIVOT
    (val
        FOR col
            IN (
   <"FIELD-SET n, only the field labels">
            )
    )
)
, ITEM_BASE_VAL AS
(SELECT 
    ID_KEY1, ID_KEY2,
    B_COL, B_VAL, LAST_UPDATE_DATE B_LAST_UPDATE_DATE,
    SEGMENT1
FROM 
    (SELECT INVENTORY_ITEM_ID ID_KEY1, ORGANIZATION_ID ID_KEY2, LAST_UPDATE_DATE, SEGMENT1,
        <"FIELD-SET 1, enlaced with TO_CHAR as necessary">
        FROM MTL_SYSTEM_ITEMS_B MSIB
        WHERE 1=1
        AND EXISTS (SELECT 1 FROM
            MTL_SYSTEM_ITEMS_INTERFACE MSII WHERE 1=1
            AND process_flag NOT IN 7 
            AND MSIB.INVENTORY_ITEM_ID = MSII.INVENTORY_ITEM_ID
            AND MSIB.ORGANIZATION_ID = MSII.ORGANIZATION_ID
            )
    ) 
UNPIVOT INCLUDE NULLS
    (B_VAL
        FOR B_COL
            IN (
   <"FIELD-SET 1, only the field labels">
            )
    )
UNION ALL /*n*/
SELECT 
    ID_KEY1, ID_KEY2,
    B_COL, B_VAL, LAST_UPDATE_DATE B_LAST_UPDATE_DATE,
    SEGMENT1
FROM 
    (SELECT INVENTORY_ITEM_ID ID_KEY1, ORGANIZATION_ID ID_KEY2, LAST_UPDATE_DATE, SEGMENT1,
        <"FIELD-SET n, enlaced with TO_CHAR as necessary">
        FROM MTL_SYSTEM_ITEMS_B MSIB
        WHERE 1=1
        AND EXISTS (SELECT 1 FROM
            MTL_SYSTEM_ITEMS_INTERFACE MSII WHERE 1=1
            AND process_flag NOT IN 7 
            AND MSIB.INVENTORY_ITEM_ID = MSII.INVENTORY_ITEM_ID
            AND MSIB.ORGANIZATION_ID = MSII.ORGANIZATION_ID
            )
    ) 
UNPIVOT INCLUDE NULLS
    (B_VAL
        FOR B_COL
            IN (
   <"FIELD-SET n, only the field labels">
            )
    )
) SELECT
    i.transaction_id,
    i.ID_KEY1,
    i.ID_KEY2,
    i.COL,
    i.VAL,
    i.PROCESS_FLAG,
    b.B_COL,
    b.B_VAL,
    b.SEGMENT1,
    mie.MESSAGE_NAME
FROM 
    ITEM_IFACE_VAL I,
    ITEM_BASE_VAL B,
    inv.mtl_interface_errors mie
WHERE 1=1
AND I.ID_KEY1 = B.ID_KEY1
AND I.ID_KEY2 = B.ID_KEY2
AND I.COL = B.B_COL 
AND i.transaction_id = mie.transaction_id (+)
AND I.VAL != NVL(B.B_VAL,'NULL')


Enabler -
Now, with all these set of ginormous fields and the requirement of breaking them into groups, also the repetitive nature and I hate typing them manually all over, I thought of building myself an enabler. I have kept the below piece 'as-is' of my needs. (play with it)

  SELECT MOD (COLUMN_ID, 7) M,
   CHR (9) || CHR (9) ||
   LISTAGG (
      DECODE (DATA_TYPE,
        'VARCHAR2', COLUMN_NAME,
        'TO_CHAR(' || COLUMN_NAME || ') ' || COLUMN_NAME),
      ', ')
   WITHIN GROUP (ORDER BY COLUMN_ID) || 
   CHR (13)
   || '' COLS,
   CHR (9) || CHR (9) ||
   LISTAGG (COLUMN_NAME, ', ') WITHIN GROUP (ORDER BY COLUMN_ID) ||
   CHR (13)
   || '' COLS_DISP,
   NULL
 FROM ALL_TAB_COLUMNS ATC
   WHERE  TABLE_NAME = 'MTL_SYSTEM_ITEMS_INTERFACE'
   /*exclusion list*/
   AND COLUMN_NAME NOT IN ('PROGRAM_APPLICATION_ID',
         'PROGRAM_ID',
         'REQUEST_ID',
         'ORGANIZATION_ID',
         'PROGRAM_UPDATE_DATE',
         'LAST_UPDATE_DATE',
         'LAST_UPDATED_BY',
         'OBJECT_VERSION_NUMBER',
         'CREATION_DATE',
         'CREATED_BY',
         'LAST_UPDATE_LOGIN',
         'INVENTORY_ITEM_ID',
         'ORGANIZATION_ID')
   AND COLUMN_NAME NOT LIKE 'SEGMENT%'
   AND EXISTS
    (SELECT 1
       FROM ALL_TAB_COLUMNS ATC_B
      WHERE  TABLE_NAME = 'MTL_SYSTEM_ITEMS_B'
      AND ATC_B.COLUMN_NAME = ATC.COLUMN_NAME)
GROUP BY MOD (COLUMN_ID, 7)


Happy New Year.