Troubles isolating target cell in recursive sql query - sql

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

Related

Use Exists with a Column of Query Result?

I have 2 tables.
One is bom_master:
CHILD
PARENT
1-111
66-6666
2-222
77-7777
2-222
88-8888
3-333
99-9999
Another one is library:
FileName
Location
66-6666_A.step
S:\ABC
77-7777_C~K1.step
S:\DEF
And I want to find out if the child's parents have related files in the library.
Expected Result:
CHILD
PARENT
FileName
1-111
66-6666
66-6666_A.step
2-222
77-7777
77-7777_C~K1.step
Tried below lines but return no results. Any comments? Thank you.
WITH temp_parent_PN(parentPN)
AS
(
SELECT
[PARENT]
FROM [bom_master]
where [bom_master].[CHILD] in ('1-111','2-222')
)
SELECT s.[filename]
FROM [library] s
WHERE EXISTS
(
SELECT
*
FROM temp_parent_PN b
where s.[filename] LIKE '%'+b.[parentPN]+'%'
)
If you have just one level of dependencies use the join solution proposed by dimis164.
If you have deeper levels you could use recursive queries allowed by WITH clause (
ref. WITH common_table_expression (Transact-SQL)).
This is a sample with one more level of relation in bom_master (you could then join the result of the recursive query with library as you need).
DECLARE #bom_master TABLE (Child NVARCHAR(MAX), Parent NVARCHAR(MAX));
INSERT INTO #bom_master VALUES
('1-111', '66-666'),
('2-222', '77-777'),
('3-333', '88-888'),
('4-444', '99-999'),
('A-AAA', '1-111');
WITH
leaf AS ( -- Get the leaf elements (elements without a child)
SELECT Child FROM #bom_master b1
WHERE NOT EXISTS (SELECT * FROM #bom_master b2 WHERE b2.Parent = b1.Child) ),
rec(Child, Parent, Level) AS (
SELECT b.Child, b.Parent, Level = 1
FROM #bom_master b
JOIN leaf l ON l.Child = b.Child
UNION ALL
SELECT rec.Child, b.Parent, Level = rec.Level + 1
FROM rec
JOIN #bom_master b
ON b.Child = rec.Parent )
SELECT * FROM rec
I think you don't have to use exists. The problem is that you need to substring to match the join.
Have a look at this:
SELECT b.CHILD, b.PARENT, l.[FileName]
FROM [bom_master] b
INNER JOIN [library] l ON b.PARENT = SUBSTRING(l.FileName,1,7)

Recursive parent child problem in MariaDB

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.

Recursive Parent/Child in same table query in SQL where parent is PK

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

Postgres recursive query with row_to_json

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;

Tree structure in SQL Server 2008

In SQL Server 2008;
I have a tree. I need to get all child nodes of node n (see diagram) and all child nodes of these child nodes, etc until the leaf nodes which is fairly trivial. I also need to be able to say 'Take node o, go up the tree until we reach m and because m is a child of node n set some property of node o to some property of node m. Node o could be 3 levels deep (as illustrated) or 45 levels deep, x levels deep.
This gets all children of a given node (or area)
--Return all sub-area structure of an area:
WITH temp_areas (ParentAreaID, AreaID, [Name], [Level]) AS
(
SELECT ParentAreaID, AreaID, [Name], 0
FROM lib_areas WHERE AreaID = #AreaID
UNION ALL
SELECT B.ParentAreaID, B.AreaID, B.[Name], A.Level + 1
FROM temp_areas AS A, lib_areas AS B
WHERE A.AreaID = B.ParentAreaID
)
INSERT INTO #files (id) SELECT fileid FROM lib_filesareasxref where areaid in (select areaid from temp_areas)
while exists (select * from #files)
begin
select top 1
#ID = id
from
#files ORDER BY id DESC
delete from #files where id = #id
This will track back from #node_o until it reaches #node_m or it reaches the top of the tree (if #node_m is not above #node_o).
WITH
parents
AS
(
SELECT
A.ParentAreaID, A.AreaID, A.[Name], 0
FROM
lib_areas AS A
WHERE
A.AreaID = #node_o
UNION ALL
SELECT
A.ParentAreaID, A.AreaID, A.[Name], B.Level + 1
FROM
lib_areas AS A
INNER JOIN
parents AS B
ON A.AreaID = B.ParentAreaID
WHERE
B.AreaID <> #node_m
)
SELECT
*
FROM
parents
I'd propose using a HierarchyID data type in your table, and using the GetAncestor method