SQL Server: Recursive table update - sql-server

I have a table, let's say TestTable. This table has below columns:
ID1 | ID2 | ID3 | LEVEL | PARENT_LEVEL | ENABLED | OBSOLET
All columns are integers, ENABLED and OBSOLET only two possible values (0 or 1)
LEVEL Column can have a parent level, thi parent level another parent level and so on, for example, imagine following table content:
ID1 | ID2 | ID3 | LEVEL | PARENT_LEVEL | ENABLED | OBSOLET
1 6 7 98 NULL 1 0
1 6 6 99 98 1 0
1 4 6 100 99 1 0
1 2 3 200 100 1 0
2 4 1 300 NULL 0 0
3 3 4 400 NULL 0 1
3 4 5 500 400 0 0
ID1, ID2 and ID3 is the primary key.
So representing this in a tree:
+ 98
|__ 99
|__ 100
|___ 200
+ 300
+ 400
|__ 500
200 has 100 as parent, 100 has 99 as parent and 99 has 98 as parent.
300 has no parent.
500 has 400 as parent and 400 has no parent.
So what I need is an update query to update recursively the field 'ENABLED', for example:
If I update LEVEL 99 with ENABLED=1, also his parent, 98 must be updated to ENABLED=1 but not LEVELs 100 and 200.
If I update LEVEL 200 with ENABLED=1, also his parent, 100 must be updated with ENABLED=1, and also LEVELs 99 and 98, because they have parents as well.
If I update LEVEL 300 with ENABLED=1, only LEVEL 300 is updated because it has no parent.
So I need a recursive update query to update field ENABLED until LEVEL has no parent (PARENT_LEVEL). Also I need to update all the levels at once with one update query, not only execute update for a concret level.
Furthermore, on each update I need to check field 'OBSOLET', and if a LEVEL has field OBSOLET set to 1 it means that rollback has to be made, for example, taken into account above table content, if I update LEVEL 500 to ENABLED=1, no problem because its OBSOLET field is 0, so its field ENABLED is set to 1, then by recursive, we try to update its parent, LEVEL 400, to ENABLED=1, but as its OBSOLET field is set to 1 it means rollback needs to be made, that is, ENABLED field for LEVEL 400 is kept to 0 (not updated) and field ENABLED for level 500 that was set to 1 should be reverted to 0 as well.
The final problem is that this update query should be within a trigger on this table TestTable:
CREATE TRIGGER [dbo].[TG_TestTable]
ON [dbo].[TestTable]
FOR UPDATE
AS
IF UPDATE ([ENABLED])
BEGIN
// Update query must be here, so if field ENABLED is updated, trigger is fired again...so I don't know if disable trigger statement is necessary to be done before this update query and enable trigger after it.
END
This is because to activate the trigger, an update is performed on some rows of the table TestTable, for example:
UPDATE [dbo].[TestTable]
SET ENABLED = 1
WHERE
LEVEL IN (100,300,500);
so I have tried to make the update query within the trigger but I do not know how to finish it:
UPDATE [dbo].[TestTable]
SET ENABLED= inserted.ENABLED
..... // SOMETHING ELSE
FROM inserted
WHERE
[dbo].[TestTable].ID1 = inserted.ID1
AND
[dbo].[TestTable].ID2 = inserted.ID2
AND
[dbo].[TestTable].ID3 = inserted.ID3
AND
[dbo].[TestTable].PARENT_LEVEL = inserted.LEVEL;
So how can I achieve this? maybe using a recursive function or recursive CTE? or is better a recursive trigger on same table in terms of time execution and performance? All ideas will be welcome.

