41

Let's say I have a Product table in a shopping site's database to keep description, price, etc of store's products. What is the most efficient way to make my client able to re-order these products?

I create an Order column (integer) to use for sorting records but that gives me some headaches regarding performance due to the primitive methods I use to change the order of every record after the one I actually need to change. An example:

Id    Order
5     3
8     1
26    2
32    5
120   4

Now what can I do to change the order of the record with ID=26 to 3?

What I did was creating a procedure which checks whether there is a record in the target order (3) and updates the order of the row (ID=26) if not. If there is a record in target order the procedure executes itself sending that row's ID with target order + 1 as parameters.

That causes to update every single record after the one I want to change to make room:

Id    Order
5     4
8     1
26    3
32    6
120   5

So what would a smarter person do?

  • I use SQL Server 2008 R2.

Edit:

I need the order column of an item to be enough for sorting with no secondary keys involved. Order column alone must specify a unique place for its record.

In addition to all, I wonder if I can implement something like of a linked list: A 'Next' column instead of an 'Order' column to keep the next items ID. But I have no idea how to write the query that retrieves the records with correct order. If anyone has an idea about this approach as well, please share.

Şafak Gür
  • 7,045
  • 5
  • 59
  • 96
  • 15
    A more experienced person wouldn't call a column `Order` since that's already a reserved keyword in SQL .... :-) – marc_s Dec 22 '11 at 18:20

6 Answers6

40
Update product set order = order+1 where order >= @value changed

Though over time you'll get larger and larger "spaces" in your order but it will still "sort"

This will add 1 to the value being changed and every value after it in one statement, but the above statement is still true. larger and larger "spaces" will form in your order possibly getting to the point of exceeding an INT value.

Alternate solution given desire for no spaces:

Imagine a procedure for: UpdateSortOrder with parameters of @NewOrderVal, @IDToChange,@OriginalOrderVal

Two step process depending if new/old order is moving up or down the sort.

If @NewOrderVal < @OriginalOrderVal --Moving down chain 

--Create space for the movement; no point in changing the original 
    Update product set order = order+1 
    where order BETWEEN @NewOrderVal and @OriginalOrderVal-1;

end if

If @NewOrderVal > @OriginalOrderVal --Moving up chain

--Create space  for the momvement; no point in changing the original  
  Update product set order = order-1 
  where order between @OriginalOrderVal+1 and @NewOrderVal
end if

--Finally update the one we moved to correct value

    update product set order = @newOrderVal where ID=@IDToChange;

Regarding best practice; most environments I've been in typically want something grouped by category and sorted alphabetically or based on "popularity on sale" thus negating the need to provide a user defined sort.

xQbert
  • 34,733
  • 2
  • 41
  • 62
  • well you could have the user click a "DONE" button which would then use set the "Order" field = rownum based on a query sorted by "order" – xQbert Dec 24 '11 at 17:04
  • 1
    A good solution, indeed. Thank you. I have one more question though: You updated the records in between before you updated the one needed to change in your second example. That means there will be two rows sharing the same order value for a moment. How can I be able to do this if I have a unique constraint on the Order column? – Şafak Gür Jan 06 '12 at 10:41
  • 2
    Didn't know about the unique constraint; could either turn it off for the moment while this is being done: or start the procedure off by setting the `update product set order = -1 where ID = @IDtoChange;` and then the last thing we do is set the -1 to the value we care about. via the update above. Sadly this assumes that -1 is an allowed value and now has "Special meaning" in this case. (but only for a fraction of a second while this procedure (that should be in transaction logic) runs) but as our update is based on ID, and in transaction, the -1 shouldn't cause a violation of constraints. – xQbert Jan 06 '12 at 23:44
  • 2
    'BETWEEN' is asymmetric by default, so e.g. BETWEEN 3 AND 2 returns an empty result. I edited the answer to reflect this. – törzsmókus May 01 '13 at 04:35
  • Don´t fully understand, this is something you need to call with a foreach function and loop all your items? It will go through all values? – daniel_aren Feb 22 '18 at 14:50
  • no loop needed. It updates an entire set of data defined by the table and where clauses. It would be wise to have it in a procedure and call the procedure which handles the multiple updates. Only two update statements will be executed. one of the "IF's" and the setting of the value you desire. The If statements just create the space if needed. – xQbert Feb 22 '18 at 14:55
  • 1
    Guess this doens´t work in my scenario, I don´t know the new value. I just need to "clean" the sort order table after deleting a row in the middle.. – daniel_aren Feb 22 '18 at 15:09
  • 1
    In that case I'd write a simple update after the delete. You know (should know or could get) the sortOrder of the record deleted so just .. `Update tableName Set SortOrder = SortOrder-1 where SortOrder > @SortOrderDeleted` this would close the gap anytime a record is deleted. it may result in no records updated if the last record is deleted; but it would/should still run/execute just no changes. – xQbert Feb 22 '18 at 15:10
8

