Postgresql select column with minimum value of hierarchical parents - sql

Having a table like this
Table "public.access_level"
Column | Type | Modifiers | Storage | Stats target | Description
-----------+---------+-----------+----------+--------------+-------------
uid | uuid | not null | plain | |
parent_id | integer | not null | plain | |
child_id | integer | not null | plain | |
level | integer | not null | plain | |
entity | text | | extended | |
and rows like this (uid column eliminated)
parent_id | child_id | level | entity
-----------+----------+-------+--------
11 | 22 | 4 | a
22 | 33 | 5 | a
33 | 44 | 6 | a
11 | 22 | 7 | b
22 | 33 | 4 | b
33 | 44 | 5 | b
I would like an output which returns level value for each row based on minimum value of level of parents distinctive to each entity.
Here is my desired output:
parent_id | child_id | level | entity
-----------+----------+-------+--------
11 | 22 | 4 | a
22 | 33 | 4 | a
33 | 44 | 4 | a
11 | 22 | 7 | b
22 | 33 | 4 | b
33 | 44 | 4 | b
A recursive approach is desirable as hierarchies depth is not fixed.
Note:(parent_id,child_id,entity) is unique in the table
Actually parent_id and child_id are users. a parent gives a child a level of access for an entity. Then the child user can give a level of access to another child of its own. At some point, the parent of a parent may change the access level if its child. now all deeper children have to have access level not more than that. It could not be implemented using a trigger to update the corresponding rows because the parent of parent may rollback changes.
A Scenario:
11,22,7,b means user-11 gives user-22 the level of 7 to b entity.
Now user-22 gives the user-33 level of 5 to b entity at some point
then user-33 give user-44 the level of 5 to b entity.
Important: user-22 changes access level of b to 4 for the user-33 which is what you see in example table
access level of user-33 to user-44 for b entity should remain 5 in the table
But I want the a query which will return 4 for that column as if I do it recursively for all children of user-22 which got the level more than 4.
thanks

The initial part of the recursive query finds roots (nodes without parents), in the recursive part we simply choose a less level for an entity:
with recursive cte as (
select parent_id, child_id, level, entity
from access_level t
where not exists (
select from access_level l
where l.child_id = t.parent_id)
union all
select t.parent_id, t.child_id, least(c.level, t.level), t.entity
from cte c
join access_level t
on t.parent_id = c.child_id and t.entity = c.entity
)
select *
from cte
order by entity, parent_id
Db<>fiddle.

Related

Get all IDs that do not associate with a specific parent ID

There is a specific child/parent table structure in my DB:
CHILD_TABLE:
| child_table |
|-------------|
| id |
| node_id |
A PARENT_TABLE:
| parent_table |
|--------------|
| id |
| node_id |
and an ASSOCIATION_TABLE:
| association_table |
|-------------------|
| parent_node |
| child_node |
(ManyToOne on both parent and child tables)
Let's say we load them with test data as:
-- child table
| id | node_id |
|----|---------|
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
-- parent table
| id | node_id |
|----|---------|
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
| 4 | 4 |
-- association table
| parent_id | child_id |
|-----------|----------|
| 1 | 1 |
| 2 | 1 |
| 2 | 2 |
| 3 | 3 |
| 4 | 1 |
Given a list of parent IDs and a single parent ID, I want to find all child IDs that are associated with those IDs but not the single one.
In the example data above,
List of parent IDs : (1, 2)
Single parent ID: 4
The result should be child.id = 2 because that entry has no connection with parent.id = 4 but there is at least one connection with the given "parent IDs".
EDIT
I managed to get something working with by subtracting one result over the other:
SELECT child.id
FROM child_table child
WHERE child.node_id
IN (
SELECT assoc.child_node
FROM association_table assoc
WHERE assoc.parent_node
IN (
SELECT parent.node_id
FROM parent_table parent
WHERE parent.id IN (1, 2)
)
)
MINUS
SELECT child2.id
FROM child_table child2
WHERE child2.node_id
IN (
SELECT assoc2.child_node
FROM association_table assoc2
WHERE assoc2.parent_node
IN (
SELECT parent2.node_id
FROM parent_table parent2
WHERE parent2.id = 4
)
);
Is there an alternative/simpler way of doing the same thing?
You just need the association table. Select from it all children for the given parent list, from there use NOT EXISTS to remove all child associations with the single parent id. (see demo)
select a1.child_id
from association a1
where a1.parent_id in (1,2)
and not exists ( select null
from association a2
where a1.child_id = a2.child_id
and a2.parent_id = 4
);

