Convert an adjacency list to an ltree in PostgreSQL - sql

I have a tree structure stored as an adjacency list in PostgreSQL 9.6:
"departments" table (parent_id = -1 means a node has no parent)
id | name | parent_id
-----------------------------------
-1 | NULL | -1
1 | Dep_1 | -1
2 | Dep_2 | 1
3 | Dep_3 | 1
4 | Dep_4 | 3
5 | Dep_5 | -1
I need to convert it to ltree type with id's as path labels:
id | name | path
------------------------------
1 | Dep_1 | 1
2 | Dep_2 | 1.2
3 | Dep_3 | 1.3
4 | Dep_4 | 1.3.4
5 | Dep_5 | 5
What's the easiest way to do this? Is it possible using SQL only?

let's say I create a table a and fill it with your data, then recursive CTE will do it neat:
t=# with recursive r(id, name, path) as (
select id, name, ''::text
from a
where id = -1
union all
select o.id, o.name, concat(path, '.', o.id)
from a o
join r n on o.parent_id = n.id and o.id <> -1
)
select id,name,right(path,-1)::ltree path
from r
where id <> -1
order by id asc;
id | name | path
----+------------+-------
1 | Dep_1 | 1
2 | Dep_2 | 1.2
3 | Dep_3 | 1.3
4 | Dep_4 | 1.3.4
5 | Dep_5 | 5
(5 rows)

Related

How can I generate sequence number for sql select that gives sub numbers for descendant items?

I would like to generate sequence numbers for select that gives sub numbers for descendant items.
I want the numbers be the following format:
root: 1...n
children of root: 1.1 -> 1.n
sub children: 1.1.1 -> 1.1.n
and so on...
I have Item table which has an owner_ref foreign key
the table: (name of items is just an example, it can be anything)
id | item_name | parent_id | owner_ref_id
----|------------|-----------|--------------
1 | item_1 | null | 1
2 | item_1.1 | 1 | 1
3 | item_1.1.1 | 2 | 1
4 | item_2 | null | 1
5 | item_2.1 | 4 | 1
6 | item_2.2 | 4 | 1
--------------------------------------------
The outcome should looks like :
seq_num | item_name | parent_id | owner_ref_id
---------|------------|-----------|--------------
1 | item_1 | null | 1
1.1 | item_1.1 | 1 | 1
1.1.1 | item_1.1.1 | 2 | 1
2 | item_2 | null | 1
2.1 | item_2.1 | 4 | 1
2.2 | item_2.2 | 4 | 1
--------------------------------------------
Use recursive CTE to form a tree-like structure -
with recursive nodes(id,item_name, parent_id,lvl, path) as (
select id,item_name, parent_id, 1
, row_number() OVER (order by parent_id nulls first)::text as path
from items where parent_id is null
union all
select o.id,o.item_name, o.parent_id,n.lvl+1, n.path || '.' ||
row_number() OVER (partition by o.parent_id order by o.parent_id)::text
from items o
join nodes n on n.id = o.parent_id
)
select *
from nodes
order by id
View on DBFiddle

SQL Postgres display details of edges

