I've got a table in postgres 9.3.5 that looks like this:
CREATE TABLE customer_area_node
(
id bigserial NOT NULL,
customer_id integer NOT NULL,
parent_id bigint,
name text,
description text,
CONSTRAINT customer_area_node_pkey PRIMARY KEY (id)
)
I query with:
WITH RECURSIVE c AS (
SELECT *, 0 as level, name as path FROM customer_area_node WHERE customer_id = 2 and parent_id is null
UNION ALL
SELECT customer_area_node.*,
c.level + 1 as level,
c.path || '/' || customer_area_node.name as path
FROM customer_area_node
join c ON customer_area_node.parent_id = c.id
)
SELECT * FROM c ORDER BY path;
this seems to work to build paths like building1/floor1/room1, building1/floor1/room2, etc.
What I'd like to be able to do is easily turn that into either json that represents the tree structure which I've been told I can do with row_to_json.
As a reasonable alternative, any other way I can format the data to a more efficient mechanism such that I can actually easily turn it into an actual tree structure without having a ton of string.splits on /.
Is there a reasonably easy way to do this with row_to_json?
Sorry for the very late answer but i think i found an elegant solution that could become an accepted answer for this question.
Based on the awesome "little hack" found by #pozs, i came up with a solution that:
solves the "rogue leaves" situation with very little code (leveraging the NOT EXISTS predicate)
avoids the whole level calculation/condition stuff
WITH RECURSIVE customer_area_tree("id", "customer_id", "parent_id", "name", "description", "children") AS (
-- tree leaves (no matching children)
SELECT c.*, json '[]'
FROM customer_area_node c
WHERE NOT EXISTS(SELECT * FROM customer_area_node AS hypothetic_child WHERE hypothetic_child.parent_id = c.id)
UNION ALL
-- pozs's awesome "little hack"
SELECT (parent).*, json_agg(child) AS "children"
FROM (
SELECT parent, child
FROM customer_area_tree AS child
JOIN customer_area_node parent ON parent.id = child.parent_id
) branch
GROUP BY branch.parent
)
SELECT json_agg(t)
FROM customer_area_tree t
LEFT JOIN customer_area_node AS hypothetic_parent ON(hypothetic_parent.id = t.parent_id)
WHERE hypothetic_parent.id IS NULL
Update:
Tested with very simple data, it does work, but as posz pointed out in a comment, with his sample data, some rogue leaf nodes are forgotten. But, i found out that with even more complex data, the previous answer is not working either, because only rogue leaf nodes having a common ancestor with "max level" leaf nodes are caught (when "1.2.5.8" is not there, "1.2.4" and "1.2.5" are absent because they have no common ancestor with any "max level" leaf node).
So here is a new proposition, mixing posz's work with mine by extracting the NOT EXISTS subrequest and making it an internal UNION, leveraging UNION de-duplication abilities (leveraging jsonb comparison abilities):
<!-- language: sql -->
WITH RECURSIVE
c_with_level AS (
SELECT *, 0 as lvl
FROM customer_area_node
WHERE parent_id IS NULL
UNION ALL
SELECT child.*, parent.lvl + 1
FROM customer_area_node child
JOIN c_with_level parent ON parent.id = child.parent_id
),
maxlvl AS (
SELECT max(lvl) maxlvl FROM c_with_level
),
c_tree AS (
SELECT c_with_level.*, jsonb '[]' children
FROM c_with_level, maxlvl
WHERE lvl = maxlvl
UNION
(
SELECT (branch_parent).*, jsonb_agg(branch_child)
FROM (
SELECT branch_parent, branch_child
FROM c_with_level branch_parent
JOIN c_tree branch_child ON branch_child.parent_id = branch_parent.id
) branch
GROUP BY branch.branch_parent
UNION
SELECT c.*, jsonb '[]' children
FROM c_with_level c
WHERE NOT EXISTS (SELECT 1 FROM c_with_level hypothetical_child WHERE hypothetical_child.parent_id = c.id)
)
)
SELECT jsonb_pretty(row_to_json(c_tree)::jsonb)
FROM c_tree
WHERE lvl = 0;
Tested on http://rextester.com/SMM38494 ;)
You cannot do that with a usual recursive CTE, because it is almost impossible to set a json value deep in its hierarchy. But you can do it reversed: build up the tree starting from its leaves, until its root:
-- calculate node levels
WITH RECURSIVE c AS (
SELECT *, 0 as lvl
FROM customer_area_node
-- use parameters here, to select the root first
WHERE customer_id = 2 AND parent_id IS NULL
UNION ALL
SELECT customer_area_node.*, c.lvl + 1 as lvl
FROM customer_area_node
JOIN c ON customer_area_node.parent_id = c.id
),
-- select max level
maxlvl AS (
SELECT max(lvl) maxlvl FROM c
),
-- accumulate children
j AS (
SELECT c.*, json '[]' children -- at max level, there are only leaves
FROM c, maxlvl
WHERE lvl = maxlvl
UNION ALL
-- a little hack, because PostgreSQL doesn't like aggregated recursive terms
SELECT (c).*, array_to_json(array_agg(j)) children
FROM (
SELECT c, j
FROM j
JOIN c ON j.parent_id = c.id
) v
GROUP BY v.c
)
-- select only root
SELECT row_to_json(j) json_tree
FROM j
WHERE lvl = 0;
And this will work even with PostgreSQL 9.2+
SQLFiddle
Update: A variant, which should handle rogue leaf nodes too (which are located with a level between 1 and max-level):
WITH RECURSIVE c AS (
SELECT *, 0 as lvl
FROM customer_area_node
WHERE customer_id = 1 AND parent_id IS NULL
UNION ALL
SELECT customer_area_node.*, c.lvl + 1
FROM customer_area_node
JOIN c ON customer_area_node.parent_id = c.id
),
maxlvl AS (
SELECT max(lvl) maxlvl FROM c
),
j AS (
SELECT c.*, json '[]' children
FROM c, maxlvl
WHERE lvl = maxlvl
UNION ALL
SELECT (c).*, array_to_json(array_agg(j) || array(SELECT r
FROM (SELECT l.*, json '[]' children
FROM c l, maxlvl
WHERE l.parent_id = (c).id
AND l.lvl < maxlvl
AND NOT EXISTS (SELECT 1
FROM c lp
WHERE lp.parent_id = l.id)) r)) children
FROM (SELECT c, j
FROM c
JOIN j ON j.parent_id = c.id) v
GROUP BY v.c
)
SELECT row_to_json(j) json_tree
FROM j
WHERE lvl = 0;
This should work too on PostgreSQL 9.2+, however, I cannot test that. (I can only test on 9.5+ right now).
These solutions can handle any column in any hierarchical table, but will always append an int typed lvl JSON property to their output.
http://rextester.com/YNU7932
Developed the answer of pozs a little further to get recursive leaves with their subtrees. So this answer really returns the full tree.
CREATE OR REPLACE FUNCTION pg_temp.getTree(bigint)
RETURNS TABLE(
id bigint,
customer_id integer,
parent_id bigint,
name text,
description text,
children json
)
AS $$
WITH RECURSIVE relations AS (
SELECT
can.id,
can.customer_id,
can.parent_id,
can.name,
can.description,
0 AS depth
FROM customer_area_node can
WHERE can.id = $1
UNION ALL
SELECT
can.id,
can.customer_id,
can.parent_id,
can.name,
can.description,
relations.depth + 1
FROM customer_area_node can
JOIN relations ON can.parent_id = relations.id AND can.id != can.parent_id
),
maxdepth AS (
SELECT max(depth) maxdepth FROM relations
),
rootTree as (
SELECT r.* FROM
relations r, maxdepth
WHERE depth = maxdepth
UNION ALL
SELECT r.* FROM
relations r, rootTree
WHERE r.id = rootTree.parent_id AND rootTree.id != rootTree.parent_id
),
mainTree AS (
SELECT
c.id,
c.customer_id,
c.parent_id,
c.name,
c.description,
c.depth,
json_build_array() children
FROM relations c, maxdepth
WHERE c.depth = maxdepth
UNION ALL
SELECT
(relations).*,
array_to_json(
array_agg(mainTree)
||
array(
SELECT t
FROM (
SELECT
l.*,
json_build_array() children
FROM relations l, maxdepth
WHERE l.parent_id = (relations).id
AND l.depth < maxdepth
AND l.id NOT IN (
SELECT id FROM rootTree
)
) r
JOIN pg_temp.getTree(r.id) t
ON r.id = t.id
))
children
FROM (
SELECT relations, mainTree
FROM relations
JOIN mainTree
ON (
mainTree.parent_id = relations.id
AND mainTree.parent_id != mainTree.id
)
) v
GROUP BY v.relations
)
SELECT
id,
customer_id,
parent_id,
name,
description,
children
FROM mainTree WHERE id = $1
$$
LANGUAGE SQL;
SELECT *
FROM
customer_area_node can
JOIN pg_temp.getTree(can.id) t ON t.id = can.id
WHERE can.parent_id IS NULL;
Related
I need to convert a recursive SQL query into SqlAlchemy ORM. I cannot make it work. This is my code as it stands:
items_CTE = (
ses.query(BomVersion.ITEMID.label("Parent_Id"), Bom.ITEMID.label("Child_Id"))
.filter(
BomVersion.BOMID == Bom.BOMID,
Bom.DATAAREAID == "psi",
BomVersion.DATAAREAID == "psi",
)
.cte(name="Items_Cte")
)
parent_child_CTE = (
ses.query(items_CTE.Parent_Id, items_CTE.Child_Id)
.filter(items_CTE.Parent_Id == itemid)
.cte(name="Parent_Child_Cte", recursive=True)
)
parent_child_CTE = parent_child_CTE.union_all(
ses.query(items_CTE.Parent_Id, items_CTE.Child_Id)
).filter(parent_child_CTE.Child_Id == items_CTE.Parent_Id)
final_q = parent_child_CTE.all()
return final_q
I've seen examples of recursion where you have 1 CTE, but I don't understand how to approach this problem in particular. Working SQL code I wish to convert below.
WITH items_CTE AS (
-- create the mapping of items to consider
SELECT
BV.ITEMID AS Parent_id,
B.ITEMID AS Child_id
FROM BOM AS B
INNER JOIN BOMVERSION AS BV
ON B.BOMID = BV.BOMID
WHERE B.DATAAREAID = 'psi'
AND BV.DATAAREAID = 'psi'
), parent_child_cte AS (
-- select those items as initial query
SELECT
Parent_id,
Child_id
FROM items_CTE
WHERE Parent_id = '123456' -- this is the starting point
UNION ALL
-- recursive approach to find all children
SELECT
c.Parent_id,
c.Child_id
FROM items_CTE c
JOIN parent_child_cte pc
ON pc.Child_id = c.Parent_id
)
SELECT * FROM parent_child_cte
I have run into this a couple of times where a client is able to import data into a catalog with parent child relationships and I run into problems with said relationships. I need to find a way to prevent the following:
Object 1 has a child of Object 2
Object 2 has a child of Object 3
Object 3 has a child of Object 1
This throws the server into an infinite recursive loop and ultimately brings it to its knees. I can't seem to wrap my head around a SQL query that I could use to detect such recursive madness. The problem is prevalent enough that I need to find some solution. I've tried queries using CTE, nested selects/sub-selects and just can't seem to write one that will solve this issue. Any help would be greatly appreciated.
with recursive parents as (
select
s.id,
s.parent_id,
1 as depth
from categories s
where s.id = <passed in id>
union all
select
t.id,
t.parent_id,
c.depth + 1 as depth
from categories t
inner join parents c
on t.id = c.parent_id
where t.id <> t.parent_id)
select distinct parent_id from parents where parent_id <> 0 order by depth desc
This is what I finally came up with to "detect" a cycle condition
with recursive find_cycle as (
select
categories_id,
parent_id,
0 depth
from
categories
where categories_id = <passed in id>
union all
select
f.categories_id,
c.parent_id,
f.depth + 1
from
categories c
inner join find_cycle f
ON f.parent_id = c.categories_id
where c.parent_id <> c.categories_id
and f.parent_id <> f.categories_id
)
select
f.parent_id as categories_id,
c.parent_id
from find_cycle f
inner join categories c
on f.parent_id = c.categories_id
where exists (
select
1
from find_cycle f
inner join categories c
on f.parent_id = c.categories_id
where f.parent_id = <passed in id>)
order by depth desc;
It will return rows with the offending path and no rows if no cycle detected. Thanks for all the tips folks.
Here is the MariaDB function I came up with that will return 0 if there is not a cycle and 1 if there is a cycle for the id passed in to the function.
create function `detect_cycle`(id int, max_depth int) RETURNS tinyint(1)
begin
declare cycle_exists int default 0;
select (case when count(*) = 1 then 0 else 1 end) into cycle_exists
from
(
with recursive find_cycle as (
select
categories_id,
parent_id,
0 depth
from
categories
where categories_id = id
union all
select
f.categories_id,
c.parent_id,
f.depth + 1
from
categories c
inner join find_cycle f
ON f.parent_id = c.categories_id
where
c.parent_id <> c.categories_id
and f.parent_id <> f.categories_id
and f.depth < max_depth
)
select
c.parent_id
from find_cycle f
inner join categories c
on f.parent_id = c.categories_id
order by depth desc
limit 1
) __temp
where parent_id = 0;
return cycle_exists;
end;
This can then be called by executing
select categories_id, detect_cycle(categories_id, 5) as cycle_exists
from categories
where categories_id = <whatever id you want to check for a cycle condition>;
Here is a stored procedure that will accomplish the same thing but is generic enough to handle any table, id column, parent column combination.
CREATE PROCEDURE `detect_cycle`(table_name varchar(64), id_column varchar(32), parent_id_column varchar(32), max_depth int)
BEGIN
declare id int default 0;
declare sql_query text default '';
declare where_clause text default '';
declare done bool default false;
declare id_cursor cursor for select root_id from __temp_ids;
declare continue handler for not found set done = true;
drop temporary table if exists __temp_ids;
create temporary table __temp_ids(root_id int not null primary key);
set sql_query = concat('
insert into __temp_ids
select
`',id_column,'`
from ',table_name);
prepare statement from sql_query;
execute statement;
drop temporary table if exists __temp_cycle;
create temporary table __temp_cycle (id int not null, parent_id int not null);
open id_cursor;
id_loop: loop
fetch from id_cursor into id;
if done then
leave id_loop;
end if;
set where_clause = concat('where `',id_column,'` = ',id);
set sql_query = concat('
insert into __temp_cycle
select
t.`',id_column,'`,
t.`',parent_id_column,'`
from
(
with recursive find_cycle as (
select
`',id_column,'`,
`',parent_id_column,'`,
0 depth
from
`',table_name,'`
',where_clause,'
union all
select
f.`',id_column,'`,
c.`',parent_id_column,'`,
f.depth + 1
from
`',table_name,'` c
inner join find_cycle f
ON f.`',parent_id_column,'` = c.`',id_column,'`
where
c.`',parent_id_column,'` <> c.`',id_column,'`
and f.`',parent_id_column,'` <> f.`',id_column,'`
and f.depth < ',max_depth,'
)
select
c.`',id_column,'`,
c.`',parent_id_column,'`
from find_cycle f
inner join `',table_name,'` c
on f.`',parent_id_column,'` = c.`',id_column,'`
order by depth desc
limit 1
) t
where t.`',parent_id_column,'` > 0');
prepare statement from sql_query;
execute statement;
end loop;
close id_cursor;
deallocate prepare statement;
select distinct
*
from __temp_cycle;
drop temporary table if exists __temp_ids;
drop temporary table if exists __temp_cycle;
END
usage:
call detect_cycle(table_name, id_column, parent_id_column, max_depth);
This will return a result set of all cycle conditions within the given table.
Looks like you have this figured out to stop a cycling event but are looking for ways to identify a cycle. In that case, consider using a path:
with recursive parents as (
select
s.id,
s.parent_id,
1 as depth,
CONCAT(s.id,'>',s.parent_id) as path,
NULL as cycle_detection
from categories s
where s.id = <passed in id>
union all
select
t.id,
t.parent_id,
c.depth + 1 as depth,
CONCAT(c.path, '>', t.parent_id),
CASE WHEN c.path LIKE CONCAT('%',t.parent_id,'>%') THEN 'cycle' END
from categories t
inner join parents c
on t.id = c.parent_id
where t.id <> t.parent_id)
select distinct parent_id, cycle_detection from parents where parent_id <> 0 order by depth desc
I may be a bit off my syntax since it's been forever since I wrote mysql/mariadb syntax, but this is the basic idea. Capture the path that the recursion took and then see if your current item is already in the path.
If the depth of the resulting tree is not extremely deep then you can detect cycles by storing the bread crumbs that the recursive CTE is walking. Knowing the bread crumbs you can detect cycles easily.
For example:
with recursive
n as (
select id, parent_id, concat('/', id, '/') as path
from categories where id = 2
union all
select c.id, c.parent_id, concat(n.path, c.id, '/')
from n
join categories c on c.parent_id = n.id
where n.path not like concat('%/', c.id, '/%') -- cycle pruning here!
)
select * from n;
Result:
id parent_id path
--- ---------- -------
2 1 /2/
3 2 /2/3/
1 3 /2/3/1/
See running example at DB Fiddle.
I have seen a lot of examples about how to implement a recursive query where there is the parent and the child in the same table, but in the examples, the child has a parent and I need at the contrary, when a parent has a child.
I would like to obtain all children in recursive mode just like in the image.
In the image, you can see, I have a parent with id 1, it has a child with id 2. The child 2 is a parent too who has a child with id 3, etc.
I don't know how to create a recursive query to obtain all the childs from a parent.
You can access to the next link to execute the sql online: http://www.sqlfiddle.com/#!18/dbed2/1
This produces the results you are asking for:
with cte as (
select idchild, idparent,
convert(varchar(max), idchild) as children
from family f
where not exists (select 1 from family f2 where f2.idparent = f.idchild)
union all
select f.idchild, f.idparent,
concat(f.idchild, ',', cte.children)
from cte join
family f
on cte.idparent = f.idchild
)
select *
from cte
order by idchild;
Here is the SQL Fiddle.
Here you go:
with
n as (
select idparent, idchild, 1 as lvl,
cast(concat('', idchild) as varchar(255)) as children from family
union all
select n.idparent, f.idchild, lvl + 1,
cast(concat(children, ',', f.idchild) as varchar(255))
from n
join family f on f.idparent = n.idchild
)
select n.idparent, f.idchild, n.children
from n
join (
select idparent, max(lvl) as maxlvl from n group by idparent
) m on n.idparent = m.idparent and n.lvl = m.maxlvl
join family f on f.idparent = n.idparent
order by n.idparent
See SQL Fiddle.
if you are using SQL Server 2017 or newer you can use the following:
WITH CTE
AS (SELECT *
FROM dbo.Table_1
UNION ALL
SELECT Child.idParent,
Parent.idChild
FROM CTE AS Parent
INNER JOIN dbo.Table_1 AS Child
ON Parent.idParent = Child.idChild)
SELECT CTE.idParent,
STRING_AGG(CTE.idChild, ', ') AS Childs
FROM CTE
GROUP BY CTE.idParent;
but if you have older version use the following :
WITH CTE
AS (SELECT *
FROM dbo.Table_1
UNION ALL
SELECT Child.idParent,
Parent.idChild
FROM CTE AS Parent
INNER JOIN dbo.Table_1 AS Child
ON Parent.idParent = Child.idChild)
SELECT DISTINCT
B.idParent,
STUFF(
(
SELECT ',' + CONVERT(VARCHAR(10), CTE.idChild)
FROM CTE
WHERE B.idParent = CTE.idParent
ORDER BY CTE.idChild
FOR XML PATH('')
),
1,
1,
''
) AS Childs
FROM CTE AS B
I have a table, let's say it looks like this:
c | p
=====
|1|3|
|2|1|
|7|5|
c stands for current and p stands for parent
Given a c value of 2 I would return its top most ancestor (which has no parent) this value is 3. Since this is a self referencing table, I figured using CTE would be the best method however I am very new to using it. Nevertheless, I gave it a shot:
WITH Tree(this, parent) AS
( SELECT c ,p
FROM myTable
WHERE c = '2'
UNION ALL
SELECT M.c ,M.p
FROM myTable M
JOIN Tree T ON T.parent = M.c )
SELECT parent
FROM Tree
However this returns:
1
3
I only want 3 though. I have tried putting WHERE T.parent <> M.c but that doesn't entirely make sense. Neadless to say, I am a little confused for how to isolate the grandparent.
DECLARE #Table AS TABLE (Child INT, Parent INT)
INSERT INTO #Table VALUES (1,3),(2,1),(7,5)
;WITH cteRecursive AS (
SELECT
OriginalChild = Child
,Child
,Parent
,Level = 0
FROM
#Table
WHERE
Child = 2
UNION ALL
SELECT
c.OriginalChild
,t.Child
,t.Parent
,Level + 1
FROM
cteRecursive c
INNER JOIN #Table t
ON c.Parent = t.Child
)
SELECT TOP 1 TopAncestor = Parent
FROM
cteRecursive
ORDER BY
Level DESC
Use a recursive cte to Recuse up the tree until you cannot. Keep track of the Level of recursion, then take the last level of recursions parent and you have the top ancestor.
And just because I wrote it I will add in if you wanted to find the top ancestor of every child. The concept is still the same but you would need to introduce a row_number() to find the last level that was recursed.
DECLARE #Table AS TABLE (Child INT, Parent INT)
INSERT INTO #Table VALUES (1,3),(2,1),(7,5),(5,9)
;WITH cteRecursive AS (
SELECT
OriginalChild = Child
,Child
,Parent
,Level = 0
FROM
#Table
UNION ALL
SELECT
c.OriginalChild
,t.Child
,t.Parent
,Level + 1
FROM
cteRecursive c
INNER JOIN #Table t
ON c.Parent = t.Child
)
, cteTopAncestorRowNum AS (
SELECT
*
,TopAncestorRowNum = ROW_NUMBER() OVER (PARTITION BY OriginalChild ORDER BY Level DESC)
FROM
cteRecursive
)
SELECT
Child = OriginalChild
,TopMostAncestor = Parent
FROM
cteTopAncestorRowNum
WHERE
TopAncestorRowNum = 1
I am executing the following query :
WITH rg(id, childid, NAME)
AS (SELECT id,
0 ChildID,
NAME
FROM kuaf
WHERE id = 1000
UNION ALL
SELECT c.id,
c.childid,
k.NAME
FROM kuafchildren c
JOIN rg p
ON ( c.childid = p.id )
JOIN kuaf k
ON ( c.id = k.id ))
SELECT DISTINCT ka.id
FROM rg,
lm_approvals app
JOIN kuaf ka
ON ( ka.id = app.useridentifier )
JOIN dtree dt
ON ( dt.dataid = app.dataid )
WHERE app.dataid = 25773
AND app.isapproved = 0
AND Replace(ka.NAME, ka.type + '_', dt.ownerid + '_') = rg.NAME
But I am ending with the following error:
Types don't match between the anchor and the recursive part in column
"ChildID" of recursive query "rg".
If you change the first SELECT or the anchor part of the resursive CTE as follows, it will work
SELECT id,
CAST(0 as int) ChildID,
NAME
FROM kuaf
WHERE id = 1000
This is the case we experience generally with empty string or NULL is selected at this step.
So explicitely defining the type at anchor SELECT is required for CTEs
I myself have not tried this
Judging by the documentation
it is necessary to explicitly convert
0 type like kuafchildren.childid
through
CAST ( expression AS data_type [ ( length ) ] )
or
CONVERT ( data_type [ ( length ) ] , expression [ , style ] )
expression - 0
data_type [ ( length ) ] ) same as kuafchildren.childid
You can try to write instead
0 ChildID
kuaf.id*0 ChildID
This might work if kuaf.id type the same as the kuafchildren.childid
Try this ...
;WITH CTE RG AS (
SELECT Id,
0 AS 'ChildId',
Name
FROM Kuaf
WHERE id = 1000
UNION ALL
SELECT C.Id,
C.ChildId,
C.Name
FROM KuafChildren C
INNER JOIN RG ON (C.ChildId = RG.ChildId)
)
SELECT ...
I assume ChildId column datatype is int