sql nested query - group results by parent - sql

I have the need to return tree like results from a single table. At the moment im using connect by and start with to ensure that all the correct results are returned.
select id,parent_id
from myTable
connect by prior id = parent_id start with name = 'manager'
group by id, parent_id
order by parent_id asc
However i want the results to return in tree structure. So each time a parent row is found its children rows will be displayed directly underneath it. Then move onto next parent and do the same
Expected results
- Parent A
- child a
- child b
- Parent B
- child c
- child d
Actual results
- Parent A
- Parent B
- child a
- child b
- child c
- child d
Is it possible to do this in oracle? My table uses a parent_id field to identify when a row has a parent. Every row also has a sort order, which is the order it should be sorted under its parent and a unique Id.
I'm using an Oracle DB

What you want is to use ORDER SIBLINGS BY. The query you have is ordering by the parent_id column which is overriding any hierarchical ordering.
The query below should do what you need it to do:
with my_hierarchy_data as (
select 1 as id, null as parent_id, 'Manager' as name from dual union all
select 2 as id, 1 as parent_id, 'parent 1' as name from dual union all
select 3 as id, 1 as parent_id, 'parent 2' as name from dual union all
select 4 as id, 2 as parent_id, 'child 1' as name from dual union all
select 5 as id, 2 as parent_id, 'child 2' as name from dual union all
select 6 as id, 3 as parent_id, 'child 3' as name from dual union all
select 7 as id, 3 as parent_id, 'child 4' as name from dual
)
select id, parent_id, lpad('- ', level * 2, ' ') || name as name
from my_hierarchy_data
connect by prior id = parent_id
start with name= 'Manager'
order siblings by parent_id asc

There is a special value level that can be used in Oracle hierarchical queries. It returns 1 for rows at the top level of the hierarchy, 2 for their children, and so on. You can use this to create indentation like this:
select lpad('- ',level*2,' ') || name
from myTable
connect by prior id = parent_id start with name = 'manager'
group by id, parent_id

Related

Sort strings/words alphabetically separated by comma within a column in SQL (entire column)

Lets say that I have a table the following data:
(there are a 1000+ more rows like this)
Bird
----------------------------
Sparrow, Eagle, Crow
Woodpecker, Sparrow
Crow, Eagle
etc. etc.
I want the final column to be sorted out alphabetically. Something like this:
Bird
--------------------
Crow, Eagle, Sparrow
Sparrow, Woodpecker
Crow, Eagle
etc. etc.
Need to know a SQL query that can do that. Possibly SQL Developer.
Here is an Oracle solution using Common Table Expressions (CTEs) to break the problem down. Not sure if this will help, but maybe it will give you an idea or a starting point that you can apply to your environment.
SQL> -- Set up original data set
SQL> with bird_tbl(id, unsorted_list) as (
select 1, 'Sparrow, Eagle, Crow' from dual union all
select 2, 'Woodpecker, Sparrow' from dual union all
select 3, 'Crow, Eagle' from dual
),
-- Split the list into a row for each element
split_tbl(id, bird) as (
select id, regexp_substr(unsorted_list, '(.*?)(, |$)', 1, level, null, 1)
from bird_tbl
connect by level <= regexp_count(unsorted_list, ', ')+1
and prior id = id
and prior sys_guid() is not null
)
-- select * from split_tbl;
-- Rebuild the sorted row
select id, listagg(bird, ', ')
within group (order by bird) sorted_list
from split_tbl
group by id;
ID SORTED_LIST
---------- --------------------
1 Crow, Eagle, Sparrow
2 Sparrow, Woodpecker
3 Crow, Eagle
EDIT: Here's how to apply to your situation. Just replace <your_primary_key> with the primary key column name, <your_column_name> with the name of the column that contains the unsorted list and <your_table_name> with the name of the table.
with split_tbl(<your_primary_key>, <your_column_name>) as (
select <your_primary_key>, regexp_substr(<your_column_name>, '(.*?)(, |$)', 1, level, null, 1)
from <your_table_name>
connect by level <= regexp_count(<your_column_name>, ', ')+1
and prior <your_primary_key> = <your_primary_key>
and prior sys_guid() is not null
)
-- select * from split_tbl;
-- Rebuild the sorted row
select <your_primary_key>, listagg(<your_column_name>, ', ')
within group (order by <your_column_name>) sorted_list
from split_tbl
group by <your_primary_key>;

