Oracle Hierarchical query with multiple nodes - oracle

I have a View and a Table that has the following details:
V_Mgmt
DMId PMId TLId
1 1 1
1 1 2
1 2 3
2 3 4
2 3 5
2 4 6
T_ProjLevels
TLId DevId ParentDevId
1 1 0
1 2 1
1 3 1
2 4 0
2 5 4
2 6 4
2 7 6
3 8 0
3 9 0
4 10 0
4 11 0
4 12 11
Ideally, my tree structure would be as per the left image. But, I am required to create a tree structure as per the right image by skipping the TL's.
So far, I was successfully able to create my tree with only DevId's by using the below query. Need some help in creating this new tree structure.
SELECT DevId,ParentDevId from T_ProjLevels
START WITH ParentDevId=0
Connect By Nocycle Prior "DevId" = "ParentDevId"
ORDER SIBLINGS BY ParentDevId

Look at your data structure. You have one row representing DMID 1, PMID 1, which you want to show as two separate rows in your tree. Somehow, you need to create extra rows in your result set to represent these intermediate nodes. A good way to do that is using GROUPING SETS.
So, what we will do is join the two tables and GROUP BY GROUPING SETS(...) in such a way as to get a merged hierarchy of all the nodes, whether they are DM, PM, or DEV nodes.
Once we have that, we will just do a simple CONNECT BY query on the merged hierarchy.
I am pasting in a working implementation of that plan below, but first let me just say that if you need to do stuff like this often, seriously consider that maybe your data model is not properly designed.
Here is the query, with comments. This version will give you the tree on the right in your post: that is, the one that omits the TL levels. A simple modification would give the tree on the left instead... it was unclear which one you were really after (sorry).
-- First provide data to simulate your V_Mgmt table...
With V_Mgmt ( DMid, PMId, TLId ) AS (
SELECT 1, 1, 1 FROM DUAL UNION ALL
SELECT 1, 1, 2 FROM DUAL UNION ALL
SELECT 1, 2, 3 FROM DUAL UNION ALL
SELECT 2, 3, 4 FROM DUAL UNION ALL
SELECT 2, 3, 5 FROM DUAL UNION ALL
SELECT 2, 4, 6 FROM DUAL ),
-- ... and your T_ProjLevels table
T_ProjLevels (TLId, DevId, ParentDevId) AS (
SELECT 1, 1, 0 FROM DUAL UNION ALL
SELECT 1, 2, 1 FROM DUAL UNION ALL
SELECT 1, 3, 1 FROM DUAL UNION ALL
SELECT 2, 4, 0 FROM DUAL UNION ALL
SELECT 2, 5, 4 FROM DUAL UNION ALL
SELECT 2, 6, 4 FROM DUAL UNION ALL
SELECT 2, 7, 6 FROM DUAL UNION ALL
SELECT 3, 8, 0 FROM DUAL UNION ALL
SELECT 3, 9, 0 FROM DUAL UNION ALL
SELECT 4, 10, 0 FROM DUAL UNION ALL
SELECT 4, 11, 0 FROM DUAL UNION ALL
SELECT 4, 12, 11 FROM DUAL ),
-- Next, merge them together
-- (A) using GROUPING_SETS to create extra rows for the DM, PM, but not the TL-level nodes
-- (B) combining DM, PM, (but not TL), and DEV ids into a common set of "node", "parent_node", and "node_name" columns
merged_hierarchy ( node, parent_node, node_name ) AS (
SELECT rtrim(m.dmid || '.' || m.pmid || '.' || pl.devid,'.') node,
rtrim(
case
when grouping(m.pmid) = 1 then NULL
when grouping(m.tlid) = 1 then to_char(m.dmid)
when grouping(pl.devid) = 1 then m.dmid || '.' || m.pmid
else
m.dmid || '.' || m.pmid || '.' || nullif(pl.parentdevid,0) end,'.') parent_node,
case when grouping(pl.devid) = 0 THEN 'DEV' || pl.devid
when grouping(m.tlid) = 0 THEN 'TL' || m.tlid
when grouping(m.pmid) = 0 THEN 'PM' || m.pmid
when grouping(m.dmid) = 0 THEN 'DM' || m.dmid
end node_name
from v_mgmt m
left join t_projlevels pl on pl.tlid = m.tlid
group by grouping sets ( ( m.dmid ), ( m.dmid, m.pmid), (m.dmid, m.pmid, m.tlid, pl.devid, pl.parentdevid ) )
)
-- Finally, query the merged hierarchy as a straight-forward CONNECT BY query
SELECT lpad(' ',5*(level-1),' ') || node_name output
FROM merged_hierarchy
START WITH parent_node IS NULL
CONNECT BY parent_node = prior node
-- Exclude the outer-joined rows from T_ProjLevels...
AND node_name != 'DEV';
+--------------------------+
| OUTPUT |
+--------------------------+
| DM1 |
| PM1 |
| DEV1 |
| DEV2 |
| DEV3 |
| DEV4 |
| DEV5 |
| DEV6 |
| DEV7 |
| PM2 |
| DEV8 |
| DEV9 |
| DM2 |
| PM3 |
| DEV10 |
| DEV11 |
| DEV12 |
| PM4 |
+--------------------------+