I've found a solution for this. It's quite a long one, but it's fairly easy to understand.
First, Create and populate sample table (Please save us this step in your future questions)
CREATE TABLE TestTable
(
ID1 int NOT NULL,
ID2 int NOT NULL,
ID3 int NOT NULL,
LEVEL int NOT NULL,
PARENT_LEVEL int,
ENABLED int,
OBSOLET int,
PRIMARY KEY (ID1, ID2, ID3)
)
INSERT INTO TestTable VALUES
(1, 6, 7, 98, NULL, 1, 0),
(1, 6, 6, 99, 98 , 0, 0),
(1, 4, 6, 100, 99 , 0, 0),
(1, 2, 3, 200, 100 , 0, 0),
(2, 4, 1, 300, NULL, 0, 0),
(3, 3, 4, 400, NULL, 0, 1),
(3, 4, 5, 500, 400 , 0, 0)
Then, Create an INSTEAD OF UPDATE trigger, that will only update the records matching your criteria.
Note: This will also update records where the enabled value was not changed, You'll see it in the code soon.
Answer code
CREATE TRIGGER tr_TestTable_IOU ON TestTable
INSTEAD OF UPDATE
AS
;WITH CTE AS
( -- A recursive cte to get all the parents of the updated records
SELECT i.ID1,
i.ID2,
i.ID3,
i.LEVEL,
i.PARENT_LEVEL,
i.ENABLED,
i.OBSOLET
FROM inserted i
INNER JOIN deleted d ON i.ID1 = d.ID1
AND i.ID2 = d.ID2
AND i.ID3 = d.ID3
WHERE i.ENABLED = 1
AND d.ENABLED = 0
-- The where clause will allow only records where enabled was changed from 0 to 1
UNION ALL
SELECT t.ID1,
t.ID2,
t.ID3,
t.LEVEL,
t.PARENT_LEVEL,
t.ENABLED,
t.OBSOLET
FROM TestTable t
INNER JOIN CTE ON t.LEVEL = CTE.PARENT_LEVEL
), CTE_OBSOLET AS
( -- A second recursive cte to get all the records where at least in one parent the value of OBSOLET = 1
SELECT i.ID1,
i.ID2,
i.ID3,
i.LEVEL,
i.PARENT_LEVEL,
i.ENABLED,
i.OBSOLET
FROM TestTable i
WHERE OBSOLET = 1
UNION ALL
SELECT t.ID1,
t.ID2,
t.ID3,
t.LEVEL,
t.PARENT_LEVEL,
t.ENABLED,
1
FROM TestTable t
INNER JOIN CTE_OBSOLET ON t.PARENT_LEVEL = CTE_OBSOLET.LEVEL
)
-- Update the enabled column to all relevant records (including parents)
UPDATE t
SET ENABLED = 1
FROM TestTable t
INNER JOIN CTE ON t.ID1 = CTE.ID1
AND t.ID2 = CTE.ID2
AND t.ID3 = CTE.ID3
LEFT JOIN CTE_OBSOLET ON t.ID1 = CTE_OBSOLET.ID1
AND t.ID2 = CTE_OBSOLET.ID2
AND t.ID3 = CTE_OBSOLET.ID3
WHERE CTE_OBSOLET.LEVEL IS NULL -- Assuming the LEVEL is not nullable. Any other not nullable column can be used here
-- Update records where columns other then ENABLED was changed.
-- Since this is an instead of update trigger, you have to include this to enable updates on other columns.
-- This assumes that you can't update the columns of the primary key (ID1, ID2 and ID3).
UPDATE t
SET LEVEL = i.LEVEL,
PARENT_LEVEL = i.PARENT_LEVEL,
OBSOLET = i.OBSOLET
FROM TestTable t
INNER JOIN inserted i ON t.ID1 = i.ID1
AND t.ID2 = i.ID2
AND t.ID3 = i.ID3
INNER JOIN deleted d ON i.ID1 = d.ID1
AND i.ID2 = d.ID2
AND i.ID3 = d.ID3
WHERE i.LEVEL <> d.LEVEL
OR d.PARENT_LEVEL <> i.PARENT_LEVEL
OR d.OBSOLET <> i.OBSOLET
GO
Testing:
SELECT *
FROM TestTable
Results:
ID1 ID2 ID3 LEVEL PARENT_LEVEL ENABLED OBSOLET
1 6 7 98 NULL 1 0
1 6 6 99 98 0 0
1 4 6 100 99 0 0
1 2 3 200 100 0 0
2 4 1 300 NULL 0 0
3 3 4 400 NULL 0 1
3 4 5 500 400 0 0
Do a couple of updates:
UPDATE TestTable
SET ENABLED = 1
WHERE LEVEL IN(200, 500)
UPDATE TestTable
SET ENABLED = 1,
OBSOLET = 1
WHERE LEVEL = 500
Test results:
SELECT *
FROM TestTable
Results:
ID1 ID2 ID3 LEVEL PARENT_LEVEL ENABLED OBSOLET
1 6 7 98 NULL 1 0
1 6 6 99 98 1 0
1 4 6 100 99 1 0
1 2 3 200 100 1 0
2 4 1 300 NULL 0 0
3 3 4 400 NULL 0 1
3 4 5 500 400 0 1

