Oracle hierarchical path as separate row - sql

I have a table like below:
ID PARENT_ID
--- ----------
1 null
2 1
3 2
4 2
5 4
6 4
7 1
8 7
9 1
10 9
11 10
12 9
13 null
14 13
and I want query for get result like this:
ID | PARENT_ID
-----+-----------
1 | 1
2 | 1
2 | 2
3 | 1
3 | 2
3 | 3
4 | 1
4 | 2
4 | 4
5 | 1
5 | 2
5 | 4
5 | 5
...
I used Oracle connected by root and get this result:
ID PATH
--- -----------
1 1
2 1-2
3 1-2-3
4 1-2-4
5 1-2-4-5
....
but it's not that I want.
Is there another way to get result without connected by root (prefer in standard SQL), that get same result from table?
Can anyone help me?
Note: I use an Oracle database
Thanks

The first step is to make a query that will produce for you all the parents above a specific node.
Here is a example of such a query:
select * from
(SELECT parent_id
FROM test
START WITH ID = 4
CONNECT BY ID = PRIOR PARENT_ID) temp
where parent_id is not null
UNION
select ID from test where ID = 4 ;
In the above case, we start with Node 4.
The next step is to use this query, with another query to get the results for all nodes.
(Will produce this soon)
final solution
create table test(
id int,
parent_id int
);
insert into test values (1, null);
insert into test values (2,1);
insert into test values (3,2);
insert into test values (4,2);
select distinct ID, parent_id from
(
SELECT a.parent_id as aParent, b.parent_id as bParent, b.id as ID, a.id as parent_id
FROM test a, test b
START WITH a.ID = b.id
CONNECT BY a.ID = PRIOR a.PARENT_ID
) temp
where not (aParent is not null AND bParent is null)
order by id, parent_id;
Optimized
SELECT distinct b.id as ID, a.id as parent_id
FROM test a, test b
where not (a.parent_id is not null and b.parent_id is null )
START WITH a.ID = b.id
CONNECT BY a.ID = PRIOR a.PARENT_ID order by id, parent_id;;
Boolean Algebra Simplification
SELECT distinct
findNodesAboveMe.id as ID,
pathFollowing.id as parent_id
FROM
test pathFollowing,
test findNodesAboveMe
where
pathFollowing.parent_id is null
OR findNodesAboveMe.parent_id is not null START WITH pathFollowing.ID = findNodesAboveMe.id CONNECT BY pathFollowing.ID = PRIOR pathFollowing.PARENT_ID
order by
id,
parent_id;
Fix for Null parents
select id, parent_id from
(
(SELECT DISTINCT
findNodesAboveMe.id AS ID,
CASE WHEN pathFollowing.parent_id IS NULL
THEN pathFollowing.id
ELSE pathFollowing.parent_id END AS parent_id
FROM
test pathFollowing,
test findNodesAboveMe
WHERE
findNodesAboveMe.parent_id IS NOT NULL
START WITH pathFollowing.ID = findNodesAboveMe.id CONNECT BY pathFollowing.ID = PRIOR pathFollowing.PARENT_ID
)
UNION
SELECT
id,
id AS parent_id
FROM test
) order by id, parent_id
;
Update
select
distinct bid as ID, aid as parent_id
from
(
SELECT DISTINCT
a.id as aid,
a.parent_id as aparentid,
b.id as bid,
b.parent_id as bparentid,
ltrim(sys_connect_by_path(a.id, ','), ',') AS pth
FROM test a, test b
WHERE NOT
(a.parent_id IS NOT NULL AND b.parent_id IS NULL)
START WITH a.ID = b.id
CONNECT BY a.ID = PRIOR a.PARENT_ID
) temp
where ( pth like bid or pth like bid || ','|| bparentid || '%' )
order by ID, parent_id;

Related

Recursive query with parent-child relation

