SQL recursion + column concatenation - sql

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.

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

Postgresql hierarchical (tree) query

I found few topics about it but none fits my expected results.
I have levels of categories stored in the table, just want to display it as tree structure.
All answers are kind of following query:
DB FIDDLE
WITH RECURSIVE cte AS (
SELECT category_id, category_name, parent_category, 1 AS level
FROM category
WHERE level = 1
UNION ALL
SELECT c.category_id, c.category_name, c.parent_category, ct.level + 1
FROM cte ct
JOIN category c ON c.parent_category = ct.category_id
)
SELECT *
FROM cte;
But the results are like
level
1
1
2
2
2
3
3
3
3
3
What I want to achieve is
level
1
2
3
3
2
3
3
1
2
3
3
2
3
3
You would typically keek track of the path to each node and use that for ordering. In Postgres, arrays come handy for this:
with recursive cte as (
select category_id, category_name, parent_category, 1 as level, array[category_id] path
from category
where parent_category is null
union all
select c.category_id, c.category_name, c.parent_category, ct.level + 1, ct.path || c.category_id
from cte ct
join category c on c.parent_category = ct.category_id
)
select *
from cte
order by path
Note that there is no need to store the level in the table; you can compute the information on the fly as you iterate. To identify the root nodes, you can filter on rows whose parent is null.
In your db fiddle, the query returns:
category_id | category_name | parent_category | level | path
----------: | :------------ | --------------: | ----: | :-------
1 | cat1 | null | 1 | {1}
3 | cat3 | 1 | 2 | {1,3}
8 | cat8 | 3 | 3 | {1,3,8}
9 | cat9 | 3 | 3 | {1,3,9}
4 | cat4 | 1 | 2 | {1,4}
6 | cat6 | 4 | 3 | {1,4,6}
7 | cat7 | 4 | 3 | {1,4,7}
5 | cat5 | 1 | 2 | {1,5}
10 | cat10 | 5 | 3 | {1,5,10}
11 | cat11 | 5 | 3 | {1,5,11}
2 | cat2 | null | 1 | {2}
You can keep track of the hierarchy as an array and use that for ordering:
WITH RECURSIVE cte AS (
SELECT category_id, category_name, parent_category, 1 AS level, array[category_id] as categories
FROM category
WHERE level = 1
UNION ALL
SELECT c.category_id, c.category_name, c.parent_category, ct.level + 1, ct.categories || c.category_id
FROM cte ct JOIN
category c
ON c.parent_category = ct.category_id
)
SELECT *
FROM cte
ORDER BY categories;
Here is a db<>fiddle.

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?

Convert an adjacency list to an ltree in PostgreSQL

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)

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