Related

how to update large number of records as a batch of n number of records

suppose I have 100000 records in A table and 1000 records in B table. both have primary/foreign key relationship. now i want to update a column value for first 100 records in table A with column value from table B first record. similary i want to update all the 100000 records in table A as a batch 100 records for 1000 times with values from table B.
no. of records updated per batch is 100 i.e. 100000/1000=100
Lets assume you have table_a with 20 rows with a unique id column and you want to update the value column:
CREATE TABLE table_a (id, value) AS
SELECT LEVEL, CAST(NULL AS NUMBER(8,0)) FROM DUAL CONNECT BY LEVEL <= 20;
And table_b with 5 rows containing the values you want to update from:
CREATE TABLE table_b (id, value) AS
SELECT LEVEL, LEVEL FROM DUAL CONNECT BY LEVEL <= 5;
Then, you can use a correlated UPDATE statement:
UPDATE table_a a
SET value = (SELECT value
FROM table_b b
WHERE CEIL(a.id*5/20) = b.id);
or a MERGE statement:
MERGE INTO table_a a
USING table_b b
ON (CEIL(a.id*5/20) = b.id)
WHEN MATCHED THEN
UPDATE
SET value = b.value;
Both statements result in:
ID
VALUE
1
1
2
1
3
1
4
1
5
2
6
2
7
2
8
2
9
3
10
3
11
3
12
3
13
4
14
4
15
4
16
4
17
5
18
5
19
5
20
5
db<>fiddle here

Update Parent Table column from child table column values

I have a table SaleOrder(SO) and SaleOrderDetail(SOD) with one to may relation ship. ID and SOID are primary key foreign key.
i need to update SO Table with Values of SOD Tables after some aggregation based on primary key.
please see below.
SO
-----------------------------------
ID SaleOrderQty
1 --
2 --
SOD
-------------------------------------
SOID Qty PerPack
1 3 10
1 7 6
2 4 5
2 5 8
multiply Qty with PerPack
1 3*10 = 30
1 7*6 = 42
2 4*5 = 20
2 5*8 = 40
and add up all multiplication results based on keys
1 30+42 = 72
2 20+40 = 60
and update Parent table
SO
-----------------------------------
ID SaleOrderQty
1 72
2 60
i tried this
Declare #Id varchar(50)
declare #Next int
set #Next =1
WHILE #Next <= 30
Begin
Select #Id = Id From SO Where SOSerial=#Next
Update SO
Set SaleOrderQty = (SELECT sum((SOD.Quantity* SOD.PerPack)) total
FROM SO INNER JOIN
SOD ON SO.Id = SOD.SOId
WHERE SOD.SOId=#Id
group by SOD.SOId)
set #Next=#Next+1
--print #Id
End
sums are ok but it set all SaleOrderQty values with the last sum.
One more thing. i have 30 records in parent table. when query completes it shows 30 messages.
Per my prior comment, you want to avoid loops.
update so
set SaleOrderQty = a.calc
from SO so
join (select sod.soid,sum((SOD.Quantity* SOD.PerPack)) as calc
from sod
group by sod.SOID) a on a.SOID=so.ID
where so.SaleOrderQty is null --optional

SQL Server - Recursive CTE from leafs to root (inverse)

