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

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
);

Related

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.

Postgres: hierarchical one to many jsonb aggregation

I have a one-to-many relation between parent and child tables as follows:
Child table:
+----------+-----------+------------+--------------+
| table_id | parent_id | page_index | other_column |
+----------+-----------+------------+--------------+
| t1 | p1 | 1 | foo |
| t1 | p1 | 2 | bar |
| t2 | p2 | 1 | baz |
+----------+-----------+------------+--------------+
I want to get the final result as follows, i.e. group by parent_id and group by page_index:
+-----------+--------------------------------------------+
| parent_id | pages |
+-----------+--------------------------------------------+
| p1 | [{other_column: foo}, {other_column: bar}] |
| p2 | [{other_column: baz}] |
+-----------+--------------------------------------------+
I tried this query:
SELECT parent_table.parent_id, jsonb_agg(child_table.*) as pages
FROM parent_table
JOIN child_table ON child_table.parent_id = parent_table.parent_id
group by parent_table.parent_id, child_table.page_index
But I got the result containing three rows like:
+-----------+-----------------------+
| parent_id | pages |
+-----------+-----------------------+
| p1 | [{other_column: foo}] |
| p1 | [{other_column: bar}] |
| p2 | [{other_column: baz}] |
+-----------+-----------------------+
So I did another aggregation on top of that using a subquery and grouping by parent_id again as follows:
select sub_q.parent_id, jsonb_agg(sub_q.pages) as pages
from (
SELECT parent_table.parent_id, jsonb_agg(child_table.*) as pages
FROM parent_table
JOIN child_table ON child_table.parent_id = parent_table.parent_id
group by parent_table.parent_id, child_table.page_index
) as sub_q
group by sub_q.parent_id
but I ended up with
+-----------+------------------------------------------------+
| parent_id | pages |
+-----------+------------------------------------------------+
| p1 | [[{other_column: foo}], [{other_column: bar}]] |
| p2 | [{other_column: baz}] |
+-----------+------------------------------------------------+
how do I get the above desired result with each row having a one-dimensional array using the most optimal query?
Would be great if the answer has a db fiddle!
You seem to be overcomplicating this. As far as shown in your sample data, you can get the information you want directly from the child table with simple aggregation:
select
parent_id
jsonb_agg(jsonb_build_object('other_column', other_column) order by page_index) pages
from child_table
group by parent_id
Demo on DB Fiddle:
parent_id | pages
:-------- | :-------------------------------------------------
p1 | [{"other_column": "foo"}, {"other_column": "bar"}]
p2 | [{"other_column": "baz"}]

Postgresql select column with minimum value of hierarchical parents

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.

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).

SQL Server : getting all leafs from tree

I have datatable which contains:
|Parent Key| Component Key|
I need to get all leafs(parent key) for a chosen component key.
For example:
| 1 | 2 |
| 1 | 3 |
| 2 | 4 |
| 2 | 5 |
| 6 | 4 |
| 7 | 6 |
| 8 | 11 |
| 9 | 4 |
| 10 | 12 |
for component key = 4 I want to receive
| 1 |
| 7 |
| 9 |
If selected component key is already a leaf (there is no row where component key == selected component key) I want to return only the selected component key.
Can it be done by select only ?
How to do it in the most efficient way ?
Try something like that:
DECLARE #selected INT = 4;
WITH cte
AS
(
SELECT ParentKey, ComponentKey
FROM Table1
WHERE ComponentKey = #selected
UNION ALL
SELECT Table1.ParentKey, Table1.ComponentKey
FROM Table1
INNER JOIN cte ON Table1.ComponentKey = cte.ParentKey
)
SELECT ParentKey FROM cte
WHERE
ParentKey NOT IN (SELECT ComponentKey FROM Table1)
UNION
SELECT ParentKey FROM Table1
WHERE
ParentKey = #selected
AND ParentKey NOT IN (SELECT ComponentKey FROM Table1)
Replace Table1 with your table name.