PRIOR in SELECT list

I can't understand what it adds to the result of the query. From the book that I'm learning:
If you prefix a column name with PRIOR in the
select list (SELECT PRIOR EMPLOYEE_ID, ...), you specify the “prior” row’s value.
SELECT PRIOR EMPLOYEE_ID, MANAGER_ID, LPAD(' ', LEVEL * 2) || EMPLOYEES.JOB_ID
FROM EMPLOYEES
START WITH EMPLOYEE_ID = 100
CONNECT BY PRIOR EMPLOYEE_ID = MANAGER_ID;
The only difference I see, is that it adds a NULL value in the first row and increments IDs of employees by 1.
PRIOR just takes a record from a previous record in the traversed hierarchy.
I think the best way to undestand how it works is to play with a simple hierarchy:
create table qwerty(
id int,
name varchar2(100),
parent_id int
);
insert all
into qwerty values( 1, 'Grandfather', null )
into qwerty values( 2, 'Father', 1 )
into qwerty values( 3, 'Son', 2 )
into qwerty values( 4, 'Grandson', 3 )
select 1234 from dual;
The below query traverses the above hierarchy:
select level, t.*
from qwerty t
start with name = 'Grandfather'
connect by prior id = parent_id
LEVEL ID NAME PARENT_ID
---------- ---------- -------------------- ----------
1 1 Grandfather
2 2 Father 1
3 3 Son 2
4 4 Grandson 3
If we add "PRIOR name" to the above query, then the name of "parent" is displayed. This vaue is taken from prevoius record in the hierarchy (from LEVEL-1)
select level, prior name as parent_name, t.*
from qwerty t
start with name = 'Grandfather'
connect by prior id = parent_id;
LEVEL PARENT_NAME ID NAME PARENT_ID
---------- -------------------- ---------- -------------------- ----------
1 1 Grandfather
2 Grandfather 2 Father 1
3 Father 3 Son 2
4 Son 4 Grandson 3
PRIOR operator returns previous value in a hierarchy build using CONNECT BY clause.
WITH hierarchy(id, parent_id, value) AS (
SELECT 1, NULL, 'root' FROM dual UNION ALL
SELECT 2, 1, 'child 1' FROM dual UNION ALL
SELECT 3, 1, 'child 2' FROM dual UNION ALL
SELECT 4, 3, 'grand child 1' FROM dual
)
SELECT
hierarchy.*, LEVEL depth, PRIOR value
FROM
hierarchy
START WITH
parent_id IS NULL
CONNECT BY
PRIOR id = parent_id
This simple query connects the rows from root to leafs. The PRIORVALUE column returns value of VALUE column of row's parent row (predecessor within the hierarchy), so 'grand child 1' parent is 'child 2' or 'child 1' parent is 'root'. 'root', the first row within the hierarchy (LEVEL = 1) doesn't have any parent therefore PRIOR returns NULL.
If you connect the hierarchy in opposite direction, from a leaf to the root, the PRIOR operator will return child row that was used to connect the row you're looking at.
The LEVEL column shows the depth of specific row within the hierarchy.

Oracle Sql. Recursive select with join

I have a table with applications and table with messages. Applications has hierarchical structure, e.g each application has a parent. And I have a table with messages. Each message has a key and an application id. I want to be able to select message by it's key. If it's found for current application then return it, if no then try to find it with parent id.
Apps table:
id | name | parentId
--------------------
1 |parent| NULL
2 |child | 1
Msg table:
key | text | app
-------------------------------------------------
overriden.system.msg | some text | 1
parent.msg | parent txt | 1
overriden.system.msg | overriden text | 2
So if I'm in the child app(2) on key overriden.system.msg I want to get overriden text. On key parent.msg I want to get parent txt. I know that it must be done with cte, but I have very little expirience with sql and cte is mind-blowing for me right now. Could you please provide working query for this situation? Or maybe you have a better vision how to achieve such functionality without recursion?
The above should work fine:
with app_tree (id, app, lvl) as
( select id , parentID , 0
from Apps
where id = 2
union all
select t.app, a.parentID, t.lvl + 1 from
Apps a
join app_tree t
on t.app = a.id ),
all_msg as (
select key, text,
row_number() over ( partition by key order by lvl) overrideLevel
from app_tree a
join msg m
on m.app = a.id )
select * from all_msg where
overrideLevel =1
It returns:
KEY TEXT
-------------------------------------
overriden.system.msg overriden text
parent.msg parent txt
First it gets the list of all applications for specified id using recursive query with parentid. After that it gets a list of all functions for all applications and generates increasing numbers for the same keys basing on the level. At the end it just takes the first possible level and ignore all parent's levels for the same key.
Mb it will help you:
with app (id, name, parent_id) as
(select 1, 'parent', null from dual union all
select 2, 'child', 1 from dual)
,msg (key, text, app) as
(select 'overriden.system.msg', 'some text', 1 from dual union all
select 'parent.msg', 'parent txt', 1 from dual union all
select 'overriden.system.msg', 'overriden text', 2 from dual)
select key
,max(text) keep (dense_rank last order by nvl2(text,level,0)) msg
from
(select *
from app a
join msg m on (a.id = m.app)) v
start with v.parent_id is null
connect by prior v.id = v.parent_id and prior v.key = v.key
group by key