Imagine following scenario:
I have a lot of levels, from up level (root parent) to down levels (childs or leafs).
(root parent) LEVEL 0
ID:98
/ \
/ \
/ \
o + LEVEL 1
ID:99 ID:100
/ \
/ \
o + LEVEL 2
ID:101 ID:102
/ \
/ \
o o LEVEL 3
ID:201 ID:202
Imagine now '+' symbols are rooms. Rooms at the same level cannot communicate between them. Each room has some gates. Through these gates you can communicate to other rooms (childs) at another level down.
Symbols 'o' are the leafs, I mean, rooms which haven't gates to access other rooms at a lower level.
For simplicity here, Each room has two gates, but could have more than two.
So now, finally image the following: If an explosion originates in any of the child/leaf rooms belonging to the parent room, then all gates of the parent room will be closed automatically to prevent explosion propagates up to the root parent.
So imagine the following table:
ROOM_ID | PARENT_ROOM | GATES_OPEN | EXPLOSION
98 NULL 1 0
99 98 1 0
100 98 1 0
102 100 1 0
101 100 1 0
200 102 - 0
201 102 - 0
All gates for all rooms are opened since initially there are no explosions.
Rooms 200 and 201 has no gates.
Imagine each room has a sensor to detect a possible explosion. If the sensor detects an explosion, the signal is propagated to the parent room, and parent room closes all its gates. This signal is also propagated up to the parent rooms, and all the parent rooms also close all its gates and so on until root parent is reached which also closes all its gates.
So now imagine an explosion is caused in room ID:102 so I need to obtain below table updated:
ROOM_ID | PARENT_ROOM | GATES_OPEN | EXPLOSION
98 NULL 0 0
99 98 1 0
100 98 0 0
102 100 1 1
101 100 1 0
200 102 - 0
201 102 - 0
So using a recursive CTE, how can obtain final table updated from the initial table? I need to propagate it from the root in which explosion was caused to root parent.
Here is one way to do it:
First, create and populate sample table (Please save us this step in your future questions):
DECLARE #T AS TABLE
(
ROOM_ID int,
PARENT_ROOM int,
GATES_OPEN bit,
EXPLOSION bit
)
INSERT INTO #T VALUES
(98, NULL, 1, 0),
(99, 98, 1, 0),
(100, 98, 1, 0),
(102, 100, 1, 0),
(101, 100, 1, 0),
(200, 102, NULL, 0),
(201, 102, NULL, 0)
Then, create the CTE:
DECLARE #RoomId int = 102;
;WITH CTE AS
(
SELECT ROOM_ID
,PARENT_ROOM
,GATES_OPEN
,CAST(1 AS BIT) AS EXPLOSION
FROM #T
WHERE ROOM_ID = #RoomId
UNION ALL
SELECT t.ROOM_ID
,t.PARENT_ROOM
,CAST(0 AS BIT) AS GATES_OPEN
,t.EXPLOSION
FROM #T t
INNER JOIN CTE ON t.ROOM_ID = CTE.PARENT_ROOM
)
Update the table:
UPDATE t
SET GATES_OPEN = CTE.GATES_OPEN,
EXPLOSION = CTE.EXPLOSION
FROM #T t
INNER JOIN CTE ON t.ROOM_ID = CTE.ROOM_Id
Finally, test if the update was OK:
SELECT *
FROM #T
Results:
ROOM_ID PARENT_ROOM GATES_OPEN EXPLOSION
98 NULL 0 0
99 98 1 0
100 98 0 0
102 100 1 1
101 100 1 0
200 102 NULL 0
201 102 NULL 0
Update
If you don't know in what room the explosion occurs (I'm guessing some process updates the database table and sets the explosion value to 1), Then you can use a trigger on the table. It's almost the same as the query I've written before, and it's results are the same:
CREATE TRIGGER tr_Rooms_Update ON Rooms
FOR UPDATE
AS
;WITH CTE AS
(
SELECT ROOM_ID
,PARENT_ROOM
,GATES_OPEN
,EXPLOSION
FROM inserted
WHERE EXPLOSION = 1
UNION ALL
SELECT t.ROOM_ID
,t.PARENT_ROOM
,CAST(0 AS BIT) AS GATES_OPEN
,t.EXPLOSION
FROM Rooms t
INNER JOIN CTE ON t.ROOM_ID = CTE.PARENT_ROOM
)
UPDATE t
SET GATES_OPEN = CTE.GATES_OPEN,
EXPLOSION = CTE.EXPLOSION
FROM Rooms t
INNER JOIN CTE ON t.ROOM_ID = CTE.ROOM_Id
GO

