4

I need to make ability to reorder records, storing in DB (I use MS SQL, but seems it's no matter). I see possible 2 solutions:

  1. Add Order column. Then if we want to reorder 2 records, we need to change values of Order column for these 2 records. Value of the first record set to the second and vice versa. Problem - we should add an unique constraint to order column and we can't do it easy, because on the first step we catch error like:

{"Violation of UNIQUE KEY constraint 'IX_EscortItems'. Cannot insert duplicate key in object 'dbo.EscortItems'. The duplicate key value is (2, 20).\r\nThe statement has been terminated."}

  1. Second approach is singly linked list. Add column Parent, store null for the first element or Id of the previous element. But we have the same problem with unique constraint.

What is the right approach to solve this problem?

dcorking
  • 586
user285336
  • 205
  • 3
  • 6
  • 1
    What is the purpose of the ordering, and what kind of reordering do you require? Please describe your actual use case. From your question, it is not clear if the reordering takes place for all records at once, or by swaps, or by inserts, or why it is so important for your program to have a unique constraint on the order column (and "I have learned this from my textbook" is not a sufficient reason!). – Doc Brown Feb 25 '17 at 00:09
  • Related: http://softwareengineering.stackexchange.com/q/195308/20756 – Blrfl Feb 25 '17 at 13:06
  • -1 and voting to close as unclear, you had time enough to answer my questions. – Doc Brown Feb 25 '17 at 22:08

8 Answers8

7

Four options for you. I like option 4 but the first one is easiest.

Option 1. Just start one higher

The values in the Order column don't matter, just their relative values. Whether you number ten rows from 1 to 10, 11 to 20, or 57 to 66 are all the same.

Thus, a very simple solution is just to start one higher than your highest Order column: if your rows have Order values from 1 to 10, and you need to reverse them, assign them values from 20 to 11.

They will end up sorting correctly, and you will have no uniqueness violations because they are in a separate range of values.

The down side of this is that is a little tricky if you don't want to re-do the entire list and just want to swap two values.

Option 2. Include a sort version number in the uniqueness constraint

Instead of adding one column Order, add two, SortOrder and SortVersion. Apply the uniqueness constraint to the combination of these two columns.

If you want to re-order the items in the list, increase the SortVersion column by one when you save the new value for SortVersion.

Option 3. Delete the rows and re-insert them

Depending on the framework you are using, this may be the easiest option, since you will have the records stored in the application tier as domain objects. Just wipe them out and re-insert them in the order desired.

This may not be feasible if you aren't using an EF or if there are FK relationships that would result in a cascading delete.

Option 4. Store the "order by" column in a separate table

Technically speaking this is the most normalized option, since sort order is probably not an attribute of the entity but is instead an attribute of the relationship between the entity and some other entity. So Order actually violates 3NF, believe it or not-- columns values should depend on the key (1NF), the whole key (2NF), and nothing but the primary key (3NF), and in this case the Order column depends on the entity's relationship within a greater context. To normalize this you should have a join table.

So let's say you have an invoice table and an line_item table. Add a third table, invoice_line_item table that contains InvoiceID, LineItemID, and SortOrder. If you wish to re-order the rows, delete all of them, and then re-insert them. This avoids any issues with FK or related objects, since those FKs would not be in this third table but one of the other tables. This amounts to changing the relationship between the line items and the invoices, so it makes sense to delete and replace them. Naturally you will want to wrap the whole thing in a transaction.

John Wu
  • 26,462
5

There are multiple approaches that you can use:

  1. The easiest one is to forgo the UNIQUE constraint on the column that specifies the ordering (either as linked list or as sequence number). It is then up to the software to ensure that eventually the database is consistent again.
  2. Use sequence numbers with gaps.
    If you initially start with sequence numbers like 10, 20, 30, etc., then you can re-order them by allocating sequence numbers in-between the ones already given out.

    For example, to put the fourth element between the first and second, the sequence numbers would become 10, 15, 20, 30, 50, etc.

    To avoid running out of gaps to place elements, you can either periodically normalize the sequence numbers, or you can use floating point sequence numbers.

  • I understand about gaps and use this pattern (with step of 10). But the most problem is UNIQUE constraint. Of course, I can forgot about it, but it's incorrectly. DB should have all necessary restrictions – user285336 Feb 24 '17 at 22:47
  • @user285336: If you use gaps (and ensure that there always remains a gap), then there is never a need to give two items the same ordering number. Not even temporarily, so you can keep the UNIQUE constraints. – Bart van Ingen Schenau Feb 25 '17 at 08:26
3

The unique constraint should be a combination of the Order column and a foreign key to a parent table. I'll use a "Shopping Cart" as an example.

Table: ShoppingCartItems
- ShoppingCartId (FK to ShoppingCarts table)
- DisplayNumber (int)

In this case, the unique constraint would ensure the combination of ShoppingCartItems.ShoppingCartId and ShoppingCartItems.DisplayNumber is unique within the table - so that a shopping cart cannot have more than one item with then same display order.

  • 2
    If Items.Id is the primary key, then the combination of that and any other column is trivially unique, because the primary key alone is already unique. – Bart van Ingen Schenau Feb 24 '17 at 18:54
  • 1
    @BartvanIngenSchenau: Corrected my answer. I wrote "primary key" and was thinking "foreign key". Sheesh. It's been one of those days. – Greg Burghardt Feb 24 '17 at 18:59
3

I use negative for temp

declare int @rowA = 7 
declare int @rowB = 22

update table set order = -@rowB where order =  @rowA; 
update table set order =  @rowA where order =  @rowB; 
update table set order =  @rowB where order = -@rowB;

You can wrap it in a transaction but most likely you are not going to get conflicts

paparazzo
  • 1,937
2

Three suggestions:

  1. Always make your updates in a sequence that does not cause duplicates. For example, to swap 2 and 3, first update 2 with null, then insert 2 to replace 3, then finally insert 3.
  2. You will probably need a query to insert a record in the middle of the sorted list. If you number your records in sequence, you will have to renumber many records to insert one. If you use a linked list, you only have to update two. For that reason, I expect you will find the linked list less expensive at run time, and probably easier to code.
  3. I think it is conventional to name your column sort_key, ordinal or perhaps position.

An open source implementation for Active Record in Ruby might inspire you. Read the source of the acts_as_list gem and compare it with these ideas.

dcorking
  • 586
  • OK. Then other's answers might work better for you, such as s dummy value, or temporarily moving one record to the end or head of the sequence. – dcorking Feb 25 '17 at 07:45