Identifying heirarchical groupings from a Parent-Child associaiton list in SQL

I am trying to identify groupings of accounts from a Parent-Child association table in SQL. Rather than a big hierarchy tree, I am dealing with many small trees and I need to identify each Tree as a unique Group in order to label related accounts.
I have two tables, a table of all Unique ID's:
+------+-------+
| ID | Group |
+------+-------+
| A | NULL |
| B | NULL |
| C | NULL |
| etc. | NULL |
+------+-------+
And a Table showing Parent - Child association between them:
+--------+-------+
| Parent | Child |
+--------+-------+
| A | D |
| A | E |
| B | F |
| B | G |
| B | C |
| C | H |
+--------+-------+
I Need to Fill the Group field of my first table so that I can identify all accounts which have a direct or indirect relationship eg:
+----+-------+
| ID | Group |
+----+-------+
| A | 1 |
| B | 2 |
| C | 2 |
| D | 1 |
| E | 1 |
| F | 2 |
| G | 2 |
| H | 2 |
+----+-------+
Where I'm struggling is that a Parent could be a Child to another Parent eg:
Parent B -> Parent -> C -> Child H
These form a Group but there is no direct link between B and H and I am struggling to find a reliable way to identify all associated ID's
This type of logic requires a recursive CTE. The idea is to start at the parents and work your way down the hierarchy:
with cte as (
select row_number() over (order by node) as grp,
n.node as ultimate_parent, n.node as node, 1 as lev
from nodes n
where not exists (select 1 from pc where pc.child = n.node)
union all
select cte.grp, cte.ultimate_parent, pc.child, lev + 1
from cte join
pc
on cte.node = pc.parent
)
update nodes
set grp = cte.grp
from cte
where cte.node = nodes.node;
Here is a db<>fiddle.

Split data by levels in hierarchy

Example of initial data:
| ID | ParentID |
|------|------------|
| 1 | NULL |
| 2 | 1 |
| 3 | 1 |
| 4 | 2 |
| 5 | NULL |
| 6 | 2 |
| 7 | 3 |
In my initial data I have ID of element and his parent ID.
Some elements has parent, some has not, some has a parent and his parent has a parent.
The maximum number of levels in this hierarchy is 3.
I need to get this hierarchy by levels.
Lvl 1 - elements without parents
Lvl 2 - elements with parent which doesn't have parent
Lvl 3 - elements with parent which has a parent too.
Expected result looks like:
| Lvl1 | Lvl2 | Lvl3 |
|-------|----------|----------|
| 1 | NULL | NULL |
| 1 | 2 | NULL |
| 1 | 3 | NULL |
| 1 | 2 | 4 |
| 5 | NULL | NULL |
| 1 | 2 | 6 |
| 1 | 3 | 7 |
How I can do it?
For a fixed dept of three, you can use CROSS APPLY.
It can be used like a JOIN, but also return extra records to give you the NULLs.
SELECT
Lvl1.ID AS lvl1,
Lvl2.ID AS lvl2,
Lvl3.ID AS lvl3
FROM
initial_data AS Lvl1
CROSS APPLY
(
SELECT ID FROM initial_data WHERE ParentID = Lvl1.ID
UNION ALL
SELECT NULL AS ID
)
AS Lvl2
CROSS APPLY
(
SELECT ID FROM initial_data WHERE ParentID = Lvl2.ID
UNION ALL
SELECT NULL AS ID
)
AS Lvl3
WHERE
Lvl1.ParentID IS NULL
ORDER BY
Lvl1.ID,
Lvl2.ID,
Lvl3.ID
But, as per my comment, this is often a sign that you're headed down a non-sql route. It might feel easier to start with, but later it turns and bites you, because SQL benefits tremendously from normalised structures (your starting data).

Recursive query for hirarchical data based on adjacency list

