Create dynamic parent-child hierarchy? - parent-child

do you know a efficient method to generate a parent-child hierarchy from a table without parent node IDs. The parent child relationship should be switchable. All of this should be done in ABAP.
example data:
Color producer weight Airplane(key)
green CompanyA 330 A350
green CompanyA 222 A320
green CompanyB 222 B450
yellow CompanyA 330 H450
I want generate a child parent relationship based on this rows: producer weight Airplane and ignore color. Then i will change it and use: Color weight Airplane and ignore producer
At the end i need a result looks like for "producer weight Airplane"
CompanyA CompanyB
330 222 222
A350 A320 B450
H450
in a internal table it should look like this at the end
ID attribute value H_level parentID
1 producer CompanyA 1
2 weight 330 2 1
3 airplane A350 3 2
.....
does anyone have a good efficient idea?
Thanks a lot.

Here is a simple tree-building algorithm that does what you need.
Note that this algorithm is not optimized in terms of performance. If you manage large amounts of data, you may want to revise some aspects, for example use SORTED or HASHED rather than STANDARD tables to improve the lookup READ TABLEs.
The algorithm is also not refactored for optimum code style. For example, clean code suggests we may want to extract a couple of methods to improve readability.
CLASS hierarchy_builder DEFINITION
PUBLIC FINAL CREATE PUBLIC.
PUBLIC SECTION.
TYPES:
BEGIN OF ts_input,
color TYPE string,
producer TYPE string,
weight TYPE string,
airplane TYPE string,
END OF ts_input.
TYPES tt_input TYPE
STANDARD TABLE OF ts_input
WITH EMPTY KEY.
TYPES tt_attributes TYPE string_table.
TYPES:
BEGIN OF ts_output,
id TYPE string,
attribute TYPE string,
value TYPE string,
level TYPE i,
parent_id TYPE string,
END OF ts_output.
TYPES tt_output
TYPE STANDARD TABLE OF ts_output
WITH EMPTY KEY.
CLASS-METHODS build_hierarchy
IMPORTING
it_data TYPE tt_input
it_hierarchy TYPE tt_attributes
RETURNING
VALUE(rt_result) TYPE tt_output.
ENDCLASS.
CLASS hierarchy_builder IMPLEMENTATION.
METHOD build_hierarchy.
DATA(lv_parent_attribute) = ``.
DATA(lv_next_id) = 1.
LOOP AT it_hierarchy INTO DATA(lv_child_attribute).
DATA(lv_level) = sy-tabix.
LOOP AT it_data INTO DATA(ls_data).
DATA(lv_parent_id) = ``.
ASSIGN COMPONENT lv_child_attribute
OF STRUCTURE ls_data
TO FIELD-SYMBOL(<lv_child_value>).
IF lv_parent_attribute IS NOT INITIAL.
ASSIGN COMPONENT lv_parent_attribute
OF STRUCTURE ls_data
TO FIELD-SYMBOL(<lv_parent_value>).
READ TABLE rt_result
INTO DATA(ls_parent)
WITH KEY
attribute = lv_parent_attribute
value = <lv_parent_value>.
lv_parent_id = ls_parent-id.
ENDIF.
READ TABLE rt_result
TRANSPORTING NO FIELDS
WITH KEY
attribute = lv_child_attribute
value = <lv_child_value>
parent_id = lv_parent_id.
IF sy-subrc <> 0.
INSERT VALUE #(
id = |{ lv_next_id }|
attribute = lv_child_attribute
value = <lv_child_value>
level = lv_level
parent_id = lv_parent_id )
INTO TABLE rt_result.
lv_next_id += 1.
ENDIF.
ENDLOOP.
lv_parent_attribute = lv_child_attribute.
ENDLOOP.
ENDMETHOD.
ENDCLASS.
Verified with the following unit test. It represents the example you gave:
CLASS ltc_unit_tests DEFINITION
FOR TESTING RISK LEVEL HARMLESS DURATION SHORT.
PRIVATE SECTION.
METHODS builds_example FOR TESTING.
ENDCLASS.
CLASS ltc_unit_tests IMPLEMENTATION.
METHOD builds_example.
DATA(lt_data) =
VALUE hierarchy_builder=>tt_input(
( color = 'green' producer = 'CompanyA' weight = '330' airplane = 'A350' )
( color = 'green' producer = 'CompanyA' weight = '222' airplane = 'A320' )
( color = 'green' producer = 'CompanyB' weight = '222' airplane = 'B450' )
( color = 'yellow' producer = 'CompanyA' weight = '330' airplane = 'H450' ) ).
DATA(lt_hierarchy) =
VALUE hierarchy_builder=>tt_attributes(
( `PRODUCER` )
( `WEIGHT` )
( `AIRPLANE` ) ).
DATA(lt_result) =
hierarchy_builder=>build_hierarchy(
it_data = lt_data
it_hierarchy = lt_hierarchy ).
DATA(lt_expected) =
VALUE hierarchy_builder=>tt_output(
( id = '1' attribute = 'PRODUCER' value = 'CompanyA' level = 1 parent_id = '' )
( id = '2' attribute = 'PRODUCER' value = 'CompanyB' level = 1 parent_id = '' )
( id = '3' attribute = 'WEIGHT' value = '330' level = 2 parent_id = '1' )
( id = '4' attribute = 'WEIGHT' value = '222' level = 2 parent_id = '1' )
( id = '5' attribute = 'WEIGHT' value = '222' level = 2 parent_id = '2' )
( id = '6' attribute = 'AIRPLANE' value = 'A350' level = 3 parent_id = '3' )
( id = '7' attribute = 'AIRPLANE' value = 'A320' level = 3 parent_id = '4' )
( id = '8' attribute = 'AIRPLANE' value = 'B450' level = 3 parent_id = '4' )
( id = '9' attribute = 'AIRPLANE' value = 'H450' level = 3 parent_id = '3' ) ).
cl_abap_unit_assert=>assert_equals(
act = lt_result
exp = lt_expected ).
ENDMETHOD.
ENDCLASS.