Algorithm behind Nested comments with SQL

I want to know the algorithm behind child nested records are displayed within parent nested records for example
Comment 1 (parent record)
reply 1 (child record)
reply 2 (child record)
reply 3 (child record)
view all
Comment 2 (parent record)
reply 1 (child record)
reply 2 (child record)
reply 3 (child record)
view all
how is a query written to get the above results?
You can use a Recursive Common Table Expression like the one below
;WITH comments AS
(
SELECT 1 as ID,'Comment 1' detail,NULL AS ParentID
UNION ALL SELECT 2 as ID,'Comment 2',NULL AS ParentID
UNION ALL SELECT 3 as ID,'Reply 1',1 AS ParentID
UNION ALL SELECT 4 as ID,'Reply 2',3 AS ParentID
UNION ALL SELECT 5 as ID,'Reply 3',4 AS ParentID
UNION ALL SELECT 6 as ID,'Reply 4',2 AS ParentID
UNION ALL SELECT 7 as ID,'Reply 5',6 AS ParentID
),comment_hierarchy AS
(
SELECT ID,detail,ID AS prid,0 AS orderid
FROM comments
WHERE ParentID IS NULL
UNION ALL
SELECT c.ID,c.detail ,ch.prid as prid,ch.orderid + 1
FROM comments c
INNER JOIN comment_hierarchy ch
ON c.ParentID = ch.ID
)
SELECT ID,Detail
FROM comment_hierarchy
ORDER BY prid,orderid asc
For more info refer
https://technet.microsoft.com/en-us/library/ms186243%28v=sql.105%29.aspx
CTE to get all children (descendants) of a parent

Oracle CONNECT BY recursive child to parent query, include ultimate parent that self references

In the following example
id parent_id
A A
B A
C B
select id, parent_id
from table
start with id = 'A'
connect by nocycle parent_id = prior id
I get
A A
B A
C B
In my database I have millions of rows in the table and deep and wide hierarchies and I'm not interested in all children. I can derive the children I'm interested in. So I want to turn the query on its head and supply START WITH with the children ids. I then want to output the parent recursively until I get to the top. In my case the top is where the id and parent_id are equal. This is what I'm trying, but I can't get it to show the top level parent.
select id, parent_id
from table
START WITH id = 'C'
CONNECT BY nocycle id = PRIOR parent_id
This gives me
C B
B A
It's not outputting the A A. Is it possible to do this? What I'm hoping to do is not show the parent_id as a separate column in the output, but just show the name relating to the id. The hierarchy is then implied by the order.
I got that result by using WITH clause.
WITH REC_TABLE ( ID, PARENT_ID)
AS
(
--Start WITH
SELECT ID, PARENT_ID
FROM table
WHERE ID='C'
UNION ALL
--Recursive Block
SELECT T.ID, T.PARENT_ID
FROM table T
JOIN REC_TABLE R
ON R.PARENT_ID=T.ID
AND R.PARENT_ID!=R.ID --NoCycle rule
)
SELECT *
FROM REC_TABLE;
And it seems to work that way too.
select id, parent_id
from T
START WITH id = 'C'
CONNECT BY id = PRIOR parent_id and parent_id!= prior id;
-- ^^^^^^^^^^^^^^^^^^^^
-- break cycles
Hope it helps.