Transitive Group Query on 2 Columns in SQL Server

I need help with a transitive query in SQL Server.
I have a table with [ID] and [GRPID].
I would like to update a third column [NEWGRPID] based on the following logic:
For each [ID], get its GRPID;
Get all of the IDs associated with the GRPID from (1);
Set [NEWGRPID] equal to an integer (variable that is incremented by 1), for all of the rows from step (2)
The idea is several of these IDs are "transitively" linked across different [GRPID]s, and should all be having the same [GRPID].
The below table is the expected result, with [NEWGRPID] populated.
ID GRPID NEWGRPID
----- ----- ------
1 345 1
1 777 1
2 777 1
3 345 1
3 777 1
4 345 1
4 999 1
5 345 1
5 877 1
6 999 1
7 877 1
8 555 2
9 555 2
Try this code:
IF OBJECT_ID('tempdb..#tmp') IS NOT NULL
BEGIN
DROP TABLE #tmp;
END;
SELECT GRPID, count (*) AS GRPCNT
INTO #tmp
FROM yourtable
GROUP BY GRPID
UPDATE TGT
SET TGT.NEWGRPID = SRC.GRPCNT
FROM yourtable TGT
JOIN #tmp ON #tmp.GRPID = TGT.GRPID
If the values are likely to change over time you should think about a computed column or a trigger.

Update rows content adding values from rows in another table

I've already tried all things from here: Update rows from another table
with no success.
I'm using SQLite, and I'm executing the query with SQLite Database Browser.
I have 3 tables Off, Vio and Loc with data like this
Off(Aid,ox,oy):
0 0 0
1 100 100
2 200 200
Vio(Vid,Aid):
0 0
1 1
2 2
3 1
Loc(vid,x,y):
0 1 2
1 5 6
2 9 1
2 2 3
3 4 4
Aid is primary key in Off and foreign key in Vio.
Vid is primary key in Vio and foreign key in Loc.
I want to update the Loc rows adding the correct ox and oy to the x and y value to get something like this as a result:
"updated" Loc(vid,x,y)
0 1 2
1 105 106
2 209 201
2 202 203
3 104 104
I've tried this:
WITH CTE AS (
SELECT Loc.x, Loc.y, Off.ox AS offx, Off.oy AS offy
FROM Loc, Vio, Off
WHERE Vio.Aid=Off.Aid AND Vio.vid=Loc.vid
)
UPDATE CTE
SET x= x + offx, y= y + offy
And this:
UPDATE Loc, Off
SET x=x+Off.ox, y=y+Off.oy
FROM (
SELECT Off.ox, Off.oy
FROM Loc, Vio, Off
WHERE Vio.Aid = Off.Aid AND Vio.vid=Loc.vid
)
Within SQLite Database Browser with no success even when the SELECT query gives me all the data I need for the update.
As mentioned in the duplicate question, you have to use a correlated subquery. Since you're updating two columns, you need to use a subquery for each of them:
UPDATE Loc
SET x = (
SELECT L1.x + Off.ox
FROM Loc l1
JOIN Vio ON L1.vid = Vio.vid
JOIN Off ON Vio.Aid = Off.Aid
WHERE Loc.vid = l1.vid),
y = (
SELECT L1.y + Off.oy
FROM Loc l1
JOIN Vio ON L1.vid = Vio.vid
JOIN Off ON Vio.Aid = Off.Aid
WHERE Loc.vid = l1.vid)
DEMO

Resources