Related

Row data multiplication in Oracle

Consider following table where I am doing row data multiplication:
with v1 (member_id, the_number) as
(
select 1, 3 from dual union all
select 1, 5 from dual union all
select 2, 2 from dual union all
select 2, 3 from dual union all
select 2, 4 from dual union all
select 3, 9 from dual union all
select 3, 3 from dual union all
select 3, 2 from dual
)
select member_id, EXP(SUM(LN(the_number))) from v1
GROUP BY member_id;
It gives the correct result as:
MEMBER_ID EXP(SUM(LN(THE_NUMBER)))
1 15
2 24
3 54
The moment I put a negative value in the the_number column, I get the following Oracle error: ORA-01428: argument 'x' is out of range This is because the range for LN () argument is > 0.
How can I modify the query so that I can have negative values as well in the_number column? I am using Oracle 11g.
Get the product of the absolute values of the numbers and finally multiply by -1 or 1 depending on whether there is an odd or even number of negative numbers:
select
member_id,
CASE WHEN MOD(SUM(CASE WHEN the_number < 0 THEN 1 ELSE 0 END), 2) = 1 THEN -1 ELSE 1 END *
EXP(SUM(LN(ABS(the_number)))) from v1
GROUP BY member_id;
See the demo.

Oracle add group function over result rows

I'm trying to add an aggregate function column to an existing result set. I've tried variations of OVER(), UNION, but cannot find a solution.
Example current result set:
ID ATTR VALUE
1 score 5
1 score 7
1 score 9
Example desired result set:
ID ATTR VALUE STDDEV (score)
1 score 5 2
1 score 7 2
1 score 9 2
Thank you
Seems like you're after:
stddev(value) over (partition by attr)
stddev(value) over (partition by id, attr)
It just depend on what you need to partition by. Based on sample data the attr should be enough; but I could see possibly the ID and attr.
Example:
With CTE (ID, Attr, Value) as (
SELECT 1, 'score', 5 from dual union all
SELECT 1, 'score', 7 from dual union all
SELECT 1, 'score', 9 from dual union all
SELECT 1, 'Z', 1 from dual union all
SELECT 1, 'Z', 5 from dual union all
SELECT 1, 'Z', 8 from dual)
SELECT A.*, stddev(value) over (partition by attr)
FROM cte A
ORDER BY attr, value
DOCS show that by adding an order by to the analytic, one can acquire the cumulative standard deviation per record.
Giving us:
+----+-------+-------+------------------------------------------+
| ID | attr | value | stdev |
+----+-------+-------+------------------------------------------+
| 1 | Z | 1 | 3.51188458428424628280046822063322249225 |
| 1 | Z | 5 | 3.51188458428424628280046822063322249225 |
| 1 | Z | 8 | 3.51188458428424628280046822063322249225 |
| 1 | score | 5 | 2 |
| 1 | score | 7 | 2 |
| 1 | score | 9 | 2 |
+----+-------+-------+------------------------------------------+