Learing SQL, and have a bit of a problem. I have 2 tables level and level_hierarchy
|name | id | |parent_id | child_id|
------------------- ---------------------
| Level1_a | 1 | | NULL | 1 |
| Level2_a | 19 | | 1 | 19 |
| Level2_b | 3 | | 1 | 3 |
| Level3_a | 4 | | 3 | 4 |
| Level3_b | 5 | | 3 | 5 |
| Level4_a | 6 | | 5 | 6 |
| Level4_b | 7 | | 5 | 7 |
Now what I need, is a query that will return all entries from table level from every hirarchy level based on parameter that marks what level hierarchy level I want to get entries from.
Getting Level1 entries is quite easy.
SELECT name FROM level INNER JOIN level_hierarchy ON level.id =
level_hierarchy.child_id WHERE level_hierarchy.parent_id=NULL
Level2 entries:
Level2_a
Level2_b
are just the ones that have a parent and the parent of their parent is NULL and so on. This is where I suspect that recursion comes in.
Is there anyone who can guide thorugh it?
Your query for the first level (here depth to distinguish from the table) should look like this:
select l.name, h.child_id, 1 as depth
from level l
join level_hierarchy h on l.id = h.child_id
where h.parent_id is null;
name | child_id | depth
----------+----------+-------
Level1_a | 1 | 1
(1 row)
Note the proper use of is null (do not use = to compare with null as it always gives null).
You can use the above as an initial query in a recursive cte:
with recursive recursive_query as (
select l.name, h.child_id, 1 as depth
from level l
join level_hierarchy h on l.id = h.child_id
where h.parent_id is null
union all
select l.name, h.child_id, depth + 1
from level l
join level_hierarchy h on l.id = h.child_id
join recursive_query r on h.parent_id = r.child_id
)
select *
from recursive_query
-- where depth = 2
name | child_id | depth
----------+----------+-------
Level1_a | 1 | 1
Level2_b | 3 | 2
Level2_a | 19 | 2
Level3_a | 4 | 3
Level3_b | 5 | 3
Level4_a | 6 | 4
Level4_b | 7 | 4
(7 rows)
Good question, recursion is a difficult topic in SQL and its implementation varies by engine. Thanks for tagging your post with PostgreSQL. PostgreSQL has some excellent documentation on the topic.
WITH RECURSIVE rec_lh(child_id, parent_id) AS (
SELECT child_id, parent_id FROM level_hierarchy
UNION ALL
SELECT lh.child_id, lh.parent_id
FROM rec_lh rlh INNER JOIN level_hierarchy lh
ON lh.parent_id = rlh.child_id
)
SELECT DISTINCT level.name, child_id
FROM rec_lh INNER JOIN level
ON rec_lh.parent_id = level.id
ORDER BY level.name ASC;
See Also:
Recursive query in PostgreSQL. SELECT *

Determine whether a specific entry has any children in a hierarchical database

I'm working on a recursive query for a hierarchal table in psql. While I'm able to produce an ordered, hierarchal list, I cannot figure out how to determine whether a parent has any children.
My code at the moment:
WITH RECURSIVE q AS (
SELECT h, 1 AS level, ARRAY[ordering] AS ordered_path, ARRAY[id] AS breadcrumb
FROM report h
WHERE parent IS NULL
UNION ALL
SELECT hi, q.level + 1 AS level, ordered_path || ordering, breadcrumb || id FROM q
JOIN report hi ON hi.parent = (q.h).id )
SELECT (q.h).id, (q.h).parent, (q.h).name, array_to_json(breadcrumb) AS breadcrumbs,
row_number() OVER (order by ordered_path) AS flat_order
FROM q
ORDER BY ordered_path
Which produces the following table:
id | parent | name | ordering | trail | rownum
----+--------+-----------------------+----------+--------------+--------
1 | | Entry 1 | 1 | [1] | 1
2 | 1 | Entry 2 | 1 | [1,2] | 2
15 | 2 | Entry 3 | 1 | [1,2,15] | 3
159 | 2 | Entry 4 | 2 | [1,2,159] | 4
16 | 2 | Entry 5 | 3 | [1,2,16] | 5
Essentially, I'd like a column that shows if a specific entry has any children. In this example, Entry 5 has no children.
The format of the original table is:
id | name | type | parent | ordering
-----+-----------------------------------------+---------+--------+----------
186 | Entry 1 | page | 172 | 23
154 | Entry 2 | page | 63 | 3
169 | Entry 3 | page | 163 | 3
Thanks!
You could use a correlated subquery as an extra field:
exists (select 1 from report where parent = q.id) as has_children
It's not necessarily the most efficient — though tbh, given the query, I'm can't think of anything better off the top of my head. But it'll work.
sql below fill find the child and will count you can change how you want the output by using case statements I tested the code, seems to be working
select x.Col1, count(y.Col1) as child from Table1 x
inner join Table2 y on x.Col1 = y.Col1
group by x.Col1