sql - Deletion in closure table with multiple same paths - sql

I have the following hierarchical structure:
A -> E -> C -> D
|
|
|-> B -> D
Here is the closure table I've come up with:
| Ancestor | Descendant | Depth |
| A | A | 0 |
| B | B | 0 |
| C | C | 0 |
| D | D | 0 |
| E | E | 0 |
| A | E | 1 |
| A | B | 1 |
| A | C | 2 |
| E | C | 1 |
| A | D | 3 |
| E | D | 2 |
| C | D | 1 |
| A | D | 2 |
| B | D | 1 |
I want to remove the link between B and D, and therefore I want to delete the link between A and D (the one of depth 2). The problem is that I don't want to delete the link between A and D of depth 3 since I didn't delete the link between C and D.
For the moment, here is the SQL statement to list the links I want to delete:
SELECT link.ancestor, link.descendant, link.depth
FROM closure_table p,
closure_table link,
closure_table c
WHERE p.ancestor = link.ancestor
AND c.descendant = link.descendant
AND p.descendant = B
AND c.ancestor = D;
but this statement give me rows I don't want to delete:
| Ancestor | Descendant | Depth |
| A | D | 2 |
| A | D | 3 | <- As said before, I want to keep this one
| B | D | 1 |

You can select the ancestor-descendant pair that has the minimum depth of all of those same ancestor-descendant pairs:
with edges(s, e) as (
-- the pairs to be removed
select 'A', 'D'
union all
select 'B', 'D'
),
n_l as (
select c.* from closure c where c.ancestor != c.descendant
)
select c.* from n_l c where exists (select 1 from edges e where e.s = c.ancestor and e.e = c.descendant)
and c.depth = (select min(c1.depth) from n_l c1 where c1.ancestor = c.ancestor and c1.descendant = c.descendant);
Output:
ancestor
descendant
depth
A
D
2
B
D
1

I think I’ve found the solution, for those who are interested:
declare #Descendant nchar(10) = 'D';
declare #Ancestor nchar(10) = 'B';
with cte as
(
select Ancestor, Depth
from closure_table
where Descendant = #Descendant
and Ancestor = #Ancestor
and Depth = 1
union all
select r.Ancestor, l.Depth + 1 as Depth
from cte as l
join closure_table as r on r.Descendant = l.Ancestor
where r.Depth = 1
)
delete closure_table
from closure_table
join cte on cte.Ancestor = closure_table.Ancestor and cte.Depth = closure_table.Depth
where closure_table.Descendant = #Descendant;

Related

Update multiple rows with values of from a specific row based on same ID

I want to optimize my query to use CTE and some windows functions for better improvement. I am updating existing rows with specified data from other row but they have the same ID number. The code is for MS SQL.
LinkTable:
ID | TYPE | value1 | Value2 | Value3
-----------------------------------------------
234 | MAT | a | b | c
234 | PMS | null | null | null
234 | AMN | null | null | null
45 | MAT | x | m | n
45 | LKM | null | null | null
45 | DFG | null | null | null
3 | MAT | k | s | q
3 | LKM | null | null | null
The result should be:
ID | TYPE | value1 | Value2 | Value3
-----------------------------------------------
234 | MAT | a | b | c
234 | PMS | a | b | c
234 | AMN | a | b | c
45 | MAT | x | m | n
45 | LKM | x | m | n
45 | DFG | x | m | n
3 | MAT | k | s | q
3 | LKM | k | s | q
I used this code:
UPDATE m
SET m.[value1] = l.[value1]
, m.[value2] = l.[value2]
, m.[value2] = l.[value3]
FROM #LinkTable m
INNER JOIN #LinkTable l on l.[ID] = m.[ID]
WHERE l.[type] = 'MAT'
It updates also the main row from which i take the values.
Could anyone help?
Your code is basically fine, but I would add some filters:
UPDATE m
SET m.[value1] = l.[value1],
m.[value2] = l.[value2],
m.[value3] = l.[value3]
FROM #LinkTable m JOIN
#LinkTable l
ON l.[ID] = m.[ID]
WHERE l.[type] = 'MAT' AND
m.type <> 'MAT';
Note: You also have an error in the SET clause. The column value2 is set twice.
You can use window functions like this:
UPDATE m
SET m.[value1] = m.MATvalue1
, m.[value2] = m.MATvalue2
, m.[value3] = m.MATvalue3
FROM (
SELECT *,
MATvalue1 = MIN(CASE WHEN m.[type] = 'MAT' THEN l.value1 END) OVER (PARTITION BY m.ID),
MATvalue2 = MIN(CASE WHEN m.[type] = 'MAT' THEN l.value2 END) OVER (PARTITION BY m.ID),
MATvalue3 = MIN(CASE WHEN m.[type] = 'MAT' THEN l.value3 END) OVER (PARTITION BY m.ID)
FROM #LinkTable m
) m
WHERE m.[type] <> 'MAT';
Note that this may not necessarily be more performant than Gordon's answer

SQL select all rows that are not equal to an id, and replace the id column with the value - without cross join