Use the old trick that BASIC programs (amongst other places) used: jump the numbers in the order column by 10 or some other convenient increment. You can then insert a single row (indeed, up to 9 rows, if you're lucky) between two existing numbers (that are 10 apart). Or you can move row 370 to 565 without having to change any of the rows from 570 upwards.

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
7

Here is an alternative approach using a common table expression (CTE).

This approach respects a unique index on the SortOrder column, and will close any gaps in the sort order sequence that may have been left over from earlier DELETE operations.

/* For example, move Product with id = 26 into position 3 */
DECLARE @id int = 26
DECLARE @sortOrder int = 3


;WITH Sorted AS (
    SELECT  Id,
            ROW_NUMBER() OVER (ORDER BY SortOrder) AS RowNumber
    FROM    Product
    WHERE   Id <> @id
)

UPDATE  p
SET     p.SortOrder = 
        (CASE 
            WHEN p.Id = @id THEN @sortOrder
            WHEN s.RowNumber >= @sortOrder THEN s.RowNumber + 1
            ELSE s.RowNumber
        END)
FROM    Product p
        LEFT JOIN Sorted s ON p.Id = s.Id 
cwills
  • 2,136
  • 23
  • 17
6

It is very simple. You need to have "cardinality hole".

Structure: you need to have 2 columns:

  1. pk = 32bit int

  2. order = 64bit bigint (BIGINT, NOT DOUBLE!!!)

Insert/UpdateL

  1. When you insert first new record you must set order = round(max_bigint / 2).

  2. If you insert at the beginning of the table, you must set order = round("order of first record" / 2)

  3. If you insert at the end of the table, you must set order = round("max_bigint - order of last record" / 2)

  4. If you insert in the middle, you must set order = round("order of record before - order of record after" / 2)

This method has a very big cardinality. If you have constraint error or if you think what you have small cardinality you can rebuild order column (normalize).

In maximality situation with normalization (with this structure) you can have "cardinality hole" in 32 bit.

It is very simple and fast!

Remember NO DOUBLE!!! Only INT - order is precision value!

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
user2382679
  • 179
  • 2
  • 2
5

One solution I have used in the past, with some success, is to use a 'weight' instead of 'order'. Weight being the obvious, the heavier an item (ie: the lower the number) sinks to the bottom, the lighter (higher the number) rises to the top.

In the event I have multiple items with the same weight, I assume they are of the same importance and I order them alphabetically.

This means your SQL will look something like this:

ORDER BY 'weight', 'itemName'

hope that helps.

PseudoNinja
  • 2,846
  • 1
  • 27
  • 37
  • Thank you. That's a solution but it is not different than my current method. It is my fault though because I should have specified that I need the order column to be unique. (I'll update the question right away) I need the client to be able to precisely choose which item comes first. I don't have a secondary condition like Name, in this case the Order must be enough. If I have two items weighted 10 and 11, how can I make a record appear between them without changing their weights (and consequently weights of the ones after them) was my actual question. – Şafak Gür Dec 24 '11 at 10:04
  • Does the order have to be unique-with-no-gaps? If so, why? The technique outlined can perfectly well have a unique constraint on the column, so the order is unique. – Jonathan Leffler Dec 24 '11 at 17:02
  • Sorry, I don't get it. Gaps aren't the problem. I just don't understand what is going to happen when two items have the same weight or how can I make a record appear between two records weighted 10 and 11 (like I mentioned in my previous comment). Especially if I have a unique constraint on the column. I'd have to change one of the other items' weight in that case. Are you suggesting that I shouldn't increase/decrease the weight by 1? I apology if I'm missing something obvious here. – Şafak Gür Jan 06 '12 at 10:07
  • 1
    I usually do my weighting in increments of 10 that way if I do need to wedge something in somewhere special, I have room. – PseudoNinja Jan 06 '12 at 14:39
  • Why do you have to use `int` as the datatype for the ordering column? There is plenty of space between 10 and 11 if you are using floats... – charlesdeb Aug 19 '22 at 00:42
4

I am currently developing a database with a tree structure that needs to be ordered. I use a link-list kind of method that will be ordered on the client (not the database). Ordering could also be done in the database via a recursive query, but that is not necessary for this project.

I made this document that describes how we are going to implement storage of the sort order, including an example in postgresql. Please feel free to comment!

https://docs.google.com/document/d/14WuVyGk6ffYyrTzuypY38aIXZIs8H-HbA81st-syFFI/edit?usp=sharing

Elmer
  • 9,147
  • 2
  • 48
  • 38
  • +1, nice sample. I have two questions: **1st:** Why do you have both `Parent` and `Preceeding` - Why double linked list instead of a linked list? **2nd:** How do you select the ordered records: Select first 100 records as ordered by their parent/child relationships, for example. – Şafak Gür Oct 14 '14 at 13:50
  • parent refers to the parent node in a tree structure. The preceding column defines the order of the nodes. I recently updated the example in the document with a SELECT that gets the nodes in their correct order via a recursive common table expression. – Elmer Oct 17 '14 at 04:00