Look for data where child doesn't exisit - sql

How to look in Table C for those inspectors who have got ParentID but not child.
Table A has both parent and child data. Parent ID 0 is for parents and child has their parent ID.
In Table C, one inspector can have many parents and many childs.
I need to run a query to look for those inspectors who have got parents but not child.
Table A Table B Table C
-------- ------- -------
DisciplineID(PK) InspectorID(PK) ID (PK)
ParentID DisciplineID(FK)
InspectorID (Fk)
Table A Table C
In above mentioned data, Inspector 7239 and 7240 only have parent but not child. So query should return those two not 7242 because he has both parent and childs.

Use EXISTS and NOT EXISTS:
SELECT c.ID, c.InspectorID, c.DisciplineID
FROM dbo.TableC c
WHERE EXISTS
(
SELECT 1 FROM dbo.TableA a
WHERE a.DisciplineID = c.DisciplineID
AND a.ParentID = 0 -- parent exists
)
AND NOT EXISTS
(
SELECT 1 FROM dbo.TableC c2
WHERE c.InspectorID = c2.InspectorID
AND c.ID <> c2.ID -- look for another record with this InspectorID
AND EXISTS
(
SELECT 1 FROM dbo.TableA a
WHERE a.DisciplineID = c2.DisciplineID
AND a.ParentID <> 0 -- no child exists
)
)

I would start with a pre-qualifying query per discipline based on those having a count of entries that HAVE a parent ID = 0 but also no records as child... Join that result to your TableC
SELECT
c.ID,
c.InspectorID,
c.DisciplineID
FROM
dbo.TableC c
JOIN ( select
a.DisciplineID
from
TableA a
group by
a.DisciplineID
having
sum( case when a.ParentID = 0 then 1 else 0 end ) > 0
AND sum( case when a.ParentID > 0 then 1 else 0 end ) = 0 ) qual
on c.DisciplineID = qual.DisciplineID

You can try this:
SELECT DISTINCT B.INSPECTORID FROM TABLEA A
LEFT JOIN TABLEC CHILD ON CHILD.DISCIPLINEID = A.DISCIPLINEID
LEFT JOIN TABLEC PARENT ON PARENT.DISCIPLINEID = A.PARENTID
JOIN TABLEB B ON A.INSPECTORID = B.INSPECTORID
WHERE (A.PARENTID = 0 AND CHILD.DISCIPLINEID IS NOT NULL)

Related

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.

SQL Query -IF EXISTS and WITH

Hi have a stored procedure with WITH keyword that returns a resultset as follows
WITH p as(...),
mp as (select *, x.name from p left join x on ...)
Now i want to say
IF EXISTS (select * from mp where pid is not null and id != pid)
THEN
BEGIN
SELECT * FROM cp left join mp on ...
END
ELSE
BEGIN SELECT * FROM mp where...
END
Here i am facing some syntax error this select should be returned back from stored procedure
UPDATE
I have put it as follows
SELECT DISTINCT *
FROM (
--get all parent records that matches RIDE ID
SELECT * FROM mp WHERE pid is null or pid = id
UNION --get parent of a child that matches RIDE ID
SELECT pl.* [i have specified exact same columns in this select]
FROM pl
left join mp on pl.plc_id = mp.PrimaryPlacementId
left join reg on reg.usr_id = mp.plc_usr and isnull(reg.usr_isdeleted, 0) = 0 --and reg.usr_active = 1
left join ut on ut.cut_id = mp.plc_unit
left join f on f.fac_id = mp.plc_facility and isnull(f.fac_isdeleted, 0) = 0
left join s on s.sem_id = mp.plc_semester and isnull(s.sem_isdeleted, 0) = 0
WHERE mp.pid != id AND mp.pid is not null
) x
This one seems to work ok - basically pid contains the parent id
My mp=pl table has the following structure
id | name | pid
1 | sam | 1
2 | sid | 1
so basically first is the parent record and second is the child record.
so what i want is - if i search 'sam' then record 1 is returned
and if i search 'sid' again record one is returned
i.e if u search by parent id or child id the parent record should be returned or those whose pid is null

SQL - where condition for multple child tables