Related

How can I adapt this Power query recursion to handle multiple Parents in a hierarchy?

I'm trying to adapt the recursion code below, to handle the situation where there can be multiple ParentId for each Id (This is where I found the code on SO).
I see that the List.PositionOf can take an optional parameter with Occurence. All so that instead of returning the position of the Id in the List, I can return a list of the positions of all the matching Ids.
The problem I have is what to do next.
How can I use this list of positions to get the list of ParentId elements in the ParentID_List corresponding to these positions? Or is there a better approach?
let Source = Excel.CurrentWorkbook(){[Name="Table1"]}[Content],
ChangedType = Table.TransformColumnTypes(Source,{{"ID", type text}, {"ParentID", type text}}),
ID_List = List.Buffer( ChangedType[ID] ),
ParentID_List = List.Buffer( ChangedType[ParentID] ),
Type_List = List.Buffer( ChangedType[Type] ),
Highest = (n as text, searchfor as text) as text =>
let
Spot = List.PositionOf( ID_List, n ),
ThisType = Type_List{Spot},
Parent_ID = ParentID_List{Spot}
in if Parent_ID = null or ThisType=searchfor then ID_List{Spot} else #Highest(Parent_ID,searchfor),
FinalTable = Table.AddColumn( ChangedType, "StrategyID", each Highest( [ID],"Strategy" ), type text),
FinalTable2 = Table.AddColumn( FinalTable, "SubstrategyID", each Highest( [ID],"Substrategy" ), type text),
#"Replaced Errors" = Table.ReplaceErrorValues(FinalTable2, {{"SubstrategyID", null}})
in #"Replaced Errors"

unable to fetch department column value in pl-sql developer

