Home > Net >  renumbering in a column when adding a row sql
renumbering in a column when adding a row sql

Time:11-11

For a table like

create table Stations_in_route
(
ID_station_in_route int primary key,
ID_route int,
ID_station int,
Number_in_route int not null
)

There is the following trigger that changes the values ​​in the Number_in_route column after a new row is added to the route. The list of numbers in the route must remain consistent.

create trigger stations_in_route_after_insert on Stations_in_route
after insert
as

if exists
(select *from Stations_in_route
where Stations_in_route.ID_station_in_route not in (select ID_station_in_route from inserted)
and Stations_in_route.ID_route in (select ID_route from inserted) 
and Stations_in_route.Number_in_route in (select Number_in_route from inserted))

begin
update Stations_in_route
set Number_in_route = Number_in_route   1
where Stations_in_route.ID_station_in_route not in (select ID_station_in_route from inserted)
and Stations_in_route.ID_route in (select ID_route from inserted) 
and Stations_in_route.Number_in_route >= (select Number_in_route from inserted where Stations_in_route.ID_route = inserted.ID_route)
end

this trigger will throw an error if insertion into one ID_route is performed:

Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=, <, <= , >, >= or when the subquery is used as an expression.

For example,

Insert into Stations_in_route values(25, 4, 11, 3),(26, 4, 10, 5)

How to fix?

ID_station_in_route ID_route ID_station Number_in_route
1 4 1 1
2 4 2 2
3 4 3 3
4 4 4 4
5 4 5 5
6 4 6 6
7 4 7 7
8 4 8 8

i expect the list after adding will become like this

ID_station_in_route ID_route ID_station Number_in_route
1 4 1 1
2 4 2 2
25 4 11 3
3 4 3 4
26 4 10 5
4 4 4 6
5 4 5 7
6 4 6 8
7 4 7 9
8 4 8 10

this is not the whole table, as there are other routes too

CodePudding user response:

Based on the requirements, when you add new stops to the route, you need to insert them into their desired sequence correctly, and push all existing stops from that point forward so that a contiguous sequence is maintained. When you insert one row this isn't very hard (just number_in_route 1 where number_in_route > new_number_in_route), but when you insert more rows, you need to basically push the entire set of subsequent stops by 1 for each new row. To illustrate, let's say you start with this:

enter image description here

If we insert two new rows, such as:

INSERT dbo.Stations_in_route
(
  ID_station_in_route,
  ID_route,
  ID_station,
  Number_in_route
)
VALUES (25, 4, 11, 3),(26, 4, 10, 5);
-- add a stop at 3 ^              ^
----------------- add a stop at 5 ^

We can illustrate this by slowing it down into separate steps. First, we need to add this row at position #3:

enter image description here

And we do this by pushing all the rows > 3 down by 1:

enter image description here

But now when we add this row at position #5:

enter image description here

That's the new position #5, after the previous shift, so it looks like this:

enter image description here

We can do this with the following trigger, which is possibly a little more complicated than it has to be, but is better IMHO than tedious loops which might otherwise be required.

CREATE TRIGGER dbo.tr_ins_Stations_in_route ON dbo.Stations_in_route
FOR INSERT AS
BEGIN
    ;WITH x AS 
    (
      SELECT priority = 1, *, offset = ROW_NUMBER() OVER 
        (PARTITION BY ID_route ORDER BY Number_in_route)
      FROM inserted AS i
      UNION ALL 
      SELECT priority = 2, s.*, offset = NULL FROM dbo.Stations_in_route AS s
        WHERE s.ID_route IN (SELECT ID_route FROM inserted)
    ), 
    y AS 
    (
      SELECT *, rough_rank = Number_in_route 
            COALESCE(MAX(offset) OVER (PARTITION BY ID_Route 
            ORDER BY Number_in_route ROWS UNBOUNDED PRECEDING),0) 
          - COALESCE(offset, 0),
        tie_break = ROW_NUMBER() OVER 
          (PARTITION BY ID_route, ID_station_in_route ORDER BY priority)
      FROM x
    ),
    z AS 
    (
      SELECT *, new_Number_in_route = ROW_NUMBER() OVER 
        (PARTITION BY ID_Route ORDER BY rough_rank, priority) 
      FROM y WHERE tie_break = 1
    )
    UPDATE s SET s.Number_in_route = z.new_Number_in_route
      FROM dbo.Stations_in_route AS s
      INNER JOIN z ON s.ID_route = z.ID_route
        AND s.ID_station_in_route = z.ID_station_in_route;
END

I've mentioned a couple of times that you might want to handle ties for new rows, e.g. if the insert happened to be:

Insert into Stations_in_route values(25, 4, 11, 3),(26, 4, 10, 3)

For that you can add additional tie-breaking criteria to this clause:

  new_Number_in_route = ROW_NUMBER() OVER 
    (PARTITION BY ID_Route ORDER BY rough_rank, priority) 

e.g.:

  new_Number_in_route = ROW_NUMBER() OVER 
    (PARTITION BY ID_Route ORDER BY rough_rank, priority, 
     ID_station_in_route DESC) 

CodePudding user response:

I'm unable to repro the exception with the test code/data in the question, however I'm gonna guess that the issue is with this bit of the code in the trigger:

AND Stations_in_route.Number_in_route >= 
(
SELECT Number_in_route
FROM inserted
WHERE Stations_in_route.ID_route = inserted.ID_route
)

The engine there will implicitly expect that subquery on the right-side of the >= operator to return a scalar result (single row, single column result), however the inserted table is in fact, a table...which may contain multiple records (as would be the case in a multi-row insert/update/etc. type statement as outlined in your example). Given that the filter (i.e. WHERE clause) in that subquery isn't guaranteed to be unique (ID_route doesn't appear to be unique, and in your example you have an insert statement that actually inserts multiple rows with the same ID_route value), then it's certainly possible that query will return a non-scalar result.

To fix that, you'd need to adjust that subquery to guarantee a result of a scalar value (single row and single column). You've guaranteed the single column already with the selector...now you need to add logic to guarantee a single result/record as well. That could include one or more of the following (or possibly other things also):

  • Wrap the selected Number_in_route column in an aggregate function (i.e. a MAX() perhaps?)
  • Add a TOP 1 with an ORDER BY to get the record you want to compare with
  • Add additional filters to the WHERE clause to ensure a single result is returned
  • Related