Get Hierarchy level and all node references on Oracle

I have been reading about CONNECT BY and CTE in Oracle, but I can't come up with a solution. I don't know how to use properly CONNECT BY to my needs, and recursive CTE's in Oracle are limited to 2 branches(one UNION ALL) and I'm using 3 branches.
In SQL Server it was kind of easy after I found this article. I only added another UNION ALL regarding to return all node references.
What I trying to do is having a hierarchy like this:
Code|Father
1 |NULL
2 |1
3 |2
And this should return me:
Node|Father|Level|JumpsToFather
1 |1 |1 |0
2 |1 |2 |1
2 |2 |2 |0
3 |1 |3 |2
3 |2 |3 |1
3 |3 |3 |0
Note: Yes I need to return a reference to themselves counting as zero jumps on the hierarchy
Here is a solution using a recursive CTE. I used lvl as column header since level is a reserved word in Oracle. You will see other differences in terminology as well. I use "parent" for the immediately higher level and "ancestor" for >= 0 steps (to accommodate your requirement of showing a node as its own ancestor). I used an ORDER BY clause to cause the output to match yours; you may or may not need the rows ordered.
Your question stimulated me to read again, in more detail, about hierarchical queries, to see if this can be done with them instead of recursive CTEs. Actually I already know you can, by using CONNECT_BY_PATH, but using a substr on that just to retrieve the top level in a hierarchical path is not satisfying at all, there must be a better way. (If that was the only way to do it with hierarchical queries, I would definitely go the recursive CTE route if it was available). I will add the hierarchical query solution here, if I can find a good one.
with h ( node, parent ) as (
select 1 , null from dual union all
select 2 , 1 from dual union all
select 3 , 2 from dual
),
r ( node , ancestor, steps ) as (
select node , node , 0
from h
union all
select r.node, h.parent, steps + 1
from h join r
on h.node = r.ancestor
)
select node, ancestor,
1+ (max(steps) over (partition by node)) as lvl, steps
from r
where ancestor is not null
order by lvl, steps desc;
NODE ANCESTOR LVL STEPS
---------- ---------- ---------- ----------
1 1 1 0
2 1 2 1
2 2 2 0
3 1 3 2
3 2 3 1
3 3 3 0
Added: Hierarchical query solution
OK - found it. Please test both solutions to see which performs better; from tests on a different setup, recursive CTE was quite a bit faster than hierarchical query, but that may depend on the specific situation. ALSO: recursive CTE works only in Oracle 11.2 and above; the hierarchical solution works with older versions.
I added a bit more test data to match Anatoliy's.
with h ( node, parent ) as (
select 1 , null from dual union all
select 2 , 1 from dual union all
select 3 , 2 from dual union all
select 4 , 2 from dual union all
select 5 , 4 from dual
)
select node,
connect_by_root node as ancestor,
max(level) over (partition by node) as lvl,
level - 1 as steps
from h
connect by parent = prior node
order by node, ancestor;
NODE ANCESTOR LVL STEPS
---------- ---------- ---------- ----------
1 1 1 0
2 1 2 1
2 2 2 0
3 1 3 2
3 2 3 1
3 3 3 0
4 1 3 2
4 2 3 1
4 4 3 0
5 1 4 3
5 2 4 2
5 4 4 1
5 5 4 0
thx for question, i spent 1 hour to write this:
with t as ( select code, parent, level l
from (select 1 as code, NULL as parent from dual union
select 2 , 1 from dual union
select 3 , 2 from dual
-- add some more data for demo case
union
select 4 , 2 from dual union
select 5 , 4 from dual
)
start with parent is null
connect by prior code = parent )
select code, (select code
from t t1
where l = ll
and rownum = 1
start with t1.code = main_t.code
connect by prior t1.parent = t1.code
) parent,
l code_level,
jumps
from (
select distinct t.*, l-level jumps, level ll
from t
connect by level <= l
) main_t
order by code, parent
as you can see, i'am add some more data to test my sql, here is output
CODE PARENT CODE_LEVEL JUMPS
---------- ---------- ---------- ----------
1 1 1 0
2 1 2 1
2 2 2 0
3 1 3 2
3 2 3 1
3 3 3 0
4 1 3 2
4 2 3 1
4 4 3 0
5 1 4 3
5 2 4 2
5 4 4 1
5 5 4 0
13 rows selected

