0

I want the user of my application to change the order of records using a filed called Order. I generated some scripts in order to change the order of the records but I think there should be a more optimized way to do it.

My test table is called MyTable. The ID of the table is called ID and the order field is called Order.

My SQL commands are the following:

Move Up

Declare @ID int = 3;
Declare @Order int = (SELECT [Order] FROM MyTable WHERE ID = @ID);
Declare @PreviousID int = (SELECT MAX(ID) FROM MyTable WHERE [Order] < @Order);
Update MyTable SET [Order] = @Order - 1 WHERE ID = @ID;
Update MyTable SET [Order] = @Order WHERE ID = @PreviousID

Move Down

Declare @ID int = 3;
Declare @Order int = (SELECT [Order] FROM MyTable WHERE ID = @ID);
Declare @NextID int = (SELECT MIN(ID) FROM MyTable WHERE [Order] > @Order);
Update MyTable SET [Order] = @Order + 1 WHERE ID = @ID;
Update MyTable SET [Order] = @Order WHERE ID = @NextID

Move to top

Declare @ID int = 3;
Declare @MinimumOrder int = (SELECT Min([Order]) FROM MyTable);
Update MyTable SET [Order] = @MinimumOrder - 1 WHERE ID = @ID;

Move to Bottom

Declare @ID int = 3;
Declare @MaximumOrder int = (SELECT Max([Order]) FROM MyTable);
Update MyTable SET [Order] = @MaximumOrder + 1 WHERE ID = @ID;

These SQL commands work without problem. It also can have negative numbers for the Order field.

I also would like to generate one more SQL script which will update the Order filed so that it will update the Order filed so that the Order will start from 1 and increase it's value by 1. This is useful because sometimes we may delete records or my scripts may produce negative order numbers. If for example you try to move up the record with Order = 1, it will have as a result the Order to take the value 0 and if you do it again it will take value -1, etc.

pitaridis
  • 2,801
  • 3
  • 22
  • 41
  • 1
    It's a tricky problem. I've seen it done using something like a binary search method, but that requires the application store the order values as (large) multiples to begin with. Like this: https://stackoverflow.com/questions/30059116/microsoft-dynamics-gp-2013-how-to-find-line-item-sequence and here is another post by @xqbert that describes another approach: https://stackoverflow.com/a/8608085/7948962 Bottom line: I don't think there's a "best practice" approach here. – Jacob H Jan 18 '18 at 18:12
  • Those commands do not work. – paparazzo Jan 18 '18 at 18:50
  • They work. If you set @ID to be equal to the reocrd ID that you want to move it works. Did you test them or you supose they will not work? – pitaridis Jan 18 '18 at 18:56
  • Well each of those can easily be accomplished with a single statement, that's for sure. – shawnt00 Jan 18 '18 at 21:39

4 Answers4

1

When you have the ordering column normalized to values from 1 to number of items you can use code like the examples below to maintain the order. The general technique is to select all of the affected rows in the where clause of an update statement and use a case expression to update the ordering column appropriately.

-- Sample data.
declare @Samples as Table ( SampleId Int Identity, DisplayOrder Int, Name VarChar(20) );
insert into @Samples ( DisplayOrder, Name ) values
  ( 1, 'Chutney' ), ( 2, 'Marshmallows' ), ( 3, 'Carrots' ), ( 4, 'Cheddar' );

select * from @Samples order by DisplayOrder;

-- Swap display orders so that the target row is moved to the target display position.
declare @TargetId as Int = 2;
declare @TargetDisplayOrder as Int = 1;

update @Samples
  set DisplayOrder = case
    when SampleId = @TargetId then @TargetDisplayOrder
    else ( select DisplayOrder from @Samples where SampleId = @TargetId ) end
  where SampleId in ( @TargetId,
    ( select SampleId from @Samples where DisplayOrder = @TargetDisplayOrder ) );

select * from @Samples order by DisplayOrder;

-- Move the target row up one position in the display order.
set @TargetId = 3;

update @Samples
  set DisplayOrder = case
    when SampleId = @TargetId then DisplayOrder - 1
    else DisplayOrder + 1 end
  where SampleId in ( @TargetId,
    ( select SampleId from @Samples where DisplayOrder =
      ( select DisplayOrder from @Samples where SampleId = @TargetId ) - 1 ) );

select * from @Samples order by DisplayOrder;

-- Move the target row down one position in the display order.
set @TargetId = 2;

update @Samples
  set DisplayOrder = case
    when SampleId = @TargetId then DisplayOrder + 1
    else DisplayOrder - 1 end
  where SampleId in ( @TargetId,
    ( select SampleId from @Samples where DisplayOrder =
      ( select DisplayOrder from @Samples where SampleId = @TargetId ) + 1 ) );

select * from @Samples order by DisplayOrder;

-- Move the target row up to the top in the display order.
set @TargetId = 1;

update @Samples
  set DisplayOrder = case
    when SampleId = @TargetId then 1
    else DisplayOrder + 1 end
  where DisplayOrder <= ( select DisplayOrder from @Samples where SampleId = @TargetId );

select * from @Samples order by DisplayOrder;

-- Move the target row down to the bottom in the display order.
set @TargetId = 3;

update @Samples
  set DisplayOrder = case
    when SampleId = @TargetId then ( select Max( DisplayOrder ) from @Samples )
    else DisplayOrder - 1 end
  where DisplayOrder >= ( select DisplayOrder from @Samples where SampleId = @TargetId );

select * from @Samples order by DisplayOrder;

