Dynamic Sorting within SQL Stored Procedures
Asked Answered
V

15

137

This is an issue that I've spent hours researching in the past. It seems to me to be something that should have been addressed by modern RDBMS solutions but as yet I have not found anything that really addresses what I see to be an incredibly common need in any Web or Windows application with a database back-end.

I speak of dynamic sorting. In my fantasy world, it should be as simple as something like:

ORDER BY @sortCol1, @sortCol2

This is the canonical example given by newbie SQL and Stored Procedure developers all over forums across the Internet. "Why isn't this possible?" they ask. Invariably, somebody eventually comes along to lecture them about the compiled nature of stored procedures, of execution plans in general, and all sorts of other reasons why it isn't possible to put a parameter directly into an ORDER BY clause.


I know what some of you are already thinking: "Let the client do the sorting, then." Naturally, this offloads the work from your database. In our case though, our database servers aren't even breaking a sweat 99% of the time and they aren't even multi-core yet or any of the other myriad improvements to system architecture that happen every 6 months. For this reason alone, having our databases handle sorting wouldn't be a problem. Additionally, databases are very good at sorting. They are optimized for it and have had years to get it right, the language for doing it is incredibly flexible, intuitive, and simple and above all any beginner SQL writer knows how to do it and even more importantly they know how to edit it, make changes, do maintenance, etc. When your databases are far from being taxed and you just want to simplify (and shorten!) development time this seems like an obvious choice.

Then there's the web issue. I've played around with JavaScript that will do client-side sorting of HTML tables, but they inevitably aren't flexible enough for my needs and, again, since my databases aren't overly taxed and can do sorting really really easily, I have a hard time justifying the time it would take to re-write or roll-my-own JavaScript sorter. The same generally goes for server-side sorting, though it is already probably much preferred over JavaScript. I'm not one that particularly likes the overhead of DataSets, so sue me.

But this brings back the point that it isn't possible — or rather, not easily. I've done, with prior systems, an incredibly hack way of getting dynamic sorting. It wasn't pretty, nor intuitive, simple, or flexible and a beginner SQL writer would be lost within seconds. Already this is looking to be not so much a "solution" but a "complication."


The following examples are not meant to expose any sort of best practices or good coding style or anything, nor are they indicative of my abilities as a T-SQL programmer. They are what they are and I fully admit they are confusing, bad form, and just plain hack.

