Get all of the low-level children of a node - sql

I am struggling with the writing of a query for PostgreSQL. The table resembles a tree and every item can have n children.
I would like to get only and all the children of the lowest level(s) (which don't have any children by themselves) of a given element of the tree.
The structure:
CREATE TABLE items
(
ikey integer NOT NULL,
description character varying(255),
parent integer,
CONSTRAINT i_pk PRIMARY KEY (ikey),
CONSTRAINT i_relation FOREIGN KEY (parent)
REFERENCES items (ikey) MATCH SIMPLE
)
Some values of the table would look like this:
1 "Products" NULL --Parent
2 "Metal" 1
3 "Nails" 2
4 "Chains" 2
5 "Bicycle Chains" 4
6 "Shimano Bicycle Chains" 5
7 "Shimano Bicycle Chains" 5
8 "7mm chain, black" 4
9 "Wood" 1
10 "Cutting Boards" 8
11 "Cutting Board Holder" 8
Most of the solutions on SO deal with not very deep trees which have just 1-2 levels. Or it is known from which parent the children are required.
I would like to select all children of "Chains" (4), which would give the following result:
6 "Shimano Bicycle Chains" 5
7 "Shimano Bicycle Chains" 5
8 "7mm chain, black" 4
To be honest, recursive queries are not my strongest skill. I had the idea already to search upside down - getting all items first which are never used as parents and then going down, but this only shifts the problem to the point again that I have to go down recursively from the given parent which seems a bit over the top.

If you want to find all children items of an item in all levels you can write a recursive CTE. For example:
with recursive
n as (
select * from items where parent = 4 -- Children of "Chains"
union all
select i.*
from n
join items i on i.parent = n.ikey
)
select * from n
Result:
ikey description parent
----- ------------------------- ------
5 Bicycle Chains 4
8 7mm Chain Black 4
6 Shimano Bicycle Chains 1 5
7 Shimano Bicycle Chains 2 5
See running example at DB Fiddle.

You do not need a recursive query. Instead, you can simply select the rows with ikeys that do not occur as parents:
select i1.* from items i join items i1 on i.ikey = i1.parent where i.parent = 4 and
not exists (select 1 from items i2 where i2.parent = i1.ikey)

Another recursive CTE, with extras.
The base key that started the recursion & level of the tree.
WITH RECURSIVE RCTE_OFFSPRING AS (
SELECT ikey as base, 0 as lvl
, ikey, description, parent
FROM items
WHERE ikey = 4
UNION ALL
SELECT cte.base, cte.lvl + 1
, itm.ikey, itm.description, itm.parent
FROM items itm
JOIN RCTE_OFFSPRING cte
ON cte.ikey = itm.parent
)
SELECT *
FROM RCTE_OFFSPRING
WHERE lvl > 0
ORDER BY base, lvl, ikey
base
lvl
ikey
description
parent
4
1
5
Bicycle Chains
4
4
1
8
7mm chain, black
4
4
2
6
Shimano Bicycle Chains
5
4
2
7
Shimano Bicycle Chains
5
db<>fiddle here

Related

BOM Explosion with Blanks

I have a simple BOM table that contains a parent and a child column.
Every row contains values. There are no blanks.
To bring that table into a BI-Tool I need to add blank values for the parents, like here shown:
As you can see, if an EntityKay has no parent key, then there should be a blank value.
How would you do that?
Example:
Current State:
Child Parent
4 1
5 1
6 2
7 3
8 3
9 3
Needed Result:
Child Parent
1
2
3
4 1
5 1
6 2
7 3
8 3
9 3
This query provides the missing keys, i.e. the parents, that are no childs are selected with the parent null
select distinct Parent Child, null Parent from bom
where Parent not in (select Child from bom)
order by 1;
CHILD PARENT
---------- ------
1
2
3
You must add those records (with an INSERT) to your table and probably update additionaly the description.
insert into bom (Child, Parent)
select distinct Parent Child, null Parent from bom
where Parent not in (select Child from bom)
Result
select * from bom;
CHILD PARENT
---------- ----------
4 1
5 1
6 2
7 3
8 3
9 3
2
3
1
To get the extended data without modifying the table use an UNION ALL of the ofiginal table with the query providing the missing part.
You may add also the names for the new rows using the DECODE as show below in the example
select
Child, Parent, name
from BOM union all
select distinct
Parent Child, null Parent,
decode(Parent,1,'North America',2,'Europa',3,'Asia')
from bom
where Parent not in (select Child from bom)

SQL products contain other

I have a table look like this
Parent Child
10 2
10 3
10 4
10 5
11 2
11 3
as you can see parent 10 also contain parent 11 and thats what I want to display in the table, I want to add to it a row with that data :
Parent Child
10 2
10 3
10 4
10 5
**10 11**
11 2
11 3
You can get parents that "contain" other parents using a self-join and aggregation:
with t as (
select t.*, count(*) over (partition by parent) as num_child
from yourtable
)
select tp2.parent, tp.parent
from t tp join
t tp2
on tp.child = tc2.child and tp.parent <> tp2.parent
group by tp.parent, tp2.parent, tp.num_child
having count(*) = tp.num_child -- all children match
(This version assumes no duplicate rows.)
You can then use insert to add these into the table.
Note: If two parents have the same children, two rows will be inserted.

How to select path from nested sets excluding subtrees

I have an SQLite database, which contains trees with red and black nodes (not red-black trees though). Trees are stored as nested sets (https://en.wikipedia.org/wiki/Nested_set_model)
Name TreeId Left Right IsBlack
A 1 1 8 1
B 1 2 7 0
C 1 3 6 1
D 1 4 5 1
A 2 1 10 1
B 2 2 5 0
C 2 6 9 1
D 2 3 4 1
D 2 7 8 1
Both B and C nodes with TreeId = 2 point to D node. So D is written twice.
These trees may contain only black or only red nodes
I would like to select all paths for the specified node, that do not contain red nodes, i. e. exclude red nodes and all their subtrees from result
Examples:
For record:
Name TreeId Left Right IsBlack
A 1 1 8 1
the result will be:
Name TreeId Left Right IsBlack
A 1 1 8 1
For record:
Name TreeId Left Right IsBlack
C 1 3 6 1
the result will be:
Name TreeId Left Right IsBlack
C 1 3 6 1
D 1 4 5 1
And, finally, for record:
Name TreeId Left Right IsBlack
A 2 1 10 1
The result will be:
Name TreeId Left Right IsBlack
A 2 1 10 1
C 2 6 9 1
D 2 7 8 1
To keep it simple, let's assume that there is another query, which selects TreeId, Left and Right parameters of the searched node by its name.
So I came up with the following query (for node A):
SELECT Nodes.* FROM Nodes
LEFT JOIN Nodes as n ON Nodes.[TreeId] = n.TreeId AND n.IsBlack = 0 AND Nodes.Left >= n.Left AND Nodes.Right <= n.Right
AND n.Left >= 1 AND n.Right <= 10
WHERE Nodes.TreeId = 2 AND Nodes.Left >= 1 AND Nodes.[Right] <= 10 AND n.Name IS NULL
The query seems to work, but it's terribly slow because of left join even with indexes.
So I was wondering, is there a way to optimize the query in terms of SQLite avoiding left join (for example using inner joins, unions, etc)
P.S. I cannot change the way the data is stored, but I can modify database schema (add new fields).
P.P.S. I understand, that I can query all results, and then filter them on the code side. The other option is to store two types of trees in db: one with red and black nodes, and the other one with black nodes only. However, both of these solutions are last resorts.
Thanks in advance
Okay, I don't know if it'll be faster or not - can't tell with only a few rows of data, but this one uses a recursive CTE to at least get the same results as your examples:
WITH RECURSIVE n AS
(SELECT * FROM nodes WHERE name= ?1 AND treeid = ?2 AND isblack = 1
UNION ALL
SELECT n2.name, n2.treeid, n2.left, n2.right, n2.isblack
FROM n
JOIN nodes AS n2
ON n2.treeid = n.treeid
AND (n2.left = n.left + 1 OR n2.right = n.right - 1)
WHERE n2.isblack = 1)
SELECT * FROM n ORDER BY name
You'll probably want an index on nodes(isblack, treeid, name) (And don't forget to run a PRAGMA optimize once in a while.) Bind/replace ?1 and ?2 with the obvious values for a particular run of the query.

Postgresql query to get sum of tree

Hi guys I would like to ask about postgresql and what could be the best query to get sum of column when you have table of elements that has some descendants of more levels ie.
id id_parentvalue
1 null 3
2 null 4
3 1 2
4 2 3
5 3 4
6 3 2
7 4 5
8 4 7
so the result would be rows with sum of all of their tree as follows
value of ids 5 and 6 together is 6 plus value of their parent would be 8 plus his parent would be 11, and same for items with id 7 and 8 so the grandĖ‡parent with id=2 would have value 19
id id_parentvalue
1 null 11
2 null 19
thanks in advance
Use recursive CTEs:
with recursive cte as (
select t.id, t.value, ultimate_parent as id
from t
where id_parent is null
union all
select t.id, t.value, cte.ultimate_parent
from cte join
t
on t.id_parent = cte.id
)
select ultimate_parent, sum(value)
from cte
group by ultimate_parent;
The recursive part starts with the ultimate parents -- the records whose parent is NULL. It then brings in lower levels, step-by-step, keeping the id of the ultimate parent.
The final aggregation just sums the values together.

Update Multiple Tables with an Especifc Order between them

Ok, maybe is a tricky question, but I know it could happen to some of us. I will try to make it as shorter as possible.
I have 4 Tables, 1 parent table and 3 child tables. (for this example they have the same columns, in my real case the don't). Like this.
(All the columns in the 4 tables are integer)
PARENT
ID POSITION
1 1
2 1
3 1
4 1
CHILD1
IDPARENT, POSITION
1 2
1 3
1 4
1 6
2 3
2 4
3 2
3 5
CHILD2
IDPARENT, POSITION
1 5
1 7
2 2
3 3
CHILD3
IDPARENT, POSITION
4 2
4 3
3 6
All the tables have a column named position where this are the rules:
The table PARENT always have position = 1
The position number is a consecutive per parent who cannot be duplicated across all the tables. (i.e the parent 1, have children in child1, child2 and child3, but the position is consecutive 1,2,3,4,5,6,7 it doesnt get repeated across them)
The consecutive could not be consecutive in the same table (i.e. parent 1 has in child 1 the position 2 3 4 6, but the 5 and 7 are in the child 2).
A parent can and cannot have children in all the tables (It could have in child1 and child3 athe same time or just child2 or in no one).
These tables are static, it is always the parent and 3 children.
I have written a query to get the idparent with their respective childrens and position.
SELECT C.IDPARENT, '1' AS CHILD, c.POSITION
FROM Child1 c
UNION ALL
SELECT C.IDPARENT, '2' AS CHILD, c.POSITION
FROM Child2 c
UNION ALL
SELECT C.IDPARENT, '3' AS CHILD, c.POSITION
FROM Child3 c
UNION ALL
SELECT P.ID, '0' AS CHILD, P.POSITION
FROM PARENT P
ORDER BY IDPARENT, POSITION, CHILD
It gets this output with the information above.
IDPARENT CHILD POSITION
1 0 1
1 1 2
1 1 3
1 1 4
1 2 5
1 1 6
1 2 7
2 0 1
2 2 2
2 1 3
2 1 4
3 0 1
3 1 2
3 2 3
3 1 5
3 3 6
4 0 1
4 3 2
4 3 3
As you can see, the information get correctly outputed and thats what i want. But there is Bad record here
3 2 3
3 1 5
It jumps from 3 to 5 instead to 4 because sometimes in the table those records get deleted by an outside web application.
So after all of this, this is my problem what refers to the subject of this question.
How could I make a massive update to all those tables, in a proper order, that records like
3 2 3
3 1 5
3 3 6
get converted into
3 2 3
3 1 4
3 3 5
Note: In my example i just did one bad record. In my real case, i got a lot of them.
I have written that query so far, and that's the data that I want to update, but I know you cannot update massive tables, just one by one, but I dont know how to update them with the consecutive order if I just can update one table at time. Because when the next query runs to update the next table, it will have now different data.
Thanks to all of you in advance for reading this long question and for the help provided.
;with cte as
(
SELECT C.IDPARENT, c.POSITION
FROM Child1 c
UNION ALL
SELECT C.IDPARENT, c.POSITION
FROM Child2 c
UNION ALL
SELECT C.IDPARENT, c.POSITION
FROM Child3 c
UNION ALL
SELECT P.ID, P.POSITION
FROM PARENT P
)
select C.*, row_number() over(partition by IDPARENT order by POSITION) as rn
into #tmp
from cte as C
update C
set Position = T.rn
from Child1 as C
inner join #tmp as T
on C.IDParent = T.IDParent and
C.Position = T.Position
update C
set Position = T.rn
from Child2 as C
inner join #tmp as T
on C.IDParent = T.IDParent and
C.Position = T.Position
update C
set Position = T.rn
from Child3 as C
inner join #tmp as T
on C.IDParent = T.IDParent and
C.Position = T.Position
drop table #tmp
Above is the query for you. Here you can try with sample data in table variables. The syntax for the inserts is for SQL Server 2008.