Select an included values

I'm using Oracle SQL and i need help with a query. Hope it's not too much easy one. I did't find an answer for it in Google.
I have a table that need to be aggregated by ID column and then to select only the records that two values are included in a certain table (and both of them).
Table for example
ID | Value
1 | Y
1 | N
2 | N
2 | N
2 | Y
3 | Y
3 | Y
4 | Y
5 | Y
5 | N
5 | Y
5 | N
The output table need to include only the IDs that both Y and N are included in Value table. Output:
ID
1
2
5
Another solution that groups by the ID and uses HAVING to return only those with > 1 DISTINCT values:
with v_data(id, value) as (
select 1, 'Y' from dual union all
select 1, 'N' from dual union all
select 2, 'Y' from dual)
select id
from v_data
group by id
having count(distinct value) > 1
select distinct a.id
from your_table a inner join your_table b on a.id = b.id and a.value != b.value;

update row without considering space between two words

I am looking to replace text in column Text with format(A,B) where text contains A and B only and ignoring spaces between A and B.
Here is test data
Id Text
1 A B //should be replaced with format(A,B).
2 A B //should be replaced with format(A,B).
3 A B //should be replaced with format(A,B).
4 A 1 B //shouldn't be replaced.
5 A B 1 //should be replaced with format(A,B) 1.
I think I have to do something like
UPDATE test SET text = REPLACE(text, 'A[wild char for space]B', 'format(A,B)');
but how should I compare only for space? like % will compare everything.
You can use Oracle Regex fro this
UPDATE test SET text = REGEXP_REPLACE(testcol, '(A[:blank:]B)', 'format(A,B)')
Oracle Regex
Just a hint:
SQL> with t as (
2 select 'A B' col from dual union all
3 select 'A B' from dual union all
4 select 'A B' from dual union all
5 select 'AB' from dual union all
6 select 'A 1 B' from dual union all
7 select 'A B 1' from dual
8 )
9 select regexp_replace(t.col, 'A[[:space:]]*B', 'format(A,B)') from t
10 /
REGEXP_REPLACE(T.COL,'A[[:SPACE:]]*B','FORMAT(A,B)')
--------------------------------------------------------------------------------
format(A,B)
format(A,B)
format(A,B)
format(A,B)
A 1 B
format(A,B) 1
Sql for your scenario:
with tab(Id,Text) as
(select 1,'A B' from dual union all -- //should be replaced with format(A,B).
select 2,'B C' from dual union all -- //should be replaced with format(A,B).
select 3,'A B' from dual union all -- //should be replaced with format(A,B).
select 4,'A 2 B' from dual union all -- //shouldn't be replaced.
select 5,'Y Z 1' from dual) -- //should be replaced with format(A,B) 1.
select id,
text,
regexp_replace(text, '^\D\s+\D', 'format('||regexp_substr(text, '\D')||','||trim(regexp_substr(text, '\D+', 2))||')') format
from tab;
output:
| ID | TEXT | FORMAT |
|----|-------------|---------------|
| 1 | A B | format(A,B) |
| 2 | B C | format(B,C) |
| 3 | A B | format(A,B) |
| 4 | A 2 B | A 2 B |
| 5 | Y Z 1 | format(Y,Z) 1 |
And the update statement becomes
UPDATE test SET text = regexp_replace(text, '^\D\s+\D', 'format('||regexp_substr(text, '\D')||','||trim(regexp_substr(text, '\D+', 2))||')');

Resources