How to improve simple but inefficient parent/child relationship query - sql

I have a simple table as described below, where there's a possible parent/child relationship. This is actually a very large table, but this is an fair representation. There are no "grandchildren" relationships.
I need to transform this into a slightly different table which is filtered on some input value.
declare #pc table ( myId char(1) not null, parentId char(1) );
insert into #pc (myId, parentId) values ('A', null)
insert into #pc (myId, parentId) values ('B', 'A')
insert into #pc (myId, parentId) values ('C', 'A')
insert into #pc (myId, parentId) values ('D', null)
insert into #pc (myId, parentId) values ('E', null)
insert into #pc (myId, parentId) values ('F', 'E')
insert into #pc (myId, parentId) values ('G', null)
insert into #pc (myId, parentId) values ('H', 'G')
insert into #pc (myId, parentId) values ('I', 'G')
insert into #pc (myId, parentId) values ('J', 'G')
insert into #pc (myId, parentId) values ('K', null)
-- This is the results I need
declare #target table ( myId char(1) not null, parentId char(1), hasFamily bit );
Given input of "A", then I need three rows like this:
A NULL 1
B A 1
C A 1
In other words, I need everything that belongs to the "family group" A.
Given "B" I need the same output, everything in B's family group (which happens to be A's):
A NULL 1
B A 1
C A 1
Given "D" I need just the one line since no-one is in D's family:
D NULL 0
Given null, I need the entire table data set but with the proper rows marked as "having family" or not.
Here's my attempt which is technically correct, but it's not efficient at all taking 3 passes at the data to do it:
declare #testcase char(1) = 'B';
-- The inefficient method
INSERT INTO #target( myId,parentId )
SELECT pc.myId, pc.parentId
FROM #pc pc
WHERE(pc.myId = ISNULL(#testcase, pc.myId))
OR (pc.parentId =#testcase);
INSERT INTO #target( myId,parentId )
SELECT pc.myId, pc.parentId
FROM #pc pc
WHERE pc.myId IN ( SELECT parentId FROM #target )
AND pc.myId NOT IN ( SELECT myId FROM #target );
update t
set t.hasFamily = 1
from #target t
left outer join #target t2
on t.myId = t2.parentId
where t.parentId is not null or t2.myId is not null;
Can you see a better way of looking at this problem?

A window function to count children, made available on every row, simplifies the wanted queries. How you chose to deploy it is u to you. You might choose to use persisted computed columns for example, or triggers. If scale and performance are issues then you would need to consider the indexing and inspect execution plans.
Perhaps I don't see the complexity from the small model you have presented here hence my suggestions may be too simplistc.
Demo at SQL Fiddle
create table Table1 ( myId char(1) not null, parentId char(1) );
insert into Table1 (myId, parentId) values ('A', null);
insert into Table1 (myId, parentId) values ('B', 'A');
insert into Table1 (myId, parentId) values ('C', 'A');
insert into Table1 (myId, parentId) values ('D', null);
insert into Table1 (myId, parentId) values ('E', null);
insert into Table1 (myId, parentId) values ('F', 'E');
insert into Table1 (myId, parentId) values ('G', null);
insert into Table1 (myId, parentId) values ('H', 'G');
insert into Table1 (myId, parentId) values ('I', 'G');
insert into Table1 (myId, parentId) values ('J', 'G');
insert into Table1 (myId, parentId) values ('K', null);
create view too_simplistic as
select
myId
, parentId
, coalesce(parentId, myId) parent_flat
, case when count(*) over(partition by coalesce(parentId, myId))-1 = 0
then 0
else 1
end as hasFamily
from table1
;
Query 1:
declare #want char(1) = 'A'
select myId, parentId, hasFamily
from too_simplistic
where parent_flat = (select parent_flat from too_simplistic where MyId = #want)
or MyId = #want
Results:
| myId | parentId | hasFamily |
|------|----------|-----------|
| A | (null) | 1 |
| B | A | 1 |
| C | A | 1 |
Query 2:
declare #want char(1) = 'B'
select myId, parentId, hasFamily
from too_simplistic
where parent_flat = (select parent_flat from too_simplistic where MyId = #want)
or MyId = #want
Results:
| myId | parentId | hasFamily |
|------|----------|-----------|
| A | (null) | 1 |
| B | A | 1 |
| C | A | 1 |
Query 3:
declare #want char(1) = 'D'
select myId, parentId, hasFamily
from too_simplistic
where parent_flat = (select parent_flat from too_simplistic where MyId = #want)
or MyId = #want
Results:
| myId | parentId | hasFamily |
|------|----------|-----------|
| D | (null) | 0 |
Query 4:
select *
from too_simplistic
Results:
| myId | parentId | parent_flat | hasFamily |
|------|----------|-------------|-----------|
| A | (null) | A | 1 |
| B | A | A | 1 |
| C | A | A | 1 |
| D | (null) | D | 0 |
| E | (null) | E | 1 |
| F | E | E | 1 |
| G | (null) | G | 1 |
| H | G | G | 1 |
| I | G | G | 1 |
| J | G | G | 1 |
| K | (null) | K | 0 |
select d.*
, case when children = 0 then 0 else 1 end as hasFamily
from (
select *
, count(*) over(partition by coalesce(parentId, myId))-1 children
from #pc
) d
;

Related

Remove duplicate value in different categories in same table SQL but keep the first category value

Let's say I have a table with id and category like the table below
D_id | D_category
-----------------
1 | A
2 | A
3 | A
1 | B
2 | B
4 | B
5 | B
1 | C
2 | C
4 | C
5 | C
6 | C
Hence the rules are like this
values in category A should not be appear in category B and category C
values in category B should not be appear in category C
The end result should be like this
D_id | D_category
-----------------
1 | A
2 | A
3 | A
4 | B
5 | B
6 | C
I will provide a solution that works but its not an ideal solution can anyone help me to provide a better solution in case there are more categories meaning that if there are more category then it should follow the rules the values in previous categories should not appear in any other categories
DECLARE #A TABLE(
D_id INT NOT NULL,
D_category VARCHAR(MAX));
INSERT INTO #A(D_id,D_category)
VALUES (1, 'A'),
(2, 'A'),
(3, 'A'),
(1, 'B'),
(2, 'B'),
(4, 'B'),
(5, 'B'),
(1, 'C'),
(2, 'C'),
(4, 'C'),
(5, 'C'),
(6, 'C')
DELETE t
FROM #A t
WHERE t.D_category = 'B' AND EXISTS (SELECT 1 FROM #A t2 WHERE t2.D_category = 'A' and t.D_id = t2.D_id)
DELETE t
FROM #A t
WHERE t.D_category = 'C' AND EXISTS (SELECT 1 FROM #A t2 WHERE t2.D_category = 'B' and t.D_id = t2.D_id)
DELETE t
FROM #A t
WHERE t.D_category = 'C' AND EXISTS (SELECT 1 FROM #A t2 WHERE t2.D_category = 'A' and t.D_id = t2.D_id)
select * from #A
Just check that the specified record doesn't exist earlier in the sequence.
select *
from #A A1
where not exists (
select 1
from #A A2
where A2.D_id = A1.D_id
and A2.D_category < A1.D_category
)
or just make use of row_number()
select *
from
(
select *, r = row_number() over (partition by D_id order by D_category)
from #A
) a
where a.r = 1
Delete using the join syntax:
delete a
from my_table a
join my_table b on a.D_id = b.D_id
and a.D_category > b.D_category
See live demo.

SQL select parent-child recursively based on a reference table

I saw many questions related to a recursive query but couldn't find any that shows how to use it based on a reference table.
I have a MasterTable where Id, ParentId columns are establishing the parent/child relation.
I have a SubTable where I have a bunch of Ids which could be a parent Id or child Id.
I would like to retrieve all related records (parent or child, recursively) from the MasterTable based on given SubTable
Current output:
id parentId
----------- -----------
1 NULL
2 1
3 1
4 NULL
5 4
6 5
7 6
Expected output
id parentId
----------- -----------
1 NULL
2 1
3 1
4 NULL
5 4
6 5
7 6
8 9
9 NULL
10 NULL
11 10
13 11
14 10
15 16
16 NULL
Comparison of actual vs expected:
Code:
DECLARE #MasterTable TABLE
(
id INT NOT NULL,
parentId INT NULL
);
DECLARE #SubTable TABLE
(
id INT NOT NULL
);
INSERT INTO #MasterTable (id, parentId)
VALUES (1, NULL), (2, 1), (3, 1), (4, NULL), (5, 4), (6, 5),
(7, 6), (8, 9), (9, NULL), (10, NULL), (11, 10), (12, NULL),
(13, 11), (13, 11), (14, 10), (15, 16), (16, NULL);
INSERT INTO #SubTable (id)
VALUES (1), (2), (3), (4), (6), (5), (7),
(8), -- it does not show
(13), -- it does not show
(15); -- it does not show
/* beside 8,13,15 it should add 9,11,14 and 10,16 */
;WITH cte AS
(
SELECT
mt1.id,
mt1.parentId
FROM
#MasterTable AS mt1
WHERE
mt1.parentId IS NULL
AND EXISTS (SELECT NULL AS empty
FROM #SubTable AS st
WHERE st.Id = mt1.id)
UNION ALL
SELECT
mt2.id,
mt2.parentId
FROM
#MasterTable AS mt2
INNER JOIN
cte AS c1 ON c1.id = mt2.parentId
)
SELECT DISTINCT
c2.id,
c2.parentId
FROM
cte AS c2
ORDER BY
id;
Is the following query suitable for the issue in question?
with
r as(
select
m.*, iif(m.parentid is null, 1, 0) p_flag
from #MasterTable m
join #SubTable s
on s.id = m.id
union all
select
m.*, iif(m.parentid is null, 1, r.p_flag)
from r
join #MasterTable m
on (r.p_flag = 1 and m.parentid = r.id) or
(r.p_flag = 0 and r.parentid = m.id)
)
select distinct
id, parentid
from r
order by id;
Output:
| id | parentid |
+----+----------+
| 1 | NULL |
| 2 | 1 |
| 3 | 1 |
| 4 | NULL |
| 5 | 4 |
| 6 | 5 |
| 7 | 6 |
| 8 | 9 |
| 9 | NULL |
| 10 | NULL |
| 11 | 10 |
| 13 | 11 |
| 14 | 10 |
| 15 | 16 |
| 16 | NULL |
Test it online with rextester.com.
;WITH cte
AS (
SELECT mt1.id,
mt1.parentId
FROM #MasterTable AS mt1
WHERE mt1.parentId IS NULL
UNION ALL
SELECT mt2.id,
mt2.parentId
FROM #MasterTable AS mt2
INNER JOIN cte AS c1
ON c1.id = mt2.parentId
)
SELECT DISTINCT c2.id,
c2.parentId
FROM cte AS c2
where
EXISTS (
SELECT 1 AS empty FROM #SubTable AS st
WHERE ( st.Id = c2.id or st.Id = c2.parentId)
)
or
EXISTS (
SELECT 1 AS empty FROM #MasterTable AS mt
WHERE ( c2.Id = mt.parentId or c2.parentId = mt.parentId)
)
ORDER BY id;
You may try this....
; with cte as(
select distinct mas.id, mas.parentId, iif(mas.parentid is null, 1, 0) PId
from #MasterTable mas inner join #SubTable sub
on sub.id in(mas.id, mas.parentid) ----- create top node having parentid is null
union all
select mas.id, mas.parentId, ct.PId
from cte ct inner join #MasterTable mas
on (ct.PId = 1 and mas.parentid = ct.id) or
(ct.PId = 0 and ct.parentid = mas.id) ----- create child node for correspoding parentid created above
)
select distinct id, parentid from cte order by id
option (MAXRECURSION 100); ---- Add Maxrecursion to prevent the infinite loop
You can find this link for more info on recursive query in SQL link. In this link see Example E or above.

Join with dynamic Table and Column names

I'm trying to join from a table where the tables and fields are defined within the data instead of keys. So here is what I have
Table Root:
ID | Table | Field
---+---------+-----------
1 | Tab1 | Field1
2 | Tab2 | Field2
3 | Tab1 | Field2
4 | Tab3 | Field4
5 | Tab1 | Field1
Tab1
ID | Field1
---+---------
1 | A
2 | B
3 | C
4 | D
Tab2
ID | Field1 |Field2
---+--------+-----------
1 | X | Bla
2 | Y | 123
3 | Z | 456
Tab3 does not exist
I'd like to have a result like that one:
ID | Value
---+---------
1 | A
2 | 123
3 | NULL -- Field does not match
4 | NULL -- Tables does not exist
5 | NULL -- ID does not exist
Basicly trying to join using the the ID trageting a dynamic table and field.
My Starting Point is somehwere around Here, but this is just for a single specific table. I can't figure out how to join dynamicly or if it even possible without dynamic sql like exec.
you could solve this with a case expression and subqueries, like this example
declare #root table (id int, [table] varchar(10), Field varchar(10))
declare #tab1 table (id int, Field1 varchar(10))
declare #tab2 table (id int, Field1 varchar(10), Field2 varchar(10))
insert into #root (id, [table], Field)
values (1, 'Tab1', 'Field1'), (2, 'Tab2', 'Field2'), (3, 'Tab1', 'Field2'), (4, 'Tab3', 'Field4'), (5, 'Tab1', 'Field1')
insert into #tab1 (id, Field1)
values (1, 'A'), (2, 'B'), (3, 'C'), (4, 'D')
insert into #tab2 (id, Field1, Field2)
values (1, 'X', 'Bla'), (2, 'Y', '123'), (3, 'Z', '456')
select r.id,
case when r.[Table] = 'Tab1' and r.Field = 'Field1' then (select t1.Field1 from #tab1 t1 where t1.ID = r.ID)
when r.[Table] = 'Tab2' and r.Field = 'Field1' then (select t2.Field1 from #tab2 t2 where t2.id = r.id)
when r.[Table] = 'Tab2' and r.Field = 'Field2' then (select t2.Field2 from #tab2 t2 where t2.id = r.id)
end as Value
from #root r
the result is
id Value
-- -------
1 A
2 123
3 null
4 null
5 null

SQL : how to find leaf rows?

i have a self related table myTable like :
ID | RefID
----------
1 | NULL
2 | 1
3 | 2
4 | NULL
5 | 2
6 | 5
7 | 5
8 | NULL
9 | 7
i need to get leaf rows on any depth
based on the table above, the result must be :
ID | RefID
----------
3 | 2
4 | NULL
6 | 5
8 | NULL
9 | 7
thank you
PS: the depth may vary , here is very small example
Try:
SELECT id,
refid
FROM mytable t
WHERE NOT EXISTS (SELECT 1
FROM mytable
WHERE refid = t.id)
DECLARE #t TABLE (id int NOT NULL, RefID int NULL);
INSERT #t VALUES (1, NULL), (2, 1), (3, 2), (5, NULL),
(6, 5), (4, NULL), (7, 5), (8, NULL), (9, 8), (10, 7);
WITH CTE AS
(
-- top level
SELECT id, RefID, id AS RootId, 0 AS CTELevel FROM #t WHERE REfID IS NULL
UNION ALL
SELECT T.id, T.RefID, RootId, CTELevel + 1 FROM #t T JOIN CTE ON T.RefID = CTE.id
), Leafs AS
(
SELECT
id, RefID, DENSE_RANK() OVER (PARTITION BY CTE.RootId ORDER BY CTELevel DESC) AS Rn
FROM CTE
)
SELECT
id, RefID
FROM
Leafs
WHERE
rn = 1
select ID, RefId
from myTable t1 left join myTable t2 on t1.ID = t2.RefID
where t2.RefID is null
try this:
SELECT *
FROM
my_table
WHERE
id NOT IN
(
SELECT DISTINCT
refId
FROM
my_table
WHERE
refId IS NOT NULL
)

double sorted selection from a single table

I have a table with an id as the primary key, and a description as another field.
I want to first select the records that have the id<=4, sorted by description, then I want all the other records (id>4), sorted by description. Can't get there!
select id, descr
from t
order by
case when id <= 4 then 0 else 1 end,
descr
select *, id<=4 as low from table order by low, description
You may want to use an id <= 4 expression in your ORDER BY clause:
SELECT * FROM your_table ORDER BY id <= 4 DESC, description;
Test case (using MySQL):
CREATE TABLE your_table (id int, description varchar(50));
INSERT INTO your_table VALUES (1, 'c');
INSERT INTO your_table VALUES (2, 'a');
INSERT INTO your_table VALUES (3, 'z');
INSERT INTO your_table VALUES (4, 'b');
INSERT INTO your_table VALUES (5, 'g');
INSERT INTO your_table VALUES (6, 'o');
INSERT INTO your_table VALUES (7, 'c');
INSERT INTO your_table VALUES (8, 'p');
Result:
+------+-------------+
| id | description |
+------+-------------+
| 2 | a |
| 4 | b |
| 1 | c |
| 3 | z |
| 7 | c |
| 5 | g |
| 6 | o |
| 8 | p |
+------+-------------+
8 rows in set (0.00 sec)
Related post:
Using MySql, can I sort a column but have 0 come last?
select id, description
from MyTable
order by case when id <= 4 then 0 else 1 end, description
You can use UNION
SELECT * FROM (SELECT * FROM table1 WHERE id <=4 ORDER by description)aaa
UNION
SELECT * FROM (SELECT * FROM table1 WHERE id >4 ORDER by description)bbb
OR
SELECT * FROM table1
ORDER BY
CASE WHEN id <=4 THEN 0
ELSE 1
END, description