what to add so that i can fetch department column value
WHERE C1.CODE_ID(+) = ALLRESULT.BANK_CODE
AND C1.CODE_CATEGORY = 'BANK'
AND C2.CODE_CATEGORY(+) = ALLRESULT.DEPT_ID
AND C2.CODE_CATEGORY(+) = 'DEPARTMENT'
AND C3.CODE_ID(+) = ALLRESULT.PAY_CURRENCY
AND C3.CODE_CATEGORY(+) = 'CURRENCY' ORDER BY ALLRESULT.P_ID,ALLRESULT.PAY_CURRENCY
Reformatted your JPG query version to some more readable format:
SELECT
ALLRESULT.P_ID EMP_ID
,ALLRESULT.EMP_NAME
,NVL(C2.CODE_CATEGORY, 'N/A') DEPARTMENT
,ALLRESULT.BANK_CODE
,NVL(C1.CODE_DESC, 'N/A') BANK_NAME
,ALLRESULT.BANK_ACCT
,ALLRESULT.VALUE
,NVL(C3.CODE_DESC, 'N/A') CURRENCY
,ALLRESULT.ID
FROM
HRIS_CODE_SETUP C1
,HRIS_CODE_SETUP C2
,HRIS_CODE_SETUP C3
,(
SELECT
NVL(PAY_EMP.BANK_ACCNAME, 'NA') AS EMP_NAME
,PAY_EMP.DEPARTMENT DEPT_ID
,NVL(PAY_EMP.BANKNAME, 'NA') BANK_CODE
,NVL(PAY_EMP.BANK_ACCT,'NA') BANK_ACCT
,PAY_EMP.ID
,GROUPSUM.*
FROM
PAY_EMP
,(
SELECT
RESULT.P_ID
,RESULT.COM_ID
-- This can be replaced by more readable CASE statement presented below
-- ,SUM(
-- DECODE(PAYTYPE
-- ,0,LTAROUND((RESULT.VALUE*B.RATE),C.PROUND,C.ACCURACY)
-- ,1,LTAROUND((RESULT.VALUE*B.RATE),C.PROUND,C.ACCURACY)
-- ,2,LTAROUND((RESULT.VALUE*B.RATE),C.PROUND,C.ACCURACY)
-- ,3,LTAROUND((RESULT.VALUE*B.RATE),C.PROUND,C.ACCURACY)
-- ,4,LTAROUND((RESULT.VALUE*B.RATE),C.PROUND,C.ACCURACY)
-- ,5,LTAROUND((RESULT.VALUE*B.RATE),C.PROUND,C.ACCURACY)
-- ,6,LTAROUND((RESULT.VALUE*B.RATE),C.PROUND,C.ACCURACY)
-- ,7,LTAROUND((RESULT.VALUE*B.RATE),C.PROUND,C.ACCURACY)
-- ,12,LTAROUND((RESULT.VALUE*B.RATE),C.PROUND,C.ACCURACY)
-- ,8,-1*LTAROUND((RESULT.VALUE*B.RATE),C.PROUND,C.ACCURACY)
-- ,9,-1*LTAROUND((RESULT.VALUE*B.RATE),C.PROUND,C.ACCURACY)
-- ,10,-1*LTAROUND((RESULT.VALUE*B.RATE),C.PROUND,C.ACCURACY)
-- ,11,-1*LTAROUND((RESULT.VALUE*B.RATE),C.PROUND,C.ACCURACY)
-- )
-- ) VALUE
SUM(LTAROUND((RESULT.VALUE*B.RATE),C.PROUND,C.ACCURACY) * CASE WHEN PAYTYPE IN (0,1,2,3,4,5,6,7,12) THEN 1 ELSE -1 END) VALUE
-- the SUM above is way easier to read and maintain, isn't it?
,RESULT.PAY_CURRENCY
FROM
PAY_RESULT RESULT
,PAY_CURRENCY B
,V_APPLYITEM2 C
,EPAY_COMPANY D
WHERE
B.COMID = RESULT.COMID
AND B.BATCH = RESULT.BATCH
AND RESULT.BATCH IN ()
AND B.PAYYEAR=RESULT.PAYYEAR
AND B.PAYMONTH = RESULT.PAYMONTH
AND C.PAYITEM_ID=RESULT.PAYITEM
AND C.COMP_ID = RESULT.COMID
AND RESULT.COMID = D.PEID
AND RESULT.PAY_CURRENCY = B.C1
AND B.C2 = D.PAY_CURRENCY AND (
(
RESULT.PAYTYPE IN (0,1,2,3,4,5,6,7,12)
AND RESULT.PAY_MODE = 1
AND RESULT.PID IS NOT NULL
OR LOWER(RESULT.PAYITEM) IN ('BASICPAY','BACKPAY')
)
OR (RESULT.PAYTYPE IN (8,9,10,11))
)
AND RESULT.COMID = 'P00000001882-001-SG'
AND RESULT.PAYYEAR = 2015
AND RESULT.PAYMONTH = 9
GROUP BY
RESULT.PID
,RESULT.COMID
,RESULT.PAY_CURRENCY
) GROUPSUM
WHERE
PAY_EMP.PAY_METHOD = 1
AND PAY_EMP.P_ID(+) = GROUPSUM.P_ID
AND PAY_EMP.COMPANY(+) = GROUPSUM.COMID
) ALLRESULT
WHERE
C1.CODE_ID(+) = ALLRESULT.BANK_CODE
AND C1.CODE_CATEGORY = 'BANK'
AND C2.CODE_CATEGORY(+) = ALLRESULT.DEPT_ID
AND C2.CODE_CATEGORY(+) = 'DEPARTMENT'
AND C3.CODE_ID(+) = ALLRESULT.PAY_CURRENCY
AND C3.CODE_CATEGORY(+) = 'CURRENCY'
ORDER BY
ALLRESULT.P_ID
,ALLRESULT.PAY_CURRENCY
Now, please answer, what do you mean by what to add so that i can fetch department column value?
If you mean the below:
SELECT
...
,NVL(C2.CODE_CATEGORY, 'N/A') DEPARTMENT
...
FROM
...
WHERE
...
AND C2.CODE_CATEGORY(+) = ALLRESULT.DEPT_ID
AND C2.CODE_CATEGORY(+) = 'DEPARTMENT'
...
then you must've noticed, you've got OUTER JOIN over here, thus ALLRESULT.DEPT_ID does not have to be in C2 table in CODE_CATEGORY column (ALLRESULT has particular DEPT_ID while C2 doesn't).

Oracle Hierarchical query: with two node attributes NodeId and NodeType

I have the following use case where I want to make use of hierarchical queries to get the desired result.
In my use case I have two types of node say 'A' and 'B'. So the unique identifier of node is its ID and Type.
Now the problem is when two nodes with same Id and different type, when I call connect by clause only on id I get child for other types of nodes also (if id of that node is same).
create table TreeTest (
nodeid integer,
nodetype char(1),
parentid integer,
parenttype char(1)
);
Data in table for this particular use case:
1, 'A', NULL, null
2, 'A', 1, 'A'
3, 'A', 1, 'A'
2, 'B', NULL, null
3, 'B', 2, 'B'
Now I am firing the following query (which is not giving the correct result)
SELECT * FROM TREETEST
START WITH PARENTID = 1
CONNECT BY PRIOR nodeid = PARENTID;
I tried following query also, but again wrong result
SELECT * FROM TREETEST
START WITH PARENTID = 1 AND PARENTTYPE = 'A'
CONNECT BY PRIOR nodeid = PARENTID AND NODETYPE = PARENTTYPE;
Kindly provide the correct query and kindly also explain why second query is not working.
Expected output:
2 A 1 A
3 A 1 A
SELECT *
FROM treeTest
START WITH parentId = 1
AND parentType = 'A'
CONNECT BY PRIOR nodeId = parentId
AND PRIOR nodeType = parentType
;
Explanation: PRIOR is an operator applied to a column name, not to the whole condition.

PL/SQL: Conditional Where

I have the following scenario:
CREATE OR REPLACE PROCEDURE GETINBOX
(
inHasAttachments IN int
)
AS
BEGIN
SELECT M.MailId,
M.SenderId,
E.Emp_Name As "Sender",
MI.RecipientId,
M.Subject
FROM MAIL M INNER JOIN MAILINBOX MI ON M.MailId = MI.MailId
WHERE MI.RecipientId = '547' AND
M.NotificationSelected = 'Y'
IF inHasAttachments = '1' THEN
AND M.Attachments = 'Y'
END IF;
END GETINBOX;
Is it possible to add conditions to the where clause based on the value of a parameter?
WHERE MI.RecipientId = '547' AND
M.NotificationSelected = 'Y'
IF inHasAttachments = '1' THEN
AND M.Attachments = 'Y'
END IF;
Obviously this is not allowed but is it possible to do this in some way in PL/SQL?
I know one way to do it is to duplicate the query and execute a different query based on the value of the parameter but I don't want to duplicate my code.
As I understand your requirements: if the value of parameter inHasAttachments is 1 then you want to filter further by M.Attachments = 'Y', and if its value isn't 1 then you don't care about M.Attachments. This is in addition to the condition MI.RecipientId = '547' AND M.NotificationSelected = 'Y'.
You can do it like this:
SELECT M.MailId,
M.SenderId,
E.Emp_Name As "Sender",
MI.RecipientId,
M.Subject
FROM MAIL M INNER JOIN MAILINBOX MI ON M.MailId = MI.MailId
WHERE MI.RecipientId = '547' AND M.NotificationSelected = 'Y'
AND (inHasAttachments <> '1' OR M.Attachments = 'Y')

Consolidating values from multiple tables

I have an application which has data spread accross 2 tables.
There is a main table Main which has columns - Id , Name, Type.
Now there is a Sub Main table that has columns - MainId(FK), StartDate,Enddate,city
and this is a 1 to many relation (each main can have multiple entries in submain).
Now I want to display columns Main.Id, City( as comma seperated from various rows for that main item from submain), min of start date(from submain for that main item) and max of enddate( from sub main).
I thought of having a function but that will slow things up since there will be 100k records. Is there some other way of doing this. btw the application is in asp.net. Can we have a sql query or some linq kind of thing ?
This is off the top of my head, but firstly I would suggest you create a user defined function in sql to create the city comma separated list string that accepts #mainid, then does the following:
DECLARE #listStr VARCHAR(MAX)
SELECT #listStr = COALESCE(#listStr+',' , '') + city
FROM submain
WHERE mainid = #mainid
... and then return #listStr which will now be a comma separated list of cities. Let's say you call your function MainIDCityStringGet()
Then for your final result you can simply execute the following
select cts.mainid,
cts.cities,
sts.minstartdate,
sts.maxenddate
from ( select distinct mainid,
dbo.MainIDCityStringGet(mainid) as 'cities'
from submain) as cts
join
( select mainid,
min(startdate) as 'minstartdate',
max(enddate) as 'maxenddate'
from submain
group by mainid ) as sts on sts.mainid = cts.mainid
where startdate <is what you want it to be>
and enddate <is what you want it to be>
Depending on how exactly you would like to filter by startdate and enddate you may need to put the where filter within each subquery and in the second subquery in the join you may then need to use the HAVING grouped filter. You did not clearly state the nature of your filter.
I hope that helps.
This will of course be in stored procedure. May need some debugging.
An alternative to creating a stored procedure is performing the complex operations on the client side. (untested):
var result = (from main in context.Main
join sub in context.SubMain on main.Id equals sub.MainId into subs
let StartDate = subs.Min(s => s.StartDate)
let EndDate = subs.Max(s => s.EndDate)
let Cities = subs.Select(s => s.City).Distinct()
select new { main.Id, main.Name, main.Type, StartDate, EndDate, Cities })
.ToList()
.Select(x => new
{
x.Id,
x.Name,
x.Type,
x.StartDate,
x.EndDate,
Cities = string.Join(", ", x.Cities.ToArray())
})
.ToList();
I am unsure how well this is supported in other implimentations of SQL, but if you have SQL Server this works a charm for this type of scenario.
As a disclaimer I would like to add that I am not the originator of this technique. But I immediately thought of this question when I came across it.
Example:
For a table
Item ID Item Value Item Text
----------- ----------------- ---------------
1 2 A
1 2 B
1 6 C
2 2 D
2 4 A
3 7 B
3 1 D
If you want the following output, with the strings concatenated and the value summed.
Item ID Item Value Item Text
----------- ----------------- ---------------
1 10 A, B, C
2 6 D, A
3 8 B, D
The following avoids a multi-statement looping solution:
if object_id('Items') is not null
drop table Items
go
create table Items
( ItemId int identity(1,1),
ItemNo int not null,
ItemValue int not null,
ItemDesc nvarchar(500) )
insert Items
( ItemNo,
ItemValue,
ItemDesc )
values ( 1, 2, 'A'),
( 1, 2, 'B'),
( 1, 6, 'C'),
( 2, 2, 'D'),
( 2, 4, 'A'),
( 3, 7, 'B'),
( 3, 1, 'D')
select it1.ItemNo,
sum(it1.ItemValue) as ItemValues,
stuff((select ', ' + it2.ItemDesc --// Stuff is just used to remove the first 2 characters, instead of a substring.
from Items it2 with (nolock)
where it1.ItemNo = it2.ItemNo
for xml path(''), type).value('.','varchar(max)'), 1, 2, '') as ItemDescs --// Does the actual concatenation..
from Items it1 with (nolock)
group by it1.ItemNo
So you see all you need is a sub query in your select that retrieves a set of all the values you need to concatenate and then use the FOR XML PATH command in that sub query in a clever way. It does not matter where the values you need to concatenate comes from you just need to retrieve them using the sub query.

Resources