John Sheehan : Blog

About This Post

Slightly more dynamic ORDER BY in SQL Server 2005

Dynamically ordering SELECT statements in T-SQL has been an issue I’ve run into a lot. The options have traditionally been use dynamic SQL (eww) or a CASE statement in the ORDER BY. CASE kind of works, but its limited (only one column, no sort directions, datatype issues, etc).

I needed a way to dynamically sort the results of a stored procedure by multiple columns with different combinations of sort directions (e.g. Customer.LastName DESC, Customer.FirstName). I’m fortunate that in my situation there’s only six possible sort combinations based on CustomerTypeID. If you’re looking for a way to pass a string parameter to your stored procedure and sort by that; as far as I can tell you’re still stuck with the existing workarounds. Alright, let’s get into the example.

Create the following tables and test data. Remember, this example only works with SQL Server 2005.

CREATE TABLE Customer
(
    CustomerID int identity(1,1),
    FirstName nvarchar(50),
    LastName nvarchar(50),
    CustomerTypeID int
)

CREATE TABLE CustomerType
(
    CustomerTypeID int identity(1,1),
    CustomerType nvarchar(50)
)

--Insert test data

INSERT INTO CustomerType (CustomerType) VALUES('Type1') -- ID=1
INSERT INTO CustomerType (CustomerType) VALUES('Type2') -- ID=2
INSERT INTO CustomerType (CustomerType) VALUES('Type3') -- ID=3

INSERT INTO Customer (FirstName, LastName, CustomerTypeID) VALUES ('John', 'Doe', 1)
INSERT INTO Customer (FirstName, LastName, CustomerTypeID) VALUES ('Jane', 'Doe', 1)
INSERT INTO Customer (FirstName, LastName, CustomerTypeID) VALUES ('Steve', 'Jobs', 2)
INSERT INTO Customer (FirstName, LastName, CustomerTypeID) VALUES ('Steve', 'Wozniak', 2)
INSERT INTO Customer (FirstName, LastName, CustomerTypeID) VALUES ('Linus', 'Torvalds', 2)
INSERT INTO Customer (FirstName, LastName, CustomerTypeID) VALUES ('Bill', 'Gates', 3)
INSERT INTO Customer (FirstName, LastName, CustomerTypeID) VALUES ('Michael', 'Dell', 3)
INSERT INTO Customer (FirstName, LastName, CustomerTypeID) VALUES ('Steve', 'Ballmer', 3)
INSERT INTO Customer (FirstName, LastName, CustomerTypeID) VALUES ('Ray', 'Ozzie', 3)

Let’s say we need a procedure to return customers of a certain type. The proceedure needs to sort result sets with Customers of TypeID=1 by FirstName then LastName. Customers of TypeID=2 by LastName then FirstName and lastly Customers of TypeID=3 as LastName DESC then FirstName.

This is where SQL Server 2005 comes into play. SQL 2005 has introduced some new ranking functions like RANK(), ROW_NUMBER(), etc. We’re going to use RANK() here. Because RANK() accepts multiple ORDER BY statements, we can use the return value to order our result set.

First, let’s see the results of using the RANK() function in our stored procedure.

CREATE PROCEDURE CustomerGetWithRanks
AS
BEGIN
    SELECT
        CustomerID,
        FirstName,
        LastName,
        CustomerTypeID,
        RANK() OVER (ORDER BY FirstName, LastName) AS Type1SortRank,
        RANK() OVER (ORDER BY LastName, FirstName) AS Type2SortRank,
        RANK() OVER (ORDER BY LastName DESC, FirstName DESC) AS Type3SortRank
    FROM
        Customer
END

Results:

CustomerID FirstName LastName  CustomerTypeID Type1SortRank Type2SortRank Type3SortRank
---------- --------- --------- -------------- ------------- ------------- -------------
7          Steve     Wozniak   2              9             9             1
8          Linus     Torvalds  2              4             8             2
12         Ray       Ozzie     3              6             7             3
6          Steve     Jobs      2              8             6             4
9          Bill      Gates     3              1             5             5
4          John      Doe       1              3             4             6
5          Jane      Doe       1              2             3             7
10         Michael   Dell      3              5             2             8
11         Steve     Ballmer   3              7             1             9

You can see how each set of sort criteria would sort all of our test records. I only need to retrieve one type at a time however, so I’m going to combine this method with the previously limited CASE method.

The last piece of the puzzle is adding the logic to determine which sort order to use based on CustomerTypeID. We’re also going to narrow down our result set to only return a specific type of Customer and hide the RANK() output.

CREATE PROCEDURE CustomerGetFinal
    @CustomerTypeID int
AS
BEGIN
    SELECT
        CustomerID,
        FirstName,
        LastName,
        CustomerTypeID
    FROM
        Customer
    WHERE
        CustomerTypeID = @CustomerTypeID
    ORDER BY
        CASE
            WHEN @CustomerTypeID = 1 THEN (RANK() OVER (ORDER BY FirstName, LastName))
            WHEN @CustomerTypeID = 2 THEN (RANK() OVER (ORDER BY LastName, FirstName))
            WHEN @CustomerTypeID = 3 THEN (RANK() OVER (ORDER BY LastName DESC, FirstName DESC))
        END
END

(NOTE: If you want to output the RANK() result and order by the same value, you have to specify the whole line twice as SQL server doesn’t appear to be accepting dynamically created columns as ORDER BY parameters). Here are the results:

EXEC CustomerGetFinal 1

CustomerID FirstName LastName CustomerTypeID
---------- --------- -------- --------------
5          Jane      Doe      1
4          John      Doe      1