I have a table of nodes say
Table Node
Column| type
------|---------------
id | int
x | int
y | int
z | text
for example,
id | x | y | z
---|---|---|---
0 | 0 | 0 |'a'
1 | 0 | 1 |'b'
2 | 1 | 0 |'c'
3 | 1 | 1 |'d'
and
Table Edge
Column| type
------|---------------
source| int references Node.id
target| int references Node.id
for example,
source | target
-------|-------
0 | 1
1 | 3
3 | 2
assuming a node doesn't have an edge to itself, and every source is unique
I want to display the result of the entire edge information
source.x | source.y | target.x | target.y
---------|----------|----------|---------
0 | 0 | 0 | 1
0 | 1 | 1 | 1
1 | 1 | 1 | 0
I have tried many joins(self join of node with inner join with edge), (left join of edge with result of self join of node)
How can I achieve this selection result?
I think you want two joins:
select ns.x, ns.y, nt.x, nt.y
from edge e join
nodes n1
on e.source = ns.id join
nodes nd
on e.target = nt.id
In case of PostgreSQL, you can play with the RECURSIVE WITH clause.
a) build a path:
CREATE LOCAL TEMPORARY TABLE
node(id,x,y,z) AS (
SELECT 0,0,0,'a'
UNION ALL SELECT 1,0,1,'b'
UNION ALL SELECT 2,1,0,'c'
UNION ALL SELECT 3,1,1,'d'
)
;
CREATE LOCAL TEMPORARY TABLE
edge(source,target) AS (
SELECT 0,1
UNION ALL SELECT 1,3
UNION ALL SELECT 3,2
)
;
WITH RECURSIVE backbone AS (
SELECT
source::CHAR(1)||','||target::CHAR(1) AS path
, 1 AS hops
, *
FROM edge
UNION ALL SELECT
p.path||','||c.target::CHAR(1) AS path
, p.hops + 1 AS hops
, c.*
FROM backbone p JOIN edge c ON c.source=p.target
)
SELECT * FROM backbone;
path | hops | source | target
---------+------+--------+--------
0,1 | 1 | 0 | 1
1,3 | 1 | 1 | 3
3,2 | 1 | 3 | 2
0,1,3 | 2 | 1 | 3
1,3,2 | 2 | 3 | 2
0,1,3,2 | 3 | 3 | 2
b) for the paths with 2 and 3 hops, get the first and last node id from the path:
-- as above, but instead of the last line: "SELECT * FROM backbone" ...
,
origin_and_dest AS (
SELECT
*
, SPLIT_PART(path,',',1 )::INT AS origin
, SPLIT_PART(path,',',hops + 1)::INT AS destination
FROM backbone
WHERE hops >=2
)
SELECT * FROM origin_and_dest;
path | hops | source | target | origin | destination
---------+------+--------+--------+--------+-------------
0,1,3 | 2 | 1 | 3 | 0 | 3
1,3,2 | 2 | 3 | 2 | 1 | 2
0,1,3,2 | 3 | 3 | 2 | 0 | 2
With the ultimate source id and the ultimate target id out of the path, join back to nodes:
-- same as above, but instead of the last line: "SELECT * FROM origin_and_dest" ...
SELECT
origin.x AS source_x
, origin.y AS source_y
, dest.x AS target_y
, dest.y AS target_y
FROM origin_and_dest
JOIN node AS origin ON origin=origin.id
JOIN node AS dest ON destination=dest.id
;
source_x | source_y | target_y | target_y
----------+----------+----------+----------
0 | 0 | 1 | 0
0 | 0 | 1 | 1
0 | 1 | 1 | 0
Not quite your result, but then I don't really know what you mean by "entire edge information" ... can you be more precise?

SQL recursion + column concatenation

I've got a self referencing table (in SQL Server):
Page
==========
Id: int
RelativeUrl: nvarchar(max)
ParentId: int -> FK to pageId
Example data:
ID | RelativeUrl | ParentId
===============================
1 | /root | null
2 | /test1 | 1
3 | /test2 | 1
4 | /test3 | 1
5 | /test1-1 | 2
6 | /test1-2 | 2
7 | /test1-1-1 | 5
I want to create an sql query to retrieve all pages of the table with FULL relative url.
I thought about using a recursive SQL query, but don't know how to concat the relativeurl to make it a full relative url.
Wanted result:
ID | FullRelativeUrl | ParentId
===================================================
1 | /root | null
2 | /root/test1 | 1
3 | /root/test2 | 1
4 | /root/test3 | 1
5 | /root/test1/test1-1 | 2
6 | /root/test1/test1-2 | 2
7 | /root/test1/test1-1/test1-1-1 | 5
You can use a recursive CTE:
with cte as (
select id, convert(varchar(max), relativeurl) as url, 1 as lev
from page
where parentid is null
union all
select p.id, concat(cte.url, p.relativeurl), lev + 1
from cte join
page p
on p.parentid = cte.id
)
select cte.*
from cte;
Here is a db<>fiddle.

select records where condition is true in one record

I need to select cid, project, and owner from rows in the table below where one or more rows for a cid/project combination has an owner of 1.
cid | project | phase | task | owner
-----------------------------------
1 | 1 | 1 | 1 | 1
1 | 1 | 1 | 2 | 2
1 | 1 | 1 | 3 | 2
2 | 1 | 1 | 1 | 1
2 | 1 | 1 | 2 | 1
3 | 1 | 1 | 3 | 2
My output table should look like the this:
cid | project | phase | task | owner
-----------------------------------
1 | 1 | 1 | 1 | 1
1 | 1 | 1 | 2 | 2
1 | 1 | 1 | 3 | 2
2 | 1 | 1 | 1 | 1
2 | 1 | 1 | 2 | 1
The below query is what I came up with. It does seem to test okay, but my confidence is low. Is the query an effective way to solve the problem?
select task1.cid, task1.project, task1.owner
from
(select cid, project, owner from table) task1
right join
(select distinct cid, project, owner from table where owner = 1) task2
on task1.cid = task2.cid and task1.project = task2.project
(I did not remove the phase and task columns from the sample output so that it would be easier to compare.)
You can simply use a IN clause
select cid, project, owner
from table
where cid in (select distinct id from table where owner = 1)
or a inner join with a subquery
select a.cid, a.project, a.owner
from table a
INNER JOIN ( select distinct cid , project
from table where owner = 1
) t on t.cid = a.cid and t.project = a.project

How to select hierarchy collection? (mixed with non hierarchy data, etc)