Say I have a table like this:
+----+-------+
| id | value |
+----+-------+
| 1 | a |
| 1 | b |
| 2 | c |
| 2 | d |
| 3 | e |
| 3 | f |
+----+-------+
And I want to select all rows with id that are not a, and change their id to a; select all rows with id that are not b, and change the id to b; and select all rows with id that are not c, and change their id to c.
Here is the output I want:
+----+-------+
| id | value |
+----+-------+
| 1 | c |
| 1 | d |
| 1 | e |
| 1 | f |
| 2 | a |
| 2 | b |
| 2 | e |
| 2 | f |
| 3 | a |
| 3 | b |
| 3 | c |
| 3 | d |
+----+-------+
The only solution I can think of is through cross join and distinct:
select distinct a.id, b.value
from table a
cross join table b
where a.id != b.id
Is there any other way to avoid such expensive operation?
I think the typical way to write this is to generate all pairs of id and value and then remove the ones that exist:
select i.id, v.value
from (select distinct id from t) i cross join
(select distinct value from t) v left join
t
on t.id = i.id and t.value = i.value
where t.id is null;
First, I don't think this is what your query does. But this is what you seem to be describing.
From a performance perspective, you might have other sources for i and v that don't require subqueries. If so, use those for performance.
Finally, I don't think you can do much to improve the performance of this, apart from using explicit tables -- and perhaps having appropriate indexes on all the tables.

Identifying heirarchical groupings from a Parent-Child associaiton list in SQL

I am trying to identify groupings of accounts from a Parent-Child association table in SQL. Rather than a big hierarchy tree, I am dealing with many small trees and I need to identify each Tree as a unique Group in order to label related accounts.
I have two tables, a table of all Unique ID's:
+------+-------+
| ID | Group |
+------+-------+
| A | NULL |
| B | NULL |
| C | NULL |
| etc. | NULL |
+------+-------+
And a Table showing Parent - Child association between them:
+--------+-------+
| Parent | Child |
+--------+-------+
| A | D |
| A | E |
| B | F |
| B | G |
| B | C |
| C | H |
+--------+-------+
I Need to Fill the Group field of my first table so that I can identify all accounts which have a direct or indirect relationship eg:
+----+-------+
| ID | Group |
+----+-------+
| A | 1 |
| B | 2 |
| C | 2 |
| D | 1 |
| E | 1 |
| F | 2 |
| G | 2 |
| H | 2 |
+----+-------+
Where I'm struggling is that a Parent could be a Child to another Parent eg:
Parent B -> Parent -> C -> Child H
These form a Group but there is no direct link between B and H and I am struggling to find a reliable way to identify all associated ID's
This type of logic requires a recursive CTE. The idea is to start at the parents and work your way down the hierarchy:
with cte as (
select row_number() over (order by node) as grp,
n.node as ultimate_parent, n.node as node, 1 as lev
from nodes n
where not exists (select 1 from pc where pc.child = n.node)
union all
select cte.grp, cte.ultimate_parent, pc.child, lev + 1
from cte join
pc
on cte.node = pc.parent
)
update nodes
set grp = cte.grp
from cte
where cte.node = nodes.node;
Here is a db<>fiddle.

Get column value if it matches another column value in the same table

I have a SQLite table with comments like:
Id | replyId | commentID_parentID | usernameChannelId | channelId
1 | NULL | NULL | a | g
2 | NULL | NULL | b | k
NULL | 1.k | 1 | a | p
NULL | 1.p | 1 | c | i
3 | NULL | NULL | d | h
NULL | 2.k | 2 | g | g
and a table with channels like:
I want to know which user (userChannelId) replied to which user.
So I take a row with a comment and check if:
Id == NULL? Then it's a reply -> get userChannelId where commentID_parentID == Id
Id != NULL? Then it's a main comment -> userChannelId replied to channelId
And result should be:
userChannelId_Source | userChannelId_Target
a | g
b | k
a | a
c | a
g | b
Comment "d" has no entry where commentID_parentID == Id so it's left out.
How can I do that in SQL when I query in the same table?
It's a rather complicated requirement but I think that a conditional self join will do it:
select t.usernameChannelId userChannelId_Source,
case
when t.id is not null then tt.channelId
else tt.usernameChannelId
end userChannelId_Target
from tablename t inner join tablename tt
on tt.id = coalesce(t.id, t.commentID_parentID)
and exists (
select 1 from tablename
where commentID_parentID = t.id
or (commentID_parentID is null and t.id is null)
)
See the demo.
Results:
| userChannelId_Source | userChannelId_Target |
| -------------------- | -------------------- |
| a | g |
| a | a |
| c | a |
| b | k |
| g | b |

select Parent Child Rows in same Table

I have table structure like below
id |parent|value
1 | 0 | a |
2 | 1 | b |
3 | 4 | c |
4 | 0 | d |
5 | 0 | e |
I want to display only rows that have a relation parent child
like:
id |parent|value
1 | 0 | a |
2 | 1 | b |
3 | 4 | c |
4 | 0 | d |
every child should have a parent
every parent should have at least one child
This is my code but it does not work properly:
select a.id, a.parent,a.value
from myTable a inner join myTable b
on a.id = b.parent
union
select b.id, b.child,b.value
from myTable a inner join myTable b
on a.id = b.parent;
SELECT
*
FROM
yourTable
WHERE
parent != 0
OR EXISTS (SELECT *
FROM yourTable children
WHERE chilren.parent = yourTable.id
)
The first condition checks if the row points to a parent, and the second condition checks if the row has any children.