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

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

Related

Grouping between two datetimes

I have a bunch of production orders and I'm trying to group by within a datetime range, then count the quantity within that range. For example, I want to group from 2230 to 2230 each day.
PT.ActualFinish is datetime (eg. if PT.ActualFinish is 2020-05-25 23:52:30 then it would be counted on the 26th May instead of the 25th)
Currently it's grouped by date (midnight to midnight) as opposed to the desired 2230 to 2230.
GROUP BY CAST(PT.ActualFinish AS DATE)
I've been trying to reconcile some DATEADD with the GROUP without success. Is it possible?
Just add 1.5 hours (90 minutes) and then extract the date:
group by convert(date, dateadd(minute, 90, pt.acctualfinish))
For this kind of thing you can use a function I created called NGroupRangeAB (code below) which can be used to create groups over values with an upper and lower bound.
Note that this:
SELECT f.*
FROM core.NGroupRangeAB(0,1440,12) AS f
ORDER BY f.RN;
Returns:
RN GroupNumber Low High
--- ------------ ------ -------
0 1 0 120
1 2 121 240
2 3 241 360
3 4 361 480
4 5 481 600
5 6 601 720
6 7 721 840
7 8 841 960
8 9 961 1080
9 10 1081 1200
10 11 1201 1320
11 12 1321 1440
This:
SELECT
f.GroupNumber,
L = DATEADD(MINUTE,f.[Low]-SIGN(f.[Low]),CAST('00:00:00.0000000' AS TIME)),
H = DATEADD(MINUTE,f.[High]-1,CAST('00:00:00.0000000' AS TIME))
FROM core.NGroupRangeAB(0,1440,12) AS f
ORDER BY f.RN;
Returns:
GroupNumber L H
------------- ---------------- ----------------
1 00:00:00.0000000 01:59:00.0000000
2 02:00:00.0000000 03:59:00.0000000
3 04:00:00.0000000 05:59:00.0000000
4 06:00:00.0000000 07:59:00.0000000
5 08:00:00.0000000 09:59:00.0000000
6 10:00:00.0000000 11:59:00.0000000
7 12:00:00.0000000 13:59:00.0000000
8 14:00:00.0000000 15:59:00.0000000
9 16:00:00.0000000 17:59:00.0000000
10 18:00:00.0000000 19:59:00.0000000
11 20:00:00.0000000 21:59:00.0000000
12 22:00:00.0000000 23:59:00.0000000
Now for a real-life example that may help you:
-- Sample Date
DECLARE #table TABLE (tm TIME);
INSERT #table VALUES ('00:15'),('11:20'),('21:44'),('09:50'),('02:15'),('02:25'),
('02:31'),('23:31'),('23:54');
-- Solution:
SELECT
GroupNbr = f.GroupNumber,
TimeLow = f2.L,
TimeHigh = f2.H,
Total = COUNT(t.tm)
FROM core.NGroupRangeAB(0,1440,12) AS f
CROSS APPLY (VALUES(
DATEADD(MINUTE,f.[Low]-SIGN(f.[Low]),CAST('00:00:00.0000000' AS TIME)),
DATEADD(MINUTE,f.[High]-1,CAST('00:00:00.0000000' AS TIME)))) AS f2(L,H)
LEFT JOIN #table AS t
ON t.tm BETWEEN f2.L AND f2.H
GROUP BY f.GroupNumber, f2.L, f2.H;
Returns:
GroupNbr TimeLow TimeHigh Total
-------------------- ---------------- ---------------- -----------
1 00:00:00.0000000 01:59:00.0000000 1
2 02:00:00.0000000 03:59:00.0000000 3
3 04:00:00.0000000 05:59:00.0000000 0
4 06:00:00.0000000 07:59:00.0000000 0
5 08:00:00.0000000 09:59:00.0000000 1
6 10:00:00.0000000 11:59:00.0000000 1
7 12:00:00.0000000 13:59:00.0000000 0
8 14:00:00.0000000 15:59:00.0000000 0
9 16:00:00.0000000 17:59:00.0000000 0
10 18:00:00.0000000 19:59:00.0000000 0
11 20:00:00.0000000 21:59:00.0000000 1
12 22:00:00.0000000 23:59:00.0000000 2
Note that an inner join will eliminate the 0-count rows.
CREATE FUNCTION core.NGroupRangeAB
(
#min BIGINT, -- Group Number Lower boundary
#max BIGINT, -- Group Number Upper boundary
#groups BIGINT -- Number of groups required
)
/*****************************************************************************************
[Purpose]:
Creates an auxilliary table that allows for grouping based on a given set of rows (#rows)
and requested number of "row groups" (#groups). core.NGroupRangeAB can be thought of as a
set-based, T-SQL version of Oracle's WIDTH_BUCKET, which:
"...lets you construct equiwidth histograms, in which the histogram range is divided into
intervals that have identical size. (Compare with NTILE, which creates equiheight
histograms.)" https://docs.oracle.com/cd/B19306_01/server.102/b14200/functions214.htm
See usage examples for more details.
[Author]:
Alan Burstein
[Compatibility]:
SQL Server 2008+
[Syntax]:
--===== Autonomous
SELECT ng.*
FROM dbo.NGroupRangeAB(#rows,#groups) AS ng;
[Parameters]:
#rows = BIGINT; the number of rows to be "tiled" (have group number assigned to it)
#groups = BIGINT; requested number of tile groups (same as the parameter passed to NTILE)
[Returns]:
Inline Table Valued Function returns:
GroupNumber = BIGINT; a row number beginning with 1 and ending with #rows
Members = BIGINT; Number of possible distinct members in the group
Low = BIGINT; the lower-bound range
High = BIGINT; the Upper-bound range
[Dependencies]:
core.rangeAB (iTVF)
[Developer Notes]:
1. An inline derived tally table using a CTE or subquery WILL NOT WORK. NTally requires
a correctly indexed tally table named dbo.tally; if you have or choose to use a
permanent tally table with a different name or in a different schema make sure to
change the DDL for this function accordingly. The recomended number of rows is
1,000,000; below is the recomended DDL for dbo.tally. Note the "Beginning" and "End"
of tally code.To learn more about tally tables see:
http://www.sqlservercentral.com/articles/T-SQL/62867/
2. For best results a P.O.C. index should exists on the table that you are "tiling". For
more information about P.O.C. indexes see:
http://sqlmag.com/sql-server-2012/sql-server-2012-how-write-t-sql-window-functions-part-3
3. NGroupRangeAB is deterministic; for more about deterministic and nondeterministic functions
see https://msdn.microsoft.com/en-us/library/ms178091.aspx
[Examples]:
-----------------------------------------------------------------------------------------
--===== 1. Basic illustration of the relationship between core.NGroupRangeAB and NTILE.
-- Consider this query which assigns 3 "tile groups" to 10 rows:
DECLARE #rows BIGINT = 7, #tiles BIGINT = 3;
SELECT t.N, t.TileGroup
FROM ( SELECT r.RN, NTILE(#tiles) OVER (ORDER BY r.RN)
FROM core.rangeAB(1,#rows,1,1) AS r) AS t(N,TileGroup);
Results:
N TileGroup
--- ----------
1 1
2 1
3 1
4 2
5 2
6 3
7 3
To pivot these "equiheight histograms" into "equiwidth histograms" we could do this:
DECLARE #rows BIGINT = 7, #tiles BIGINT = 3;
SELECT TileGroup = t.TileGroup,
[Low] = MIN(t.N),
[High] = MAX(t.N),
Members = COUNT(*)
FROM ( SELECT r.RN, NTILE(#tiles) OVER (ORDER BY r.RN)
FROM core.rangeAB(1,#rows,1,1) AS r) AS t(N,TileGroup);
GROUP BY t.TileGroup;
Results:
TileGroup Low High Members
---------- ---- ----- -----------
1 1 3 3
2 4 5 2
3 6 7 2
This will return the same thing at a tiny fraction of the cost:
SELECT TileGroup = ng.GroupNumber,
[Low] = ng.[Low],
[High] = ng.[High],
Members = ng.Members
FROM core.NGroupRangeAB(1,#rows,#tiles) AS ng;
--===== 2.1. Divide 25 Rows into 3 groups
DECLARE #min BIGINT = 1, #max BIGINT = 25, #groups BIGINT = 4;
SELECT ng.GroupNumber, ng.Members, ng.low, ng.high
FROM core.NGroupRangeAB(#min,#max,#groups) AS ng;
--===== 2.2. Assign group membership to another table
DECLARE #min BIGINT = 1, #max BIGINT = 25, #groups BIGINT = 4;
SELECT
ng.GroupNumber, ng.low, ng.high, s.WidgetId, s.Price
FROM (VALUES('a',$12),('b',$22),('c',$9),('d',$2)) AS s(WidgetId,Price)
JOIN core.NGroupRangeAB(#min,#max,#groups) AS ng
ON s.Price BETWEEN ng.[Low] AND ng.[High]
ORDER BY ng.RN;
Results:
GroupNumber low high WidgetId Price
------------ ---- ----- --------- ---------------------
1 1 7 d 2.00
2 8 13 a 12.00
2 8 13 c 9.00
4 20 25 b 22.00
-----------------------------------------------------------------------------------------
[Revision History]:
Rev 00 - 20190128 - Initial Creation; Final Tuning - Alan Burstein
****************************************************************************************/
RETURNS TABLE WITH SCHEMABINDING AS RETURN
SELECT
RN = r.RN, -- Sort Key
GroupNumber = r.N2, -- Bucket (group) number
Members = g.S-ur.N+1, -- Count of members in this group
[Low] = r.RN*g.S+rc.N+ur.N, -- Lower boundary for the group (inclusive)
[High] = r.N2*g.S+rc.N -- Upper boundary for the group (inclusive)
FROM core.rangeAB(0,#groups-1,1,0) AS r -- Range Function
CROSS APPLY (VALUES((#max-#min)/#groups,(#max-#min)%#groups)) AS g(S,U) -- Size, Underflow
CROSS APPLY (VALUES(SIGN(SIGN(r.RN-g.U)-1)+1)) AS ur(N) -- get Underflow
CROSS APPLY (VALUES(#min+r.RN-(ur.N*(r.RN-g.U)))) AS rc(N); -- Running Count
GO

SQL Server find chains between two columns

I have a table like this:
from | to
-----+-----
23 | 24
24 | 25
25 | 27
27 | 30
45 | 46
46 | 47
50 | 52
53 | 60
I need a SQL Server query that detect chain's and return min (from) and max (to) in each chain (also chain's with one record):
from | to
-----+-----
23 | 30
45 | 47
50 | 52
53 | 60
Here's an approach using a recursive CTE.
CREATE TABLE #chainLinks(linkFrom INTEGER, linkTo INTEGER);
INSERT INTO #chainLinks VALUES (23,24);
INSERT INTO #chainLinks VALUES (24,25);
INSERT INTO #chainLinks VALUES (25,27);
INSERT INTO #chainLinks VALUES (27,30);
INSERT INTO #chainLinks VALUES (45,46);
INSERT INTO #chainLinks VALUES (46,47);
INSERT INTO #chainLinks VALUES (50,52);
INSERT INTO #chainLinks VALUES (53,60);
WITH reccte AS
(
/*Recursive Seed*/
SELECT linkFrom AS chainStart,
linkFrom,
linkTo,
0 as links
FROM #chainLinks as chainLinks
WHERE linkFrom NOT IN (SELECT DISTINCT linkTo FROM #chainLinks)
UNION ALL
/*Recursive Term*/
SELECT
reccte.chainStart,
chainLinks.linkFrom,
chainLinks.linkTo,
links + 1
FROM reccte
INNER JOIN #chainLinks as chainLinks ON reccte.linkTo = chainLinks.linkFrom
)
SELECT chainStart, linkTo AS chainEnd
FROM
(
SELECT chainStart, linkFrom, linkTo, links, ROW_NUMBER() OVER (PARTITION BY chainStart ORDER BY links DESC) AS rn
FROM reccte
)subrn
WHERE rn = 1;
A recursive CTE takes two parts
A recursive seed - This is the part above the UNION where we determine which records from our table begin the recursion. Here we want any linkFrom that isn't also a linkTo
A recusrive term - This is the part below the UNION where we join the cte called reccte back to the original table. This part of the CTE iterates over and over again until that join fails.
In here we are also tracking that links which is just a counter of the number of iterations we have gone through to get to that outputted record. We keep the highest number of links for each starting point chainStart.
Here is the working example: https://rextester.com/JWUW57837
If there are branches within the chains it become a little bit more tricky.
In the sample data below, there's a split on From=12.
So the result shows 2 chains starting from 14.
create table yourtable (
[From] int not null,
[To] int not null,
PRIMARY KEY ([From],[To])
)
GO
✓
insert into yourtable
([From],[To]) values
(2,3),(3,5),(5,4)
,(14,12),(12,15),(15,11),(11,10)
,(12,9)
,(21,23)
GO
9 rows affected
;WITH RCTE_CHAINS AS
(
-- seeding with the start of chains
SELECT [From] AS MinFrom, [From], [To], 0 AS Lvl
, CAST(IIF(EXISTS(
SELECT 1 FROM YourTable n
WHERE n.[From] = t.[To]
),1,0) AS BIT) AS hasNext
FROM YourTable t
WHERE NOT EXISTS
(
SELECT 1
FROM YourTable t2
WHERE t2.[To] = t.[From]
)
UNION ALL
-- looping through the childs
SELECT c.MinFrom, t.[From], t.[To], c.Lvl+1
, CAST(IIF(EXISTS(
SELECT 1 FROM YourTable n
WHERE n.[From] = t.[To]
),1,0) AS BIT) AS hasNext
FROM RCTE_CHAINS c
JOIN YourTable t ON t.[From] = c.[To]
)
SELECT MinFrom AS [From], [To]
FROM RCTE_CHAINS
WHERE hasNext = 0
GO
From | To
---: | -:
21 | 23
14 | 9
14 | 10
2 | 4
db<>fiddle here

Which select statement is suitable for the below SQLServer View?

I am trying to get the total number of trips a meter has undergone based on a set of records.
-- MeterRecord Table
Id IVoltage ICurrent
--------------------
1 340 0 <<<-- (Trip is zero at this point)
2 288 1
3 312 2
4 236 1
5 343 0 <<<-- (Trip is one at this point)
6 342 0
7 264 1
8 269 0 <<<-- (Trip is two at this point)
Trip is incremented by one only when 'ICurrent' value returns back to zero from a previous non-zero state.
What i have tried using Count function:
Select SUM(IVoltage) as Sum_Voltage, COUNT(case when ICurrent = 0 then 1 else 0 end) as Trips
This returns
Sum_Voltage Trips
---------------------
45766 8
What i am trying to achieve based on the table above
--MeterRecord View
Sum_Voltage Trips
---------------------
45766 2
Use LAG to determine if you have a trip:
DROP TABLE IF EXISTS #meterRecord
CREATE TABLE #meterRecord
(
Id INT,
IVoltage INT,
ICurrent INT
);
INSERT INTO #meterRecord
VALUES
(1,340,0),
(2,288,1),
(3,312,2),
(4,236,1),
(5,343,0),
(6,342,0),
(7,264,1),
(8,269,0);
WITH cte AS
(
SELECT IVoltage,
CASE WHEN ICurrent = 0 AND LAG(ICurrent,1) OVER(ORDER BY Id) != 0 THEN 1 ELSE 0 END isTrip
FROM #meterRecord
)
SELECT SUM(cte.IVoltage) AS Sum_Voltage,
SUM(isTrip) AS Trips
FROM cte

SQL Server: Recursive table update

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

SQL Re-sequence Records

I have a list of items that I need to re-sequence with no gaps. It starts out as:
ID Sequence
123 1
125 2
155 3
158 4
165 6
170 9
I need to end up with (Note that Sequence 6 changes to 5 and Sequence 9 changes to 6)
ID Sequence
123 1
125 2
155 3
158 4
165 5
170 6
I have tried using this update statement
UPDATE tblA
SET tblA.Sequence = temp.Sequence
FROM ( SELECT id ,
ROW_NUMBER() OVER ( ORDER BY Sequence, ID ) AS Sequence
FROM dbo.tblA
) AS temp
but I just end up with ...
ID Sequence
123 1
125 1
155 1
158 6
165 6
170 6
Pulling the select statement out of the update produces the correct results.
Changing it to something like
UPDATE tblA
SET tblA.Sequence = temp.NewSequence
FROM ( SELECT id ,
ROW_NUMBER() OVER ( PARTITION BY id ORDER BY Sequence, id )
AS NewSequence
FROM dbo.tblA
) AS temp
Produces the results
ID Sequence
123 1
125 1
155 1
158 1
165 1
170 1
What am I doing wrong?
You need to associate your re-numbered set with the IDs from your original set, otherwise you're updating the original ID with a sequence number for some other ID from your temp set:
UPDATE a
SET a.Sequence = temp.Sequence
FROM
tblA a JOIN
(
SELECT id, ROW_NUMBER() OVER (ORDER BY Sequence, ID) AS Sequence
FROM dbo.tblA
) AS temp ON temp.ID = a.ID

Resources