Having the table:
I need to show the following:
| ID | PERSONID | MASTERID | CHILDID | VALUE | DEPTHLEVEL |
---------------------------------------------------------------
| 1 | 3 | 78452 | 21456 | 100 | 1 |
| 2 | 3 | 21456 | | 0 | 2 |
| 3 | 3 | 652314 | 417859 | 115 | 1 |
| 4 | 3 | 417859 | | 0 | 2 |
| 5 | 4 | 998654 | 223655 | 300 | 1 |
| 6 | 4 | 223655 | | 0 | 2 |
| 7 | 4 | 201302 |789654,441592| 200 | 1 |
| 8 | 4 | 789654 | | 0 | 2 |
| 9 | 4 | 441592 | | 0 | 2 |
| 10 | 5 | 999852 | | 123 | 1 |
Look at the row with id 10 this row has not relations (childs), the row with id 7 has two childs.
I need to quit (put value to 0) the value for every child/leaf.
For the row 1-9 I try the following query:
select v.* from
(
select v.id, v.personid,
case when level > 1
then 0
else
v.value
end thevalue,
v.masterid, v.childid, level depthlevel
from tmpsimpleexample v
start with v.childid is not null
connect by v.masterid = prior v.childid
) v
order by v.id
Results:
Look the rows with id 7, 8 is the master with two childs, I need to put this in one row.
This is the first problem.
Also I need to show the data with no hierarchy relation(id 10 in expected result table, id 11 in image table data).
I think that I can query all rows with masterid not referenced by a childid and then make an union between the first query(above) and the query to search all master id not referenced by childid.
The query to to search all rows with masterid not referenced by childid will show me the row without relation and the master rows of level 1.
select id, personid, value thevalue, masterid, childid, 1 depthlevel
from TMPSIMPLEEXAMPLE
where masterid not in
(select childid from TMPSIMPLEEXAMPLE where childid is not null)
Here I can do an union and the result will fit my requirements(except the childid concatenate for master row).
select v.* from
(
select v.id, v.personid,
case when level > 1
then 0
else
v.value
end thevalue,
v.masterid, v.childid, level depthlevel
from tmpsimpleexample v
start with v.childid is not null
connect by v.masterid = prior v.childid
union
select id, personid, value thevalue, masterid, childid, 1 depthlevel
from TMPSIMPLEEXAMPLE
where masterid not in
(select childid from TMPSIMPLEEXAMPLE where childid is not null)
) v
order by v.id
Almost final result:
But knowing that my real table has hundred of thousands of records make union like that are a good approach?
I've taken a stab at what I think your source data looks like:
| ID | PERSONID | MASTERID | CHILDID | VALUE |
-----------------------------------------------
| 1 | 3 | 78452 | 21456 | 100 |
| 2 | 3 | 21456 | | -1 |
| 3 | 3 | 652314 | 417859 | 115 |
| 4 | 3 | 417859 | | -1 |
| 5 | 4 | 998654 | 223655 | 300 |
| 6 | 4 | 223655 | | -1 |
| 7 | 4 | 201302 | 441592 | 200 |
| 7 | 4 | 201302 | 789654 | 200 |
| 9 | 4 | 441592 | | -1 |
| 8 | 4 | 789654 | | -1 |
| 10 | 4 | 999852 | | 123 |
-----------------------------------------------
The following query gets you your desired results:
enter code here
select id,
personid,
masterid,
listagg(childid, ',') within group (order by childid) childid,
-- Took a guess that all values for a personid were the same and didn't need to be aggregated...
min(decode(depthlevel, 1, value, null)) value,
min(depthlevel) depthlevel
from (select v.*, level depthlevel
from tmpsimpleexample v
connect by v.masterid = prior v.childid
-- Trick here is to start with all of the desired starting conditions...
start with not exists ( select 'X' from tmpsimpleexample v2 where v2.childid = v.masterid ))
group by id, personid, masterid;
If ordering of your CHILDID is important, you would need to re-join the nested view with TMPSIMPLEEXAMPLE:
select v1.id,
v1.personid,
v1.masterid,
listagg(v1.childid, ',') within group (order by v2.id) childid,
min(decode(depthlevel, 1, v1.value, null)) value,
min(depthlevel) depthlevel
from (select v.*, level depthlevel
from tmpsimpleexample v
connect by v.masterid = prior v.childid
start with not exists ( select 'X' from tmpsimpleexample v2 where v2.childid = v.masterid )) v1,
tmpsimpleexample v2
-- Outer Join is important!
where v1.childid = v2.masterid (+)
group by v1.id, v1.personid, v1.masterid;
The real magic here is the LISTAGGG function. If you are not on 11g or better yet (why not?!?), then the following article can guide you in building your own aggregate function:
http://www.oracle-base.com/articles/misc/string-aggregation-techniques.php