EXEC CustomerGetFinal 2

CustomerID FirstName LastName CustomerTypeID
---------- --------- -------- --------------
6          Steve     Jobs     2
8          Linus     Torvalds 2
7          Steve     Wozniak  2

EXEC CustomerGetFinal 3

CustomerID FirstName LastName CustomerTypeID
---------- --------- -------- --------------
12         Ray       Ozzie    3
9          Bill      Gates    3
10         Michael   Dell     3
11         Steve     Ballmer  3

kick it on DotNetKicks.com

  • Jared Roberts
    This is a great post... I have been trying to figure out a good way to do this forever. Good work.
  • Lamprey
    (NOTE: If you want to output the RANK() result and order by the same value, you have to specify the whole line twice as SQL server doesn’t appear to be accepting dynamically created columns as ORDER BY parameters). Here are the results:

    Almost, you can get the rank back without specifing it twice:

    CREATE PROCEDURE CustomerGetFinal
    @CustomerTypeID int
    AS
    BEGIN
    SELECT
    CustomerID,
    FirstName,
    LastName,
    CustomerTypeID,
    CASE
    WHEN @CustomerTypeID = 1 THEN (RANK() OVER (ORDER BY FirstName, LastName))
    WHEN @CustomerTypeID = 2 THEN (RANK() OVER (ORDER BY LastName, FirstName))
    WHEN @CustomerTypeID = 3 THEN (RANK() OVER (ORDER BY LastName DESC, FirstName DESC))
    END AS RankNumber
    FROM
    Customer
    WHERE
    CustomerTypeID = @CustomerTypeID
    ORDER BY
    RankNumber
    END
  • John
    I like it Lamprey!
  • Excellent post! A very clean way of writing SPs with dynamic order by.
  • Steve
    Absolute magic - very well explained and saved me a real headache. Thanks a million!
  • Ken
    This is a neat way of doing dynamic sorting but performance on my query is very bad with this method. Granted it is a large query. But other methods out perform this.
  • John
    Hi Ken,
    I kind of thought that would be the case for larger queries. I'm only using this method on a query for under 100 rows so I haven't run into any performance issues yet.
  • rob
    Very nice work. A fun trick to look at too! Thanks for sharing...
  • RedLine
    Great thanks!!! Very powerfull, clear and intelligence solution.
  • Francisco
    Excellent way.- Thanks
  • demvin01
    Great tip, thanks !

    About your sentence : "CASE kind of works, but its limited (only one column, no sort directions, datatype issues, etc)" I just want to say that you can control the direction with multiple CASE statements :

    Order By
    Case @orderby
    when 'DATEASC' then StartDate
    when 'NAMEASC' then LastName + FirstName
    end ASC,
    Case @orderby
    when 'DATEDESC' then StartDate
    when 'NAMEDESC' then LastName+ FirstName
    end DESC


    Also, I didn't run into any datatype problem in with the CASE, but I only tried with nvarchars and datetime, so I'm not sure about this.
  • Uncle
    Thank you soooo much. You saved my life! :)
  • Rash
    The great job......................easily explained
  • john
    An other alternative work around is not varchar columns casting sql_variant
    fitCount is int

    elect * from tFittest
    declare @sortCriteria varchar(50) set @sortCriteria = 'name'
    declare @sortDirection varchar(4) set @sortDirection = 'desc'


    select fitCount, name, wwid, @sortCriteria, @sortDirection, cast(fitCount as sql_variant)
    from tFittest
    order by
    case when @sortDirection = 'ASC' then
    case when @sortCriteria = 'fitCount' then (cast(fitCount as sql_variant) )
    when @sortCriteria = 'name' then (name )
    end
    end ASC,
    case when @sortDirection = 'DESC' then
    case when @sortCriteria = 'fitCount' then (cast(fitCount as sql_variant) )
    when @sortCriteria = 'name' then (name )
    end
    end DESC
  • Mab
    Thanks for the article.
    Just a quick question. Is it possible to write this dynamic Order By with DISTINCT, as I am getting error with DISTINCT in my select statement.
  • Alexander
    Great work, thanks!
  • li
    How can I use

    ORDER BY
    CASE @AccountOperator
    WHEN '>' THEN UserLastName
    END ASC,
    CASE @AccountOperator
    WHEN '<' THEN UserFirstName
    END DESC

    when UserLastName is an alias?(without using sub query)
  • Sorry, I don't know off hand. Try stackoverflow.com
  • Miguel_TX
    I just tried this way of sorting and for the query I was running (a very big one) it took twice as long to run than it did with my own dynamic sorting.

    Everyone, please use this instead - it's faster when it counts:
    ORDER BY
    (CASE WHEN @CustomerTypeID = 1 THEN FirstName END),
    (CASE WHEN @CustomerTypeID = 1 THEN LastName END),
    (CASE WHEN @CustomerTypeID = 2 THEN LastName END),
    (CASE WHEN @CustomerTypeID = 2 THEN FirstName END),
    (CASE WHEN @CustomerTypeID = 3 THEN LastName END) DESC,
    (CASE WHEN @CustomerTypeID = 3 THEN FirstName END) DESC

    I ask that the author of this post (John Sheehan) try it and update this post with his findings if he finds that it works better. Thank you.

    I can't take credit for this way of doing it as I found it months ago on another posting - but I can't remember where.

    - Miguel_TX
  • Miguel_TX
    I forgot to mention that this way should also work in SQL Server 2000 for anyone still using that.
blog comments powered by Disqus