SQL Server custom record sort in table, allowing to delete records

假装没事ソ 提交于 2020-01-06 20:05:12

问题


SQL Server table with custom sort has columns: ID (PK, auto-increment), OrderNumber, Col1, Col2..

By default an insert trigger copies value from ID to OrderNumber as suggested here. Using some visual interface, user can sort records by incrementing or decrementing OrderNumber values.

However, how to deal with records being deleted in the meantime?

Example: Say you add records with PK ID: 1,2,3,4,5 - OrderNumber receives same values. Then you delete records with ID=4,ID=5. Next record will have ID=6 and OrderNumber will receive the same value. Having a span of 2 missing OrderNumbers would force user to decrement record with ID=6 like 3 times to change it's order (i.e. 3x button pressed).

Alternatively, one could insert select count(*) from table into OrderNumber, but it would allow to have several similar values in table, when some old rows are deleted.

If one doesn't delete records, but only "deactivate" them, they're still included in sort order, just invisible for user. At the moment, solution in Java is needed, but I think the issue is language-independent.

Is there a better approach at this?


回答1:


I would simply modify the script that switches the OrderNumber values so it does it correctly without relying on their being without gaps.

I don't know what arguments your script accepts and how it uses them, but the one that I've eventually come up with accept the ID of the item to move and the number of positions to move by (a negative value would mean "toward the lower OrderNumber values", and a positive one would imply the opposite direction).

The idea is as follows:

  1. Look up the specified item's OrderNumber.

  2. Rank all the items starting from OrderNumber in the direction determined by the second argument. The specified item thus receives the ranking of 1.

  3. Pick the items with rankings from 1 to the one that is the absolute value of the second argument plus one. (I.e. the last item is the one where the specified item is being moved to.)

  4. Join the resulting set with itself so that every row is joined with the next one and the last row is joined with the first one and thus use one set of rows to update the other.

This is the query that implements the above, with comments explaining some tricky parts:

Edited: fixed an issue with incorrect reordering

/* these are the arguments of the query */
DECLARE @ID int, @JumpBy int;
SET @ID = ...
SET @JumpBy = ...

DECLARE @OrderNumber int;
/* Step #1: Get OrderNumber of the specified item */
SELECT @OrderNumber = OrderNumber FROM atable WHERE ID = @ID;

WITH ranked AS (
  /* Step #2: rank rows including the specified item and those that are sorted
     either before or after it (depending on the value of @JumpBy */
  SELECT
    *,
    rnk = ROW_NUMBER() OVER (
      ORDER BY OrderNumber * SIGN(@JumpBy)
      /* this little "* SIGN(@JumpBy)" trick ensures that the
         top-ranked item will always be the one specified by @ID:
         * if we are selecting rows where OrderNumber >= @OrderNumber,
           the order will be by OrderNumber and @OrderNumber will be
           the smallest item (thus #1);
         * if we are selecting rows where OrderNumber <= @OrderNumber,
           the order becomes by -OrderNumber and @OrderNumber again
           becomes the top ranked item, because its negative counterpart,
           -@OrderNumber, will again be the smallest one
      */
    )
  FROM atable
  WHERE OrderNumber >= @OrderNumber AND @JumpBy > 0
     OR OrderNumber <= @OrderNumber AND @JumpBy < 0
),
affected AS (
  /* Step #3: select only rows that need be affected */
  SELECT *
  FROM ranked
  WHERE rnk BETWEEN 1 AND ABS(@JumpBy) + 1
)
/* Step #4: self-join and update */
UPDATE old
SET OrderNumber = new.OrderNumber
FROM affected old
  INNER JOIN affected new ON old.rnk = new.rnk % (ABS(@JumpBy) + 1) + 1
            /* if old.rnk = 1, the corresponding new.rnk is N,
               because 1 = N MOD N + 1  (N is ABS(@JumpBy)+1),
               for old.rnk = 2 the matching new.rnk is 1: 2 = 1 MOD N + 1,
               for 3, it's 2 etc.
               this condition could alternatively be written like this:
               new.rnk = (old.rnk + ABS(@JumpBy) - 1) % (ABS(@JumpBy) + 1) + 1
             */

Note: this assumes SQL Server 2005 or later version.

One known issue with this solution is that it will not "move" rows correctly if the specified ID cannot be moved exactly by the specified number of positions (for instance, if you want to move the topmost row up by any number of positions, or the second row by two or more positions etc.).




回答2:


Ok - if I'm not mistaken, you want to defragment your OrderNumber. What if you use ROW_NUMBER() for this ?

Example:

;WITH calc_cte AS (
  SELECT
    ID
    , OrderNumber
    , RowNo = ROW_NUMBER() OVER (ORDER BY ID)
  FROM
    dbo.Order    
)
UPDATE
  c
SET
  OrderNumber = c.RowNo
FROM
  calc_cte c
WHERE EXISTS (SELECT * FROM inserted i WHERE c.ID = i.ID)



回答3:


Didn't want to reply my own question, but I believe I have found a solution.

Insert query:

INSERT INTO table (OrderNumber, col1, col2) 
VALUES ((select count(*)+1 from table),val1,val2)

Delete trigger:

CREATE TRIGGER Cleanup_After_Delete ON table
AFTER DELETE AS
BEGIN
  WITH rowtable AS (SELECT [ID], OrderNumber, rownum = ROW_NUMBER() 
                    OVER (ORDER BY OrderNumber ASC) FROM table)
  UPDATE rt SET OrderNumber = rt.rownum FROM rowtable rt 
  WHERE OrderNumber >= (SELECT OrderNumber FROM deleted)
END

The trigger fires up after every delete and corrects all OrderNumbers above the deleted one (no gaps). This means that I can simply change the order of 2 records by switching their OrderNumbers.


This is a working solution for my problem, however this one is also very good one, perhaps more useful for others.



来源:https://stackoverflow.com/questions/11308096/sql-server-custom-record-sort-in-table-allowing-to-delete-records

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!