Note that if you use multiple statements to perform work, e.g. insert a new row and then update to move it to the desired order, you need to wrap the statements in a transaction (with a suitable isolation level) to prevent multiple users from corrupting the data.

HABO
  • 15,314
  • 5
  • 39
  • 57
0

I do not know if this is the best way to do it but I managed to do it using a cursor.

DECLARE @i int = 1;
DECLARE @ID int

DECLARE db_cursor CURSOR FOR  
SELECT ID FROM MyTable ORDER BY [Order]

OPEN db_cursor   
FETCH NEXT FROM db_cursor INTO @ID

WHILE @@FETCH_STATUS = 0   
BEGIN   
    update MyTable set [Order] = @i WHERE ID = @ID;
    SET @i = @i + 1;
       FETCH NEXT FROM db_cursor INTO @ID   
END   

CLOSE db_cursor   
DEALLOCATE db_cursor

Please inform me in case there is a better way to do it.

pitaridis
  • 2,801
  • 3
  • 22
  • 41
  • 1
    A good rule of thumb when working with any flavor of SQL is to try avoiding the use of cursors and instead try and use set-based operations - it's what the database is designed for. – mikurski Jan 18 '18 at 19:47
0

T-SQL's ROW_NUMBER() operation might be useful to you in terms of consolidating order values.

This is an example of how you can use this functionality to consolidate your order values without affecting the ranking of items within that order:

/* declare placeholder and populate with test values */
declare @MyTable table (ID bigint identity(1,1), [Order] bigint)

insert into @MyTable ([Order]) 
VALUES
(1),
(3),
(2),
(5),
(-4),
(13),
(0)

/* Look at values we've just inserted */
select * from @MyTable order by [Order]

/* Show how ROW_NUMBER() can apply a consolidated ranking based on our existing order */
select *, ROW_NUMBER() over (order by [Order] asc) as sort from @MyTable

/* Apply that consolidated ranking to update the order values */
update @MyTable
set [Order] = consolidated.sort
from 
(
select ID as refID, ROW_NUMBER() over (order by [Order] asc) as sort from @MyTable
) consolidated
where consolidated.refID = ID

/* Final display of updated table */
select * from @MyTable order by [Order]

Ideally, though, it's worth taking some time to manage and keep clean the data from the start.

Part of this is database structure and normalization, looking at things like:

  • Can a record exist and not be ordered?
  • If there is more than one user, do they all use the same order, or would each user have a separate order ranking?
  • Is it possible a user might have more than one ordering that they want to use and switch between?

If any of those are true, you might want to break the ordering out into a separate table for data integrity purposes (and because of transaction locks and things, it might be worthwhile to do even if you don't)

Database design aside, it's also worthwhile to look at how you handle the data operations for modifying record order.

If we're using a table with N records, and these records have a densely-packed order (as per what the query above demonstrates, where the order values are 1,2,3,4,5,etc.), then any time we make a change to that order, we have to update a lot of the existing order values in the table.

For example:

  • The user modifies a record so its order changes from 5 to 2. We now have to shift every record from order 2 onwards to the end of the order.

One way to compensate for this is to calculate ranking using offsets - rather than ordering by 1,2,3... instead use larger values like 10,20,30.... This lets you handle the user's ordering changes without a lot of immediate database load (move something up to order 2? Slot it in at position 15, between 10 and 20), and you can then optimize the ordering later.

mikurski
  • 1,353
  • 7
  • 20
  • Glad to help! I've appended some additional data design stuff to the bottom that might give you some support while you're developing. – mikurski Jan 18 '18 at 20:11
  • Really? Why don't you test moving ID = 3 up using the script for the OP. Your consolidation did not break it. The OP script was and still is broken. – paparazzo Jan 19 '18 at 00:09
  • The OP was asking for guidance regarding a script that "will update the Order filed so that the Order will start from 1 and increase it's value by 1." The example work in my answer was designed around answering that question. Are there issues in how the rest of the data structure and scripting are designed from a RDBMS perspective? Yes, and I've also included guidance towards better practices in my answer so the OP can refer to them later. – mikurski Jan 19 '18 at 00:24
-1

Below will not get the previous item in the [Order] (unless by luck).

Declare @PreviousID int = (SELECT MAX(ID) FROM MyTable WHERE [Order] < @Order);

Same problem with MIN(ID).

paparazzo
  • 44,497
  • 23
  • 105
  • 176
  • For this query there are two possibilities. The first is that there is a record with a lower order number which will be fine and both the selected and the previous record will be updated so that we will have the order that we want. The second possiblitity is that there is not a record with order lower than the order of the current record. In that case the PreviousID will be null and we will try to update the records with ID equal to null, which will have as a result that we will not change anything and the current record will be decreased. Trust me I tested it and it works. – pitaridis Jan 18 '18 at 18:52
  • Trust you? The MAX(ID) is not necessarily the prior item in ORDER. – paparazzo Jan 18 '18 at 19:01
  • The only problem with this script is when there are two records with the same Order number which can happen if someone go and change manually the Order value of a record. – pitaridis Jan 18 '18 at 19:06
  • I did not say that I use duplicate values. I said that this is the only problem of the script. You do not have to educate me my friend. I have this script in an application right now and it works. I do not know why you are so sure that it does not work if you have not tested it. If you see a problem in this script just tell me what is wrong with it and explain me what make you belive that this script will not give the previous record. You just say that it will not work without explaining what is the problem. – pitaridis Jan 18 '18 at 19:18