I am trying to make a recursive query in SQL Server, that display data hierarchically. Here is the structure of the table
[id] [int] IDENTITY(1,1) NOT NULL,
[name] [varchar(100)] NOT NULL,
[Parent_Id] [int] NULL,
Each product has a parent. The column Parent_Id content the id of the parent. The parent_id is null for root products.
I want to make a sql query that display products hierarchically. The following image is an example of how the products could be organized.
Products can have products childs.
For the picture above, the query result should be like the following :
id name parent_id
1 P1 NULL
2 P2 NULL
3 P2-1 2
4 P2-2 2
5 P2-3 2
6 P2-3-1 5
7 P2-3-2 5
8 P3 NULL
9 P3-1 8
Here is the request I wrote to achieve it :
with tree as (select * from products
union all
select * from tree where parent_id = tree.id
)
select * from tree;
But I get a result similar to the following:
1 P1 NULL
2 P2 NULL
8 P3 NULL
3 P2-1 2
4 P2-2 2
5 P2-3 2
9 P3-1 8
6 P2-3-1 5
7 P2-3-2 5
What I want is to group each products sibling so that each product is displayed under its direct parent.
Just another option using the data type hierarchyid
There are some additional features and functions associated with hierarchyid
Example
-- Optional See 1st WHERE
Declare #Top int = null --<< Sets top of Hier Try 2
;with cteP as (
Select ID
,parent_id
,Name
,HierID = convert(hierarchyid,concat('/',ID,'/'))
From YourTable
Where IsNull(#Top,-1) = case when #Top is null then isnull(parent_id ,-1) else ID end
--Where parent_id is null -- Use this where if you always want the full hierarchy
Union All
Select ID = r.ID
,parent_id = r.parent_id
,Name = r.Name
,HierID = convert(hierarchyid,concat(p.HierID.ToString(),r.ID,'/'))
From YourTable r
Join cteP p on r.parent_id = p.ID)
Select Lvl = HierID.GetLevel()
,ID
,parent_id
,Name
From cteP A
Order By A.HierID
Results
Lvl ID parent_id Name
1 1 NULL P1
1 2 NULL P2
2 3 2 P2-1
2 4 2 P2-2
2 5 2 P2-3
3 6 5 P2-3-1
3 7 5 P2-3-2
1 8 NULL P3
2 9 8 P3-1
Just for fun, If I set #Top to 2, the results would be
Lvl ID parent_id Name
1 2 NULL P2
2 3 2 P2-1
2 4 2 P2-2
2 5 2 P2-3
3 6 5 P2-3-1
3 7 5 P2-3-2
Construct the path in the recursive query. The following does this as a string with fixed length ids:
with tree as (
select p.id, p.name, p.parentid,
format(p.parentid, '0000') as path
from products p
where p.parentid is null
union all
select p.id, p.name, p.parentid,
concat(cte.path, '->', format(p.id, '0000')
from tree join
products p
where p.parent_id = t.id
)
select *
from tree;
If I understand this correct and you have the result you want but just unordered, you should be able to just order the result by name.
with tree as (select * from products
union all
select * from tree where parent_id = tree.id
)
select * from tree order by name asc;

Oracle hierarchical queries data

The link gives a good example for overview on how to use Oracle hierarchical queries. I was trying to generate the below combination of data with the example table tab1 given in the link but struck for a while.Using Oracle version 12.2
PARENT_ID CHILD_ID
-------- ---------
1 2 # the root node and id combination (This is evident by using ROOT_ID,ID)
1 3
1 4
. .
. .
2 5 # How to generate below combination in a select statement ont TAB1 table.
2 6
. .
. .
9 11
SELECT id,
parent_id,
RPAD('.', (level-1)*2, '.') || id AS tree,
level,
CONNECT_BY_ROOT id AS root_id,
LTRIM(SYS_CONNECT_BY_PATH(id, '-'), '-') AS path,
CONNECT_BY_ISLEAF AS leaf
FROM tab1
START WITH parent_id IS NULL
CONNECT BY parent_id = PRIOR id
ORDER SIBLINGS BY id;
Output of the above select statement
ID PARENT_ID TREE LEVEL ROOT_ID PATH LEAF
---------- ---------- -------------------- ---------- ---------- -------------------- ----------
1 1 1 1 1 0
2 1 ..2 2 1 1-2 0
3 2 ....3 3 1 1-2-3 1
4 2 ....4 3 1 1-2-4 0
5 4 ......5 4 1 1-2-4-5 1
6 4 ......6 4 1 1-2-4-6 1
7 1 ..7 2 1 1-7 0
8 7 ....8 3 1 1-7-8 1
9 1 ..9 2 1 1-9 0
10 9 ....10 3 1 1-9-10 0
11 10 ......11 4 1 1-9-10-11 1
12 9 ....12 3 1 1-9-12 1
Diagraph:
The sql query to get the desired output are below:
Solution 1: using CTE
with h ( id, parent_id ) as (
select id ,parent_id from tab1
),
r ( id , parent_id, steps ) as (
select id , id , 0
from h
union all
select r.id, h.parent_id, steps + 1
from h join r
on h.id = r.parent_id
)
select parent_id, id
from r
where parent_id != id
order by parent_id asc;
Solution 2: Using Oracle only connect by query. The CONNECT BY NOCYCLE clause can be used to not traverse cyclical hierarchies if any.
with hier_data as
(
select
connect_by_root id as parent_id
,id
from tab1
connect by parent_id = prior id
order by parent_id,id
)
select * from hier_data
where
parent_id != id;

get the max child job record in the same table

I have the below Job table where in we have 5 columns.
consider Job_id is primary key and parent_job_id will be null for the parent job and I have given the number values for the DATE column values
for convenience..
pls consider the below table data
JOB_ID ABC XYZ DATE PARENT_JOB_ID
1 a b 1 null
2 a b 1 1
3 a b 2 1
7 a b 3 1
4 a b 3 null
5 a b 1 4
6 a b 2 4
In this, I need to query in such a way that it should give me the below results,
JOB_ID ABC XYZ PQR PARENT_JOB_ID
1 a b 1 null
7 a b 3 1
4 a b 3 null
6 a b 2 4
they are the parent jobs + the latest child jobs only ( where the DATE value is max)
Please help/advise.
thank you
If your database supports row_number() analytic function, then one way is to get
the latest child jobs only ( where the DATE value is max)
using row_number() function.
then get parent jobs separately (... where PARENT_JOB_ID is null)
and then UNION ALL results from both queries.
Something like:
select * from (
select JOB_ID, ABC, XYZ, DATE, PARENT_JOB_ID from (
select *, row_number() over(partition by PARENT_JOB_ID order by DATE desc) rn
from your_table
where PARENT_JOB_ID is not null
) t
where rn = 1
union all
select JOB_ID, ABC, XYZ, DATE, PARENT_JOB_ID
from your_table
where PARENT_JOB_ID is null
) t
order by coalesce(PARENT_JOB_ID, JOB_ID) , JOB_ID

SQL update all records except the last one with a value

I need to make a query where only the last line of each user that has a car gets a license plate number.
ID UserId LicensePlate HasCar
1 1 ABC123 1
2 1 ABC123 1
3 2 NULL 0
4 3 UVW789 1
5 3 UVW789 1
Should become:
ID UserId LicensePlate HasCar
1 1 NULL 1
2 1 ABC123 1
3 2 NULL 0
4 3 NULL 1
5 3 UVW789 1
So I basically need to find all users with a licenseplate and change all but the last one and make the LicensePlate NULL
Assuming the ID column is an identity column so it can provide the ordering, something like this should do the trick:
;WITH CTE AS
(
SELECT Id,
UserId,
LicensePlate,
ROW_NUMBER() OVER(PARTITION BY UserId ORDER BY Id DESC) rn
FROM Table
WHERE HasCar = 1
)
UPDATE CTE
SET LicensePlate = NULL
WHERE rn > 1
You can try this
UPDATE l
SET l.LicensePlate = null
FROM Car l
INNER JOIN (SELECT UserId, Max(Id) AS max_id
FROM Car
GROUP BY UserId) m ON m.UserId = l.UserId
AND m.max_id <> l.id
You can do it with a join on the table itself like that :
UPDATE car c
INNER JOIN car c2 ON c.userId = c2.userId AND c.id < c2.id AND c.HasCar = 1 AND c2.HasCar = 1
SET c.LicensePlate = NULL
The condition c.id < c2.id will avoid to select the last line
By using LAG Function also you can achieve it.
;WITH License(ID,UserId,LicensePlate,HasCar)
as
(
SELECT 1,1,'ABC123',1 UNION ALL
SELECT 2,1,'ABC123',1 UNION ALL
SELECT 3,2,NULL ,0 UNION ALL
SELECT 4,3,'UVW789',1 UNION ALL
SELECT 5,3,'UVW789',1
)
SELECT ID,UserId,LAG(LicensePlate,1,NULL) OVER(PARTITION BY UserId ORDER BY LicensePlate),HasCar FROM License

Enumerating rows in a inner join

My table
id name num
1 a 3
2 b 4
I need to return every row num number of times. I do it this way.
select DB.BAN_KEY as BAN_KEY, DB.CUST_FULLNAME as CUST_FULLNAME
from TST_DIM_BAN_SELECTED DB
inner join (select rownum rn from dual connect by level < 10) a
on a.rn <= DB.N
There resulting table looks like this.
id name
1 a
1 a
1 a
2 b
2 b
2 b
2 b
But I also need every row in the group to be numbered like this.
id name row_num
1 a 1
1 a 2
1 a 3
2 b 1
2 b 2
2 b 3
2 b 4
How can I do it?
You don't need an inner join to a dummy table or an analytic function to generate the row numbers; you could just use connect by (and its corresponding level function) on the table itself, like so:
WITH tst_dim_ban_selected AS (SELECT 1 ban_key, 'a' cust_fullname, 3 n FROM dual UNION ALL
SELECT 2 ban_key, 'b' cust_fullname, 4 n FROM dual)
-- end of mimicking your table with data in it. See SQL below
SELECT db.ban_key,
db.cust_fullname,
LEVEL row_num
FROM tst_dim_ban_selected db
CONNECT BY LEVEL <= db.n
AND PRIOR db.ban_key = db.ban_key -- assuming this is the primary key
AND PRIOR sys_guid() IS NOT NULL;
BAN_KEY CUST_FULLNAME ROW_NUM
---------- ------------- ----------
1 a 1
1 a 2
1 a 3
2 b 1
2 b 2
2 b 3
2 b 4
If you have other columns than ban_key in the table's primary key, you need to make sure they are included in the connect by clause's list of prior <column> = <column>s. This is so the connect by can identify each row uniquely, meaning that it's looping just over that row and no others. The PRIOR sys_guid() IS NOT NULL is required to prevent connect by loops from occurring.
You can use analytic function for this:
Select id, name,
row_number() over (partition by id, name order by id, name)
From(/* your query */) t;
This can be done without subquery:
Select id, name,
row_number() over (partition by id, name order by id, name)
From /* joins */
You could use this:
SELECT db.ban_key AS ban_key, db.cust_fullname AS cust_fullname,
ROW_NUMBER() OVER (PARTITION BY db.n ORDER BY db.ban_key) AS row_num
FROM tst_dim_ban_selected db
INNER JOIN (SELECT rownum rn FROM dual CONNECT BY level < 10) a
ON a.rn <= db.n;
Use a recursive sub-query factoring clause:
WITH split ( id, name, rn, n ) AS (
SELECT BAN_KEY, CUST_FULLNAME, 1, N
FROM TST_DIM_BAN_SELECTED
UNION ALL
SELECT id, name, rn + 1, n
FROM split
WHERE rn < n
)
SELECT id, name, rn
FROM split;