We pass an integer value as a parameter to a stored procedure (let's call the parameter just "sort") and from that we determine a bunch of other variables. For example... let's say sort is 1 (or the default):

DECLARE @sortCol1 AS varchar(20)
DECLARE @sortCol2 AS varchar(20)
DECLARE @dir1 AS varchar(20)
DECLARE @dir2 AS varchar(20)
DECLARE @col1 AS varchar(20)
DECLARE @col2 AS varchar(20)

SET @col1 = 'storagedatetime';
SET @col2 = 'vehicleid';

IF @sort = 1                -- Default sort.
BEGIN
    SET @sortCol1 = @col1;
    SET @dir1 = 'asc';
    SET @sortCol2 = @col2;
    SET @dir2 = 'asc';
END
ELSE IF @sort = 2           -- Reversed order default sort.
BEGIN
    SET @sortCol1 = @col1;
    SET @dir1 = 'desc';
    SET @sortCol2 = @col2;
    SET @dir2 = 'desc';
END

You can already see how if I declared more @colX variables to define other columns I could really get creative with the columns to sort on based on the value of "sort"... to use it, it usually ends up looking like the following incredibly messy clause:

ORDER BY
    CASE @dir1
        WHEN 'desc' THEN
            CASE @sortCol1
                WHEN @col1 THEN [storagedatetime]
                WHEN @col2 THEN [vehicleid]
            END
    END DESC,
    CASE @dir1
        WHEN 'asc' THEN
            CASE @sortCol1
                WHEN @col1 THEN [storagedatetime]
                WHEN @col2 THEN [vehicleid]
            END
    END,
    CASE @dir2
        WHEN 'desc' THEN
            CASE @sortCol2
                WHEN @col1 THEN [storagedatetime]
                WHEN @col2 THEN [vehicleid]
            END
    END DESC,
    CASE @dir2
        WHEN 'asc' THEN
            CASE @sortCol2
                WHEN @col1 THEN [storagedatetime]
                WHEN @col2 THEN [vehicleid]
            END
    END

Obviously this is a very stripped down example. The real stuff, since we usually have four or five columns to support sorting on, each with possible secondary or even a third column to sort on in addition to that (for example date descending then sorted secondarily by name ascending) and each supporting bi-directional sorting which effectively doubles the number of cases. Yeah... it gets hairy really quick.

The idea is that one could "easily" change the sort cases such that vehicleid gets sorted before the storagedatetime... but the pseudo-flexibility, at least in this simple example, really ends there. Essentially, each case that fails a test (because our sort method doesn't apply to it this time around) renders a NULL value. And thus you end up with a clause that functions like the following:

ORDER BY NULL DESC, NULL, [storagedatetime] DESC, blah blah

You get the idea. It works because SQL Server effectively ignores null values in order by clauses. This is incredibly hard to maintain, as anyone with any basic working knowledge of SQL can probably see. If I've lost any of you, don't feel bad. It took us a long time to get it working and we still get confused trying to edit it or create new ones like it. Thankfully it doesn't need changing often, otherwise it would quickly become "not worth the trouble."

Yet it did work.


My question is then: is there a better way?

I'm okay with solutions other than Stored Procedure ones, as I realize it may just not be the way to go. Preferably, I'd like to know if anyone can do it better within the Stored Procedure, but if not, how do you all handle letting the user dynamically sort tables of data (bi-directionally, too) with ASP.NET?

And thank you for reading (or at least skimming) such a long question!

PS: Be glad I didn't show my example of a stored procedure that supports dynamic sorting, dynamic filtering/text-searching of columns, pagination via ROWNUMBER() OVER, AND try...catch with transaction rollbacking on errors... "behemoth-sized" doesn't even begin to describe them.


Update:

  • I would like to avoid dynamic SQL. Parsing a string together and running an EXEC on it defeats a lot of the purpose of having a stored procedure in the first place. Sometimes I wonder though if the cons of doing such a thing wouldn't be worth it, at least in these special dynamic sorting cases. Still, I always feel dirty whenever I do dynamic SQL strings like that — like I'm still living in the Classic ASP world.
  • A lot of the reason we want stored procedures in the first place is for security. I don't get to make the call on security concerns, only suggest solutions. With SQL Server 2005 we can set permissions (on a per-user basis if need be) at the schema level on individual stored procedures and then deny any queries against the tables directly. Critiquing the pros and cons of this approach is perhaps for another question, but again it's not my decision. I'm just the lead code monkey. :)
Vig answered 29/9, 2008 at 16:4 Comment(2)
Refer #3660481 too -- SQL Server dynamic ORDER BY with mixed datatypesNoisy
Dynamic SQL is the FAR superior way... IF [and this is a big IF] ..your Data Access Layer is strict and your dynamic SQL is generated by a system that is rigidly programmed with RDBMS rules expressed in perfect form. An Algorithmically Engineered Database Architecture is a thing of beauty...Pervious
O
110

Yeah, it's a pain, and the way you're doing it looks similar to what I do:

order by
case when @SortExpr = 'CustomerName' and @SortDir = 'ASC' 
    then CustomerName end asc, 
case when @SortExpr = 'CustomerName' and @SortDir = 'DESC' 
    then CustomerName end desc,
...

This, to me, is still much better than building dynamic SQL from code, which turns into a scalability and maintenance nightmare for DBAs.

What I do from code is refactor the paging and sorting so I at least don't have a lot of repetition there with populating values for @SortExpr and @SortDir.

As far as the SQL is concerned, keep the design and formatting the same between different stored procedures, so it's at least neat and recognizable when you go in to make changes.

Orangery answered 29/9, 2008 at 16:42 Comment(5)
Exactly. My goal was to avoid doing an EXEC command on a big 5000 varchar string. Everything we do must be done via stored procedures if only for the added security since we can set permissions on them at the schema level. The scalability and performance gains are just a plus in our case.Vig
Add maintainability to {security, scalability, performance}. Once you have 3 or 4 apps out there with dynamic SQL running against your DB, you are screwed, you can't change anything, especially as the apps age and developers move on. Exec and dynamic sql are evil.Orangery
That's just it --- we already do, from way before I got here, for all of the still running Classic ASP web apps and many, many Access VB apps still circulating. I twitch and have to hold back urges to fix glaring mistakes any time I have to perform maintenance on any of them.Vig
This is what I do also, except I encode the direction into the SortExpr: ORDER BY CASE WHEN sort = 'FirstName' THEN FirstName END ASC, CASE WHEN sort = '-FirstName' THEN FirstName END DESCEddra
This is THE nightmare for DBAs and software engineers alike. So instead of being able to have dynamic but strict systems that generate expressive SQL statements based on your information schema you have this disgusting glop of hardcoded gibberish. It's poor programming at it's finest.Pervious
M
26

This approach keeps the sortable columns from being duplicated twice in the order by, and is a little more readable IMO:

SELECT
  s.*
FROM
  (SELECT
    CASE @SortCol1
      WHEN 'Foo' THEN t.Foo
      WHEN 'Bar' THEN t.Bar
      ELSE null
    END as SortCol1,
    CASE @SortCol2
      WHEN 'Foo' THEN t.Foo
      WHEN 'Bar' THEN t.Bar
      ELSE null
    END as SortCol2,
    t.*
  FROM
    MyTable t) as s
ORDER BY
  CASE WHEN @dir1 = 'ASC'  THEN SortCol1 END ASC,
  CASE WHEN @dir1 = 'DESC' THEN SortCol1 END DESC,
  CASE WHEN @dir2 = 'ASC'  THEN SortCol2 END ASC,
  CASE WHEN @dir2 = 'DESC' THEN SortCol2 END DESC
Matroclinous answered 30/9, 2008 at 4:35 Comment(1)
This seemed like a good answer but doesnt seem to work when the sortable columns have different datatypesPerforming
D
7

My applications do this a lot but they are all dynamically building the SQL. However, when I deal with stored procedures I do this:

  1. Make the stored procedure a function that returns a table of your values - no sort.
  2. Then in your application code do a select * from dbo.fn_myData() where ... order by ... so you can dynamically specify the sort order there.

Then at least the dynamic part is in your application, but the database is still doing the heavy lifting.

Desberg answered 29/9, 2008 at 19:8 Comment(2)
That's probably the best compromise I've seen yet between using dynamic SQL and stored procedures together. I like it. I might experiment sometime with a similar approach, but such a change would be prohibitive in any of our existing on-going projects.Vig
You can achieve the same using a local table variable instead of tabular function returning data. I find local tables more flexible than functions, as you can output some debug info.Ridley
C
7

A stored procedure technique (hack?) I've used to avoid dynamic SQL for certain jobs is to have a unique sort column. I.e.,

SELECT
   name_last,
   name_first,
   CASE @sortCol WHEN 'name_last' THEN [name_last] ELSE 0 END as mySort
FROM
   table
ORDER BY 
    mySort

This one is easy to beat into submission -- you can concat fields in your mySort column, reverse the order with math or date functions, etc.

Preferably though, I use my asp.net gridviews or other objects with build-in sorting to do the sorting for me AFTER retrieving the data fro Sql-Server. Or even if it's not built-in -- e.g., datatables, etc. in asp.net.

Conchaconchie answered 2/8, 2010 at 17:17 Comment(0)
T
6

Dynamic SQL is still an option. You just have to decide whether that option is more palatable than what you currently have.

Here is an article that shows that: https://web.archive.org/web/20211029044050/https://www.4guysfromrolla.com/webtech/010704-1.shtml.

Tenorrhaphy answered 29/9, 2008 at 16:28 Comment(0)
S
4

There may be a third option, since your server has lots of spare cycles - use a helper procedure to do the sorting via a temporary table. Something like

create procedure uspCallAndSort
(
    @sql varchar(2048),        --exec dbo.uspSomeProcedure arg1,'arg2',etc.
    @sortClause varchar(512)    --comma-delimited field list
)
AS
insert into #tmp EXEC(@sql)
declare @msql varchar(3000)
set @msql = 'select * from #tmp order by ' + @sortClause
EXEC(@msql)
drop table #tmp
GO

Caveat: I haven't tested this, but it "should" work in SQL Server 2005 (which will create a temporary table from a result set without specifying the columns in advance.)

Schoen answered 29/9, 2008 at 16:49 Comment(0)
I
4

There's a couple of different ways you can hack this in.

Prerequisites:

  1. Only one SELECT statement in the sp
  2. Leave out any sorting (or have a default)

Then insert into a temp table:

create table #temp ( your columns )

insert #temp
exec foobar

select * from #temp order by whatever

Method #2: set up a linked server back to itself, then select from this using openquery: http://www.sommarskog.se/share_data.html#OPENQUERY

Ibis answered 29/9, 2008 at 22:17 Comment(0)
H
2

At some point, doesn't it become worth it to move away from stored procedures and just use parameterized queries to avoid this sort of hackery?

Hertahertberg answered 29/9, 2008 at 16:27 Comment(5)
In certain cases maybe they're sledgehammers on a nail, but often we want to set permissions (EXECUTE in particular) directly on the stored procedures and disallow any SQL queries directly against the tables, even SELECTs. I don't much like the hackery either, but security is not my call to make.Vig
This is why so many people are moving to Object Relational Mapping. Needless round trips for sorting, huge CASE blocks for the same, senseless updates to tons of columns when really only one needed to be updated, etc. The one winning argument for stored procedures that still remains is the security.Anarchism
I'm moving from an ORM (EF) to a stored procedure because the ORM doesn't support full text search.Benzine
@RonnieOverby Full-text search is often better-served by a dedicated solution, e.g., Lucene.Hertahertberg
@HankGay I have the strange feeling that entity framework also doesn't support Lucene.Benzine
A
2

I agree, use client side. But it appears that is not the answer you want to hear.

So, it is perfect the way it is. I don't know why you would want to change it, or even ask "Is there a better way." Really, it should be called "The Way". Besides, it seems to work and suit the needs of the project just fine and will probably be extensible enough for years to come. Since your databases aren't taxed and sorting is really really easy it should stay that way for years to come.

I wouldn't sweat it.

Arrack answered 29/9, 2008 at 18:0 Comment(3)
I have no problem with client-side, as I go that route with Windows apps. But what about web apps? I don't much find any JavaScript solution really flexible enough. And yes, it does work as I said the way we have it, but it's a nightmare of SQL. Of course I'd like to know if there are better ways.Vig
It is built into the newer (2.0 and up) .NET controls. Or you can create your own and apply it to a dataview. msdn.microsoft.com/en-us/library/hwf94875(VS.80).aspxArrack
My problem then is one of scalability and performance. Doing client-side or web-server-side sorting requires loading all of the data instead of just the 10 or 15 you're going to display on a page at a time. This is extremely costly, in the long run, whereas database sorting doesn't have that.Vig
W
2

When you are paging sorted results, dynamic SQL is a good option. If you're paranoid about SQL injection you can use the column numbers instead of the column name. I've done this before using negative values for descending. Something like this...

declare @o int;
set @o = -1;

declare @sql nvarchar(2000);
set @sql = N'select * from table order by ' + 
    cast(abs(@o) as varchar) + case when @o < 0 then ' desc' else ' asc' end + ';'

exec sp_executesql @sql

Then you just need to make sure the number is inside 1 to # of columns. You could even expand this to a list of column numbers and parse that into a table of ints using a function like this. Then you would build the order by clause like so...

declare @cols varchar(100);
set @cols = '1 -2 3 6';

declare @order_by varchar(200)

select @order_by = isnull(@order_by + ', ', '') + 
        cast(abs(number) as varchar) + 
        case when number < 0 then ' desc' else '' end
from dbo.iter_intlist_to_tbl(@cols) order by listpos

print @order_by

One drawback is you have to remember the order of each column on the client side. Especially, when you don't display all the columns or you display them in a different order. When the client wants to sort, you map the column names to the column order and generate the list of ints.

Woolfolk answered 7/5, 2009 at 20:13 Comment(1)
We use sp_executesql for building dynamic reporting queries. Very effective. The SQL can't be built from the application, but the parameters are just inserted where needed and exec'd as normal.Couteau
C
2

An argument against doing the sorting on the client side is large volume data and pagination. Once your row count gets beyond what you can easily display you're often sorting as part of a skip/take, which you probably want to run in SQL.

For Entity Framework, you could use a stored procedure to handle your text search. If you encounter the same sort issue, the solution I've seen is to use a stored proc for the search, returning only an id key set for the match. Next, re-query (with the sort) against the db using the ids in a list (contains). EF handles this pretty well, even when the ID set is pretty large. Yes, this is two round trips, but it allows you to always keep your sorting in the DB, which can be important in some situations, and prevents you from writing a boatload of logic in the stored procedure.

Cleaves answered 7/1, 2014 at 18:56 Comment(0)
O
2

Sorry I'm late to the party, but here's another option for those who really want to avoid dynamic SQL, but want the flexibility it offers:

Instead of dynamically generating the SQL on the fly, write code to generate a unique proc for every possible variation. Then you can write a method in the code to look at the search options and have it choose the appropriate proc to call.

If you only have a few variations then you can just create the procs by hand. But if you have a lot of variations then instead of having to maintain them all, you would just maintain your proc generator instead to have it recreate them.

As an added benefit, you'll get better SQL plans for better performance doing it this way too.

Oppilate answered 19/12, 2018 at 22:46 Comment(0)
P
1

How about handling sorting on the stuff displaying the results -- grids, reports, etc. rather than on SQL?

EDIT:

To clarify things since this answer got down-voted earlier, I'll elaborate a bit...

You stated you knew about client-side sorting but wanted to steer clear of it. That's your call, of course.

What I want to point out, though, is that by doing it on the client-side, you're able to pull data ONCE and then work with it however you want -- versus doing multiple trips back and forth to the server each time the sort gets changed.

Your SQL Server isn't getting taxed right now and that's awesome. It shouldn't be. But just because it isn't overloaded yet doesn't mean that it'll stay like that forever.

If you're using any of the newer ASP.NET stuff for displaying on the web, a lot of that stuff is already baked right in.

Is it worth adding so much code to each stored procedure just to handle sorting? Again, your call.

I'm not the one who will ultimately be in charge of supporting it. But give some thought to what will be involved as columns are added/removed within the various datasets used by the stored procedures (requiring modifications to the CASE statements) or when suddenly instead of sorting by two columns, the user decides they need three -- requiring you to now update every one of your stored procedures that uses this method.

For me, it's worth it to get a working client-side solution and apply it to the handful of user-facing displays of data and be done with it. If a new column is added, it's already handled. If the user wants to sort by multiple columns, they can sort by two or twenty of them.

Photography answered 29/9, 2008 at 16:6 Comment(7)
That would be the right way, but is not considered "a better way"Arrack
Because then I'm still writing my own sorting in either C# or JavaScript and it seems like it should be so much easier and quicker in SQL. Thus my question. Was I just missing something obvious or are we stuck writing our own custom sorting (in C# or JavaScript) every damn app we work on?Vig
Wait, what about result sets with tens of thousands of rows? You can't return all that data to the client. You have to do paging and sorting on the database.Orangery
Yadyn, understood. But once you have a generic sorter for your grids, you just use that for all of your stuff.Photography
Eric, True... In cases like that, you do need extra handling and maybe it would make sense within SQL. It's far from a right vs. wrong issue. In some cases, it'll make sense for SQL and some cases, on the client.Photography
Another reason why I can't believe something doesn't already exist on databases to help this. You might not have a zillion rows yet that makes client or even web server sorting a problem, but you might. It then makes sense, were it available, to always have the database handle it.Vig
Yeah, I'm surprised too. When I saw your question I was really hoping somebody would have an answer for doing this within a proc without exec.Orangery
T
-1

This solution might only work in .NET, I don't know.

I fetch the data into the C# with the initial sort order in the SQL order by clause, put that data in a DataView, cache it in a Session variable, and use it to build a page.

When the user clicks on a column heading to sort (or page, or filter), I don't go back to the database. Instead, I go back to my cached DataView and set its "Sort" property to an expression I build dynamically, just like I would dynamic SQL. ( I do the filtering the same way, using the "RowFilter" property).

You can see/feel it working in a demo of my app, BugTracker.NET, at http://ifdefined.com/btnet/bugs.aspx

Thaumatology answered 30/9, 2008 at 4:11 Comment(1)
SWEET! Bug tracker.NET rocks!Wineshop
O
-8

You should avoid the SQL Server sorting, unless if necessary. Why not sort on app server or client side? Also .NET Generics does exceptional sortin

Osteoma answered 30/9, 2008 at 4:16 Comment(1)
Because of scalability. It's fine for a few thousand rows, but I don't want to be pulling down ten thousand and sorting that. Or more. Also, what about paging? I often only want to pull in what I need to display. Sorting rows 21-30 of 24056 after the fact would be incorrect.Vig

© 2022 - 2024 — McMap. All rights reserved.