I have some 10 tables in which 1 is parent and other 9 are parallel children.
All these 9 tables have a column named Version with values 0 & above. zero is draft.
I tried with JOINS but with joins I got ambiguity for Version Column.
Is there any way where I can say that any of these 9 child tables has any drafts
Required output is Parent table columns + HasDrafts (From child tables).
Is there any way to achieve this ? If yes then guide me please.
If you don't care which table has the drafts or how many, then you can use exists in a case expression:
select p.*,
(case when exists (select 1 from child1 c where c.parentid = p.parentid and c.version = 0) or
exists (select 1 from child2 c where c.parentid = p.parentid and c.version = 0) or
exists (select 1 from child3 c where c.parentid = p.parentid and c.version = 0) or
. . .
then 1 else 0
end) as has_drafts
from parent p;
I would have made a union and set an extra column for which table it is Ex.
Select ID, Version, 'tableA' from TableA
union
Select ID, Version, 'tableB' from TableB
Thats my dirty solution.

Simple SQL select overloads my server

Problem is that this Task, though looking easy challenges my SQL Server so I have to look for an easier one.
I have table items (with 300.000 rows)
item
-----
10
11
13
14
15
Then I have table parent-Child (with 900.000 rows)
Parent Child
--------------
10 13
14 13
13 15
For every row in items I want to find out if
item is at least one time in Parent and NOT in Child
item is in Parent AND Child
item is in Child AND NOT in Parent
is neither (ok - the rest...)
I want to write the related case a/b/c/d into a dedicated column in item table.
The approaches Select -> worked fine as long as there were few rows -> kill my server with growing log and extreme CPU load
-- Select with CTE
With G1 (P) as
(
select PARENT
from parent_child
where < ... condition>
), G2 (C) as
(
select CHILD
from parent_child
where < ... condition>
)
update Item_Master
set Item_Assembly_Level = '1'
where Item_Number in (select * from G1)
and Item_Number not in (select * from G2) ;
you can test this.
UPDATE I SET Item_Assembly_Level =
CASE
WHEN P.PARENT IS NOT NULL AND C.CHILD IS NULL THEN 1 --a) item is at least one time in Parent and NOT in Child
WHEN P.PARENT IS NOT NULL AND C.CHILD IS NOT NULL THEN 2 --b) item is in Parent AND Child
WHEN P.PARENT IS NULL AND C.CHILD IS NOT NULL THEN 3 --c) item is in Child AND NOT in Parent
ELSE 4 --d) is neither (ok - the rest...)
END
FROM Item_Master I
LEFT JOIN parent_child P ON I.Item_Number = P.PARENT
LEFT JOIN parent_child C ON I.Item_Number = C.CHILD
With G1 (P)
as (
select DISTINCT PARENT from parent_child
where < ... condition>
EXCEPT
select DISTINCT CHILD from parent_child
where < ... condition>
)
update Item_Master set Item_Assembly_Level = '1' where
Item_Number in (select P from G1)
;

SQL query number of parent

I have a table in my database called type
ID Name ParentID
---------------------
1 name1 0
2 name2 0
3 name3 1
4 name4 2
5 name1 1
I need to know how many parent (descendants) each type has
ID -------- descendants
ID-> 1 (have no parent)
ID-> 3 (have 1 parent (ID->1))
ID-> 5 (have two parent ((ID->3(ID->1))))
How can I write an optimized sql statement to do this using MySQL?
Also you could implement function to compute level
Sort of:
create function get_level(_id int) returns int
begin
declare _level int default -1;
repeat
set _level = _level + 1;
select parent_id
into _id
from your_table
where id = _id;
until _id is null
end repeat;
return _level;
end
Usage:
select id, get_level(id)
from your_table
I do not test a code.
That approach is not effective. In most cases advice to store and compute level on insert/update is better.
MySQL unfortunately doesn't support recursive CTE's. But if the number of parents is limited, you can implement this using joins:
select p.id
, case
<... more whens here ...>
when c3.id is not null then 3
when c2.id is not null then 2
when c1.id is not null then 1
else 0
end as NumberOfChildren
from yourtable p
left join yourtable c1 on c1.parentid = p.id
left join yourtable c2 on c2.parentid = c1.id
left join yourtable c3 on c3.parentid = c2.id
<